1
1
#!/usr/bin/env node
2
- // @ts -check
3
2
4
- import fs from 'fs'
5
- import path from 'path'
3
+ import * as fs from 'fs'
4
+ import * as path from 'path'
6
5
7
6
import minimist from 'minimist'
8
7
import prompts from 'prompts'
9
8
import { red , green , bold } from 'kolorist'
10
9
11
- import renderTemplate from './utils/renderTemplate.js'
12
- import { postOrderDirectoryTraverse , preOrderDirectoryTraverse } from './utils/directoryTraverse.js'
13
- import generateReadme from './utils/generateReadme.js'
14
- import getCommand from './utils/getCommand.js'
10
+ import renderTemplate from './utils/renderTemplate'
11
+ import { postOrderDirectoryTraverse , preOrderDirectoryTraverse } from './utils/directoryTraverse'
12
+ import generateReadme from './utils/generateReadme'
13
+ import getCommand from './utils/getCommand'
14
+ import renderEslint from './utils/renderEslint'
15
+ import banner from './utils/banner'
15
16
16
17
function isValidPackageName ( projectName ) {
17
18
return / ^ (?: @ [ a - z 0 - 9 - * ~ ] [ a -z 0 -9 -* ._ ~ ] * \/ ) ? [ a - z 0 - 9 - ~ ] [ a - z 0 - 9 - ._ ~ ] * $ / . test ( projectName )
@@ -31,6 +32,10 @@ function canSafelyOverwrite(dir) {
31
32
}
32
33
33
34
function emptyDir ( dir ) {
35
+ if ( ! fs . existsSync ( dir ) ) {
36
+ return
37
+ }
38
+
34
39
postOrderDirectoryTraverse (
35
40
dir ,
36
41
( dir ) => fs . rmdirSync ( dir ) ,
@@ -39,45 +44,76 @@ function emptyDir(dir) {
39
44
}
40
45
41
46
async function init ( ) {
47
+ console . log ( `\n${ banner } \n` )
48
+
42
49
const cwd = process . cwd ( )
43
50
// possible options:
44
51
// --default
45
52
// --typescript / --ts
53
+ // --jsx
46
54
// --router / --vue-router
47
55
// --pinia
48
- // --with-tests / --tests / --cypress
56
+ // --with-tests / --tests (equals to `--vitest --cypress`)
57
+ // --vitest
58
+ // --cypress
59
+ // --eslint
60
+ // --eslint-with-prettier (only support prettier through eslint for simplicity)
49
61
// --force (for force overwriting)
50
62
const argv = minimist ( process . argv . slice ( 2 ) , {
51
63
alias : {
52
64
typescript : [ 'ts' ] ,
53
- 'with-tests' : [ 'tests' , 'cypress' ] ,
65
+ 'with-tests' : [ 'tests' ] ,
54
66
router : [ 'vue-router' ]
55
67
} ,
56
68
// all arguments are treated as booleans
57
69
boolean : true
58
70
} )
59
71
60
72
// if any of the feature flags is set, we would skip the feature prompts
61
- // use `??` instead of `||` once we drop Node.js 12 support
62
73
const isFeatureFlagsUsed =
63
- typeof ( argv . default || argv . ts || argv . router || argv . pinia || argv . tests ) === 'boolean'
74
+ typeof (
75
+ argv . default ??
76
+ argv . ts ??
77
+ argv . jsx ??
78
+ argv . router ??
79
+ argv . pinia ??
80
+ argv . tests ??
81
+ argv . vitest ??
82
+ argv . cypress ??
83
+ argv . eslint
84
+ ) === 'boolean'
64
85
65
86
let targetDir = argv . _ [ 0 ]
66
87
const defaultProjectName = ! targetDir ? 'vue-project' : targetDir
67
88
68
89
const forceOverwrite = argv . force
69
90
70
- let result = { }
91
+ let result : {
92
+ projectName ?: string
93
+ shouldOverwrite ? : boolean
94
+ packageName ? : string
95
+ needsTypeScript ? : boolean
96
+ needsJsx ? : boolean
97
+ needsRouter ? : boolean
98
+ needsPinia ? : boolean
99
+ needsVitest ? : boolean
100
+ needsCypress ? : boolean
101
+ needsEslint ? : boolean
102
+ needsPrettier ? : boolean
103
+ } = { }
71
104
72
105
try {
73
106
// Prompts:
74
107
// - Project name:
75
108
// - whether to overwrite the existing directory or not?
76
109
// - enter a valid package name for package.json
77
110
// - Project language: JavaScript / TypeScript
111
+ // - Add JSX Support?
78
112
// - Install Vue Router for SPA development?
79
113
// - Install Pinia for state management?
80
114
// - Add Cypress for testing?
115
+ // - Add ESLint for code quality?
116
+ // - Add Prettier for code formatting?
81
117
result = await prompts (
82
118
[
83
119
{
@@ -99,7 +135,7 @@ async function init() {
99
135
} ,
100
136
{
101
137
name : 'overwriteChecker' ,
102
- type : ( prev , values = { } ) => {
138
+ type : ( prev , values ) => {
103
139
if ( values . shouldOverwrite === false ) {
104
140
throw new Error ( red ( '✖' ) + ' Operation cancelled' )
105
141
}
@@ -121,6 +157,14 @@ async function init() {
121
157
active : 'Yes' ,
122
158
inactive : 'No'
123
159
} ,
160
+ {
161
+ name : 'needsJsx' ,
162
+ type : ( ) => ( isFeatureFlagsUsed ? null : 'toggle' ) ,
163
+ message : 'Add JSX Support?' ,
164
+ initial : false ,
165
+ active : 'Yes' ,
166
+ inactive : 'No'
167
+ } ,
124
168
{
125
169
name : 'needsRouter' ,
126
170
type : ( ) => ( isFeatureFlagsUsed ? null : 'toggle' ) ,
@@ -138,9 +182,41 @@ async function init() {
138
182
inactive : 'No'
139
183
} ,
140
184
{
141
- name : 'needsTests' ,
185
+ name : 'needsVitest' ,
186
+ type : ( ) => ( isFeatureFlagsUsed ? null : 'toggle' ) ,
187
+ message : 'Add Vitest for Unit Testing?' ,
188
+ initial : false ,
189
+ active : 'Yes' ,
190
+ inactive : 'No'
191
+ } ,
192
+ {
193
+ name : 'needsCypress' ,
142
194
type : ( ) => ( isFeatureFlagsUsed ? null : 'toggle' ) ,
143
- message : 'Add Cypress for testing?' ,
195
+ message : ( prev , answers ) =>
196
+ answers . needsVitest
197
+ ? 'Add Cypress for End-to-End testing?'
198
+ : 'Add Cypress for both Unit and End-to-End testing?' ,
199
+ initial : false ,
200
+ active : 'Yes' ,
201
+ inactive : 'No'
202
+ } ,
203
+ {
204
+ name : 'needsEslint' ,
205
+ type : ( ) => ( isFeatureFlagsUsed ? null : 'toggle' ) ,
206
+ message : 'Add ESLint for code quality?' ,
207
+ initial : false ,
208
+ active : 'Yes' ,
209
+ inactive : 'No'
210
+ } ,
211
+ {
212
+ name : 'needsPrettier' ,
213
+ type : ( prev , values ) => {
214
+ if ( isFeatureFlagsUsed || ! values . needsEslint ) {
215
+ return null
216
+ }
217
+ return 'toggle'
218
+ } ,
219
+ message : 'Add Prettier for code formatting?' ,
144
220
initial : false ,
145
221
active : 'Yes' ,
146
222
inactive : 'No'
@@ -160,16 +236,22 @@ async function init() {
160
236
// `initial` won't take effect if the prompt type is null
161
237
// so we still have to assign the default values here
162
238
const {
163
- packageName = toValidPackageName ( defaultProjectName ) ,
164
- shouldOverwrite,
239
+ projectName,
240
+ packageName = projectName ?? defaultProjectName ,
241
+ shouldOverwrite = argv . force ,
242
+ needsJsx = argv . jsx ,
165
243
needsTypeScript = argv . typescript ,
166
244
needsRouter = argv . router ,
167
245
needsPinia = argv . pinia ,
168
- needsTests = argv . tests
246
+ needsCypress = argv . cypress || argv . tests ,
247
+ needsVitest = argv . vitest || argv . tests ,
248
+ needsEslint = argv . eslint || argv [ 'eslint-with-prettier' ] ,
249
+ needsPrettier = argv [ 'eslint-with-prettier' ]
169
250
} = result
251
+ const needsCypressCT = needsCypress && ! needsVitest
170
252
const root = path . join ( cwd , targetDir )
171
253
172
- if ( shouldOverwrite ) {
254
+ if ( fs . existsSync ( root ) && shouldOverwrite ) {
173
255
emptyDir ( root )
174
256
} else if ( ! fs . existsSync ( root ) ) {
175
257
fs . mkdirSync ( root )
@@ -194,17 +276,43 @@ async function init() {
194
276
render ( 'base' )
195
277
196
278
// Add configs.
279
+ if ( needsJsx ) {
280
+ render ( 'config/jsx' )
281
+ }
197
282
if ( needsRouter ) {
198
283
render ( 'config/router' )
199
284
}
200
285
if ( needsPinia ) {
201
286
render ( 'config/pinia' )
202
287
}
203
- if ( needsTests ) {
288
+ if ( needsVitest ) {
289
+ render ( 'config/vitest' )
290
+ }
291
+ if ( needsCypress ) {
204
292
render ( 'config/cypress' )
205
293
}
294
+ if ( needsCypressCT ) {
295
+ render ( 'config/cypress-ct' )
296
+ }
206
297
if ( needsTypeScript ) {
207
298
render ( 'config/typescript' )
299
+
300
+ // Render tsconfigs
301
+ render ( 'tsconfig/base' )
302
+ if ( needsCypress ) {
303
+ render ( 'tsconfig/cypress' )
304
+ }
305
+ if ( needsCypressCT ) {
306
+ render ( 'tsconfig/cypress-ct' )
307
+ }
308
+ if ( needsVitest ) {
309
+ render ( 'tsconfig/vitest' )
310
+ }
311
+ }
312
+
313
+ // Render ESLint config
314
+ if ( needsEslint ) {
315
+ renderEslint ( root , { needsTypeScript, needsCypress, needsCypressCT, needsPrettier } )
208
316
}
209
317
210
318
// Render code template.
@@ -227,17 +335,32 @@ async function init() {
227
335
228
336
// Cleanup.
229
337
338
+ // We try to share as many files between TypeScript and JavaScript as possible.
339
+ // If that's not possible, we put `.ts` version alongside the `.js` one in the templates.
340
+ // So after all the templates are rendered, we need to clean up the redundant files.
341
+ // (Currently it's only `cypress/plugin/index.ts`, but we might add more in the future.)
342
+ // (Or, we might completely get rid of the plugins folder as Cypress 10 supports `cypress.config.ts`)
343
+
230
344
if ( needsTypeScript ) {
231
- // rename all `.js` files to `.ts`
232
- // rename jsconfig.json to tsconfig.json
345
+ // Convert the JavaScript template to the TypeScript
346
+ // Check all the remaining `.js` files:
347
+ // - If the corresponding TypeScript version already exists, remove the `.js` version.
348
+ // - Otherwise, rename the `.js` file to `.ts`
349
+ // Remove `jsconfig.json`, because we already have tsconfig.json
350
+ // `jsconfig.json` is not reused, because we use solution-style `tsconfig`s, which are much more complicated.
233
351
preOrderDirectoryTraverse (
234
352
root ,
235
353
( ) => { } ,
236
354
( filepath ) => {
237
355
if ( filepath . endsWith ( '.js' ) ) {
238
- fs . renameSync ( filepath , filepath . replace ( / \. j s $ / , '.ts' ) )
356
+ const tsFilePath = filepath . replace ( / \. j s $ / , '.ts' )
357
+ if ( fs . existsSync ( tsFilePath ) ) {
358
+ fs . unlinkSync ( filepath )
359
+ } else {
360
+ fs . renameSync ( filepath , tsFilePath )
361
+ }
239
362
} else if ( path . basename ( filepath ) === 'jsconfig.json' ) {
240
- fs . renameSync ( filepath , filepath . replace ( / j s c o n f i g \. j s o n $ / , 'tsconfig.json' ) )
363
+ fs . unlinkSync ( filepath )
241
364
}
242
365
}
243
366
)
@@ -246,44 +369,37 @@ async function init() {
246
369
const indexHtmlPath = path . resolve ( root , 'index.html' )
247
370
const indexHtmlContent = fs . readFileSync ( indexHtmlPath , 'utf8' )
248
371
fs . writeFileSync ( indexHtmlPath , indexHtmlContent . replace ( 'src/main.js' , 'src/main.ts' ) )
249
- }
250
-
251
- if ( ! needsTests ) {
252
- // All templates assumes the need of tests.
253
- // If the user doesn't need it:
254
- // rm -rf cypress **/__tests__/
372
+ } else {
373
+ // Remove all the remaining `.ts` files
255
374
preOrderDirectoryTraverse (
256
375
root ,
257
- ( dirpath ) => {
258
- const dirname = path . basename ( dirpath )
259
-
260
- if ( dirname === 'cypress' || dirname === '__tests__' ) {
261
- emptyDir ( dirpath )
262
- fs . rmdirSync ( dirpath )
376
+ ( ) => { } ,
377
+ ( filepath ) => {
378
+ if ( filepath . endsWith ( '.ts' ) ) {
379
+ fs . unlinkSync ( filepath )
263
380
}
264
- } ,
265
- ( ) => { }
381
+ }
266
382
)
267
383
}
268
384
269
385
// Instructions:
270
386
// Supported package managers: pnpm > yarn > npm
271
387
// Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
272
388
// it is not possible to tell if the command is called by `pnpm init`.
273
- const packageManager = / p n p m / . test ( process . env . npm_execpath )
274
- ? 'pnpm'
275
- : / y a r n / . test ( process . env . npm_execpath )
276
- ? 'yarn'
277
- : 'npm'
389
+ const userAgent = process . env . npm_config_user_agent ?? ''
390
+ const packageManager = / p n p m / . test ( userAgent ) ? 'pnpm' : / y a r n / . test ( userAgent ) ? 'yarn' : 'npm'
278
391
279
392
// README generation
280
393
fs . writeFileSync (
281
394
path . resolve ( root , 'README.md' ) ,
282
395
generateReadme ( {
283
- projectName : result . projectName || defaultProjectName ,
396
+ projectName : result . projectName ?? defaultProjectName ,
284
397
packageManager,
285
398
needsTypeScript,
286
- needsTests
399
+ needsVitest,
400
+ needsCypress,
401
+ needsCypressCT,
402
+ needsEslint
287
403
} )
288
404
)
289
405
@@ -292,6 +408,9 @@ async function init() {
292
408
console . log ( ` ${ bold ( green ( `cd ${ path . relative ( cwd , root ) } ` ) ) } ` )
293
409
}
294
410
console . log ( ` ${ bold ( green ( getCommand ( packageManager , 'install' ) ) ) } ` )
411
+ if ( needsPrettier ) {
412
+ console . log ( ` ${ bold ( green ( getCommand ( packageManager , 'lint' ) ) ) } ` )
413
+ }
295
414
console . log ( ` ${ bold ( green ( getCommand ( packageManager , 'dev' ) ) ) } ` )
296
415
console . log ( )
297
416
}
0 commit comments