create-express-new 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +24 -0
- package/README.md +282 -2
- package/bin/express-cli.js +575 -0
- package/lib/app.js +607 -0
- package/lib/prompts.js +338 -0
- package/lib/wizard.js +398 -0
- package/package.json +65 -4
- package/templates/app/Dockerfile.ejs +64 -0
- package/templates/app/app.js.ejs +163 -0
- package/templates/app/csrf.js.ejs +21 -0
- package/templates/app/dockerignore +15 -0
- package/templates/app/env.example.ejs +40 -0
- package/templates/app/eslint.config.js.ejs +28 -0
- package/templates/app/gitignore +22 -0
- package/templates/app/npmrc +8 -0
- package/templates/app/pnpm-workspace.yaml +24 -0
- package/templates/app/routes/index.js.ejs +22 -0
- package/templates/app/routes/uploads.js.ejs +94 -0
- package/templates/app/routes/users.js.ejs +42 -0
- package/templates/app/test/app.test.js.ejs +199 -0
- package/templates/app/www.js.ejs +46 -0
- package/templates/public/index.html +13 -0
- package/templates/public/stylesheets/style.css +8 -0
- package/templates/versions.json +49 -0
- package/templates/views/error.ejs +12 -0
- package/templates/views/error.hbs +3 -0
- package/templates/views/error.pug +6 -0
- package/templates/views/error.twig +7 -0
- package/templates/views/index.ejs +11 -0
- package/templates/views/index.hbs +2 -0
- package/templates/views/index.pug +5 -0
- package/templates/views/index.twig +6 -0
- package/templates/views/layout.hbs +10 -0
- package/templates/views/layout.pug +7 -0
- package/templates/views/layout.twig +10 -0
- package/templates/views/new-user.ejs +18 -0
- package/templates/views/new-user.hbs +9 -0
- package/templates/views/new-user.pug +13 -0
- package/templates/views/new-user.twig +13 -0
- package/templates/views/uploads.ejs +16 -0
- package/templates/views/uploads.hbs +7 -0
- package/templates/views/uploads.pug +10 -0
- package/templates/views/uploads.twig +11 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import readline from 'node:readline'
|
|
7
|
+
import util from 'node:util'
|
|
8
|
+
import { DEFAULT_PACKAGE_MANAGER, LOGGERS, PACKAGE_MANAGERS, VIEW_ENGINES, checkDestination, createAppName, mergeLines, planApp, scriptCommand } from '../lib/app.js'
|
|
9
|
+
import { CancelError } from '../lib/prompts.js'
|
|
10
|
+
import { wizard } from '../lib/wizard.js'
|
|
11
|
+
|
|
12
|
+
const MODE_0755 = 0o755
|
|
13
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '..', 'package.json'), 'utf-8')).version
|
|
14
|
+
|
|
15
|
+
// command line options
|
|
16
|
+
const OPTIONS = {
|
|
17
|
+
api: { type: 'boolean' },
|
|
18
|
+
cjs: { type: 'boolean' },
|
|
19
|
+
compression: { type: 'boolean' },
|
|
20
|
+
cookies: { type: 'boolean' },
|
|
21
|
+
cors: { type: 'boolean' },
|
|
22
|
+
csrf: { type: 'boolean' },
|
|
23
|
+
docker: { type: 'boolean' },
|
|
24
|
+
force: { type: 'boolean', short: 'f' },
|
|
25
|
+
git: { type: 'boolean' },
|
|
26
|
+
helmet: { type: 'boolean' },
|
|
27
|
+
help: { type: 'boolean', short: 'h' },
|
|
28
|
+
install: { type: 'boolean' },
|
|
29
|
+
'keep-config': { type: 'boolean' },
|
|
30
|
+
lint: { type: 'boolean' },
|
|
31
|
+
logger: { type: 'string' },
|
|
32
|
+
'no-git': { type: 'boolean' },
|
|
33
|
+
'no-view': { type: 'boolean' },
|
|
34
|
+
pm: { type: 'string' },
|
|
35
|
+
'rate-limit': { type: 'boolean' },
|
|
36
|
+
session: { type: 'boolean' },
|
|
37
|
+
ts: { type: 'boolean' },
|
|
38
|
+
uploads: { type: 'boolean' },
|
|
39
|
+
version: { type: 'boolean' },
|
|
40
|
+
view: { type: 'string', short: 'v' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// run the wizard when started in a terminal without arguments
|
|
44
|
+
const args = process.argv.slice(2)
|
|
45
|
+
|
|
46
|
+
if (args.length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
47
|
+
wizard({ input: process.stdin, output: process.stdout }).then((options) => {
|
|
48
|
+
main(options, exit)
|
|
49
|
+
}, (err) => {
|
|
50
|
+
if (!(err instanceof CancelError)) throw err
|
|
51
|
+
console.error('aborting')
|
|
52
|
+
exit(1)
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
main(parseOptions(args), exit)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prompt for confirmation on STDOUT/STDIN
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
function confirm (msg, fn) {
|
|
63
|
+
const rl = readline.createInterface({
|
|
64
|
+
input: process.stdin,
|
|
65
|
+
output: process.stdout
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
let answered = false
|
|
69
|
+
|
|
70
|
+
rl.question(msg, (input) => {
|
|
71
|
+
answered = true
|
|
72
|
+
rl.close()
|
|
73
|
+
fn(/^(y|yes|ok|true)$/i.test(input.trim()))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// treat closing STDIN without an answer as "no"
|
|
77
|
+
rl.on('close', () => {
|
|
78
|
+
if (!answered) {
|
|
79
|
+
console.log()
|
|
80
|
+
fn(false)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create application at the given directory, from its plan. Existing files
|
|
87
|
+
* with other contents are overwritten, except config files with
|
|
88
|
+
* `--keep-config`, and an existing .gitignore gets the missing lines.
|
|
89
|
+
* Leftover files from other options are left in place, with a warning.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} dir
|
|
92
|
+
* @param {object[]} entries
|
|
93
|
+
* @param {string[]} leftovers
|
|
94
|
+
* @param {object} options
|
|
95
|
+
* @param {function} done
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
function createApplication (dir, entries, leftovers, options, done) {
|
|
99
|
+
console.log()
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const file = path.join(dir, entry.path)
|
|
103
|
+
|
|
104
|
+
if (entry.type === 'dir') {
|
|
105
|
+
if (entry.path !== '.' || dir !== '.') mkdir(file)
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const existing = read(file)
|
|
110
|
+
|
|
111
|
+
if (existing === null) {
|
|
112
|
+
write(file, entry.contents, entry.mode)
|
|
113
|
+
} else if (existing === entry.contents) {
|
|
114
|
+
log('identical', file)
|
|
115
|
+
executable(file, entry.mode)
|
|
116
|
+
} else if (entry.merge) {
|
|
117
|
+
const merged = mergeLines(existing, entry.contents)
|
|
118
|
+
if (merged === existing) log('keep', file)
|
|
119
|
+
else write(file, merged, entry.mode, 'update')
|
|
120
|
+
} else if (entry.config && options.keepConfig) {
|
|
121
|
+
log('keep', file)
|
|
122
|
+
} else {
|
|
123
|
+
write(file, entry.contents, entry.mode, 'overwrite')
|
|
124
|
+
executable(file, entry.mode)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (leftovers.length) {
|
|
129
|
+
warning('these existing files are not used by the new app, so they were left as they are:\n' +
|
|
130
|
+
leftovers.map((file) => ' ' + path.join(dir, file)).join('\n') + '\n' +
|
|
131
|
+
'remove them if they are from an earlier app with other options')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (options.install) {
|
|
135
|
+
installDependencies(dir, options.pm, done)
|
|
136
|
+
} else {
|
|
137
|
+
printNextSteps(dir, options.pm, true)
|
|
138
|
+
done(0)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Install the generated app's dependencies with its package manager, then
|
|
144
|
+
* display the next steps.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} dir
|
|
147
|
+
* @param {string} pm the app's package manager
|
|
148
|
+
* @param {function} done
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
function installDependencies (dir, pm, done) {
|
|
152
|
+
console.log(' installing dependencies...')
|
|
153
|
+
console.log()
|
|
154
|
+
|
|
155
|
+
const child = spawn(pm, ['install'], {
|
|
156
|
+
cwd: dir,
|
|
157
|
+
shell: process.platform === 'win32',
|
|
158
|
+
stdio: 'inherit'
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// a package manager that cannot be started fails with an error, then closes
|
|
162
|
+
let failed = false
|
|
163
|
+
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
failed = true
|
|
166
|
+
error(err.code === 'ENOENT'
|
|
167
|
+
? pm + ' is not installed, see ' + PACKAGE_MANAGERS[pm].docs
|
|
168
|
+
: pm + ' install failed: ' + err.message)
|
|
169
|
+
printNextSteps(dir, pm, true)
|
|
170
|
+
done(1)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
child.on('close', (status) => {
|
|
174
|
+
if (failed) return
|
|
175
|
+
if (status !== 0) error(pm + ' install failed')
|
|
176
|
+
printNextSteps(dir, pm, status !== 0)
|
|
177
|
+
done(status === 0 ? 0 : 1)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Display the commands to run the generated app.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} dir
|
|
185
|
+
* @param {string} pm the app's package manager
|
|
186
|
+
* @param {boolean} install include the install step
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
function printNextSteps (dir, pm, install) {
|
|
190
|
+
const prompt = launchedFromCmd() ? '>' : '$'
|
|
191
|
+
|
|
192
|
+
if (dir !== '.') {
|
|
193
|
+
console.log()
|
|
194
|
+
console.log(' change directory:')
|
|
195
|
+
console.log(' %s cd %s', prompt, dir)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (install) {
|
|
199
|
+
console.log()
|
|
200
|
+
console.log(' install dependencies:')
|
|
201
|
+
console.log(' %s %s install', prompt, pm)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log()
|
|
205
|
+
console.log(' run the app:')
|
|
206
|
+
console.log(' %s %s', prompt, scriptCommand(pm, 'start'))
|
|
207
|
+
console.log()
|
|
208
|
+
console.log(' run the app in development, restarting on change:')
|
|
209
|
+
console.log(' %s %s', prompt, scriptCommand(pm, 'dev'))
|
|
210
|
+
console.log()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if the given directory `dir` is empty.
|
|
215
|
+
*
|
|
216
|
+
* @param {String} dir
|
|
217
|
+
* @param {Function} fn
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
function emptyDirectory (dir, fn) {
|
|
221
|
+
fs.readdir(dir, (err, files) => {
|
|
222
|
+
if (err && err.code !== 'ENOENT') throw err
|
|
223
|
+
fn(!files || !files.length)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Display an error.
|
|
229
|
+
*
|
|
230
|
+
* @param {String} message
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
function error (message) {
|
|
234
|
+
console.error()
|
|
235
|
+
message.split('\n').forEach((line) => {
|
|
236
|
+
console.error(' error: %s', line)
|
|
237
|
+
})
|
|
238
|
+
console.error()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Set the exit code, letting the process exit once all output is flushed.
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
function exit (code) {
|
|
246
|
+
process.exitCode = code
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Determine if launched from cmd.exe
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
function launchedFromCmd () {
|
|
254
|
+
return process.platform === 'win32' &&
|
|
255
|
+
process.env._ === undefined
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Main program.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
function main (options, done) {
|
|
263
|
+
// top-level argument direction
|
|
264
|
+
if (options['!'].length > 0) {
|
|
265
|
+
usage()
|
|
266
|
+
error('unknown option `' + options['!'][0] + "'")
|
|
267
|
+
done(1)
|
|
268
|
+
} else if (options.help) {
|
|
269
|
+
usage()
|
|
270
|
+
done(0)
|
|
271
|
+
} else if (options.version) {
|
|
272
|
+
version()
|
|
273
|
+
done(0)
|
|
274
|
+
} else if (options.view === '') {
|
|
275
|
+
usage()
|
|
276
|
+
error('option `-v, --view <engine>\' argument missing')
|
|
277
|
+
done(1)
|
|
278
|
+
} else if (options.logger === '') {
|
|
279
|
+
usage()
|
|
280
|
+
error('option `--logger <name>\' argument missing')
|
|
281
|
+
done(1)
|
|
282
|
+
} else if (options.pm === '') {
|
|
283
|
+
usage()
|
|
284
|
+
error('option `--pm <name>\' argument missing')
|
|
285
|
+
done(1)
|
|
286
|
+
} else {
|
|
287
|
+
// Path
|
|
288
|
+
const destinationPath = options._[0] || '.'
|
|
289
|
+
|
|
290
|
+
// App name
|
|
291
|
+
const appName = createAppName(path.resolve(destinationPath)) || 'hello-world'
|
|
292
|
+
|
|
293
|
+
// A JSON API has no view engine
|
|
294
|
+
if (options.api) {
|
|
295
|
+
if (typeof options.view === 'string') {
|
|
296
|
+
usage()
|
|
297
|
+
error('option `--api\' cannot be used with `--view\'')
|
|
298
|
+
return done(1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
options.view = false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Default view engine
|
|
305
|
+
if (options.view === true) {
|
|
306
|
+
options.view = 'pug'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Renamed engines
|
|
310
|
+
if (options.view === 'jade') {
|
|
311
|
+
warning("jade has been renamed to pug, using `--view=pug'")
|
|
312
|
+
options.view = 'pug'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.ts && options.cjs) {
|
|
316
|
+
usage()
|
|
317
|
+
error('option `--ts\' cannot be used with `--cjs\'')
|
|
318
|
+
return done(1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Unsupported engines
|
|
322
|
+
if (options.view && !VIEW_ENGINES[options.view]) {
|
|
323
|
+
usage()
|
|
324
|
+
error('unsupported view engine `' + options.view + "'")
|
|
325
|
+
return done(1)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Unsupported loggers
|
|
329
|
+
if (!LOGGERS.includes(options.logger)) {
|
|
330
|
+
usage()
|
|
331
|
+
error('unsupported logger `' + options.logger + "'")
|
|
332
|
+
return done(1)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Unsupported package managers
|
|
336
|
+
if (!Object.hasOwn(PACKAGE_MANAGERS, options.pm)) {
|
|
337
|
+
usage()
|
|
338
|
+
error('unsupported package manager `' + options.pm + "'")
|
|
339
|
+
return done(1)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Sessions are for web apps, and CSRF protection needs them and views
|
|
343
|
+
if (options.session && options.api) {
|
|
344
|
+
usage()
|
|
345
|
+
error('option `--session\' cannot be used with `--api\'')
|
|
346
|
+
return done(1)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (options.csrf && !options.session) {
|
|
350
|
+
usage()
|
|
351
|
+
error('option `--csrf\' requires `--session\'')
|
|
352
|
+
return done(1)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (options.csrf && !options.view) {
|
|
356
|
+
usage()
|
|
357
|
+
error('option `--csrf\' needs a view engine, so cannot be used with `--no-view\'')
|
|
358
|
+
return done(1)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check the destination before writing anything
|
|
362
|
+
const entries = planApp(appName, options)
|
|
363
|
+
const { blockers, conflicts, leftovers } = checkDestination(destinationPath, entries)
|
|
364
|
+
|
|
365
|
+
if (blockers.length) {
|
|
366
|
+
error('cannot create the app:\n' + blockers.join('\n'))
|
|
367
|
+
return done(1)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const overwrites = conflicts.filter((entry) => !(entry.config && options.keepConfig))
|
|
371
|
+
|
|
372
|
+
// Generate application
|
|
373
|
+
emptyDirectory(destinationPath, (empty) => {
|
|
374
|
+
if (empty || options.force) {
|
|
375
|
+
createApplication(destinationPath, entries, leftovers, options, done)
|
|
376
|
+
} else {
|
|
377
|
+
if (overwrites.length) {
|
|
378
|
+
console.log()
|
|
379
|
+
console.log(' these existing files will be overwritten:')
|
|
380
|
+
for (const entry of overwrites) console.log(' ' + path.join(destinationPath, entry.path))
|
|
381
|
+
console.log()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
confirm('destination is not empty, continue? [y/N] ', (ok) => {
|
|
385
|
+
if (ok) {
|
|
386
|
+
process.stdin.destroy()
|
|
387
|
+
createApplication(destinationPath, entries, leftovers, options, done)
|
|
388
|
+
} else {
|
|
389
|
+
console.error('aborting')
|
|
390
|
+
done(1)
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Create a directory, with its parents.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} dir
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
function mkdir (dir) {
|
|
405
|
+
log('create', dir + path.sep)
|
|
406
|
+
fs.mkdirSync(dir, { recursive: true, mode: MODE_0755 })
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Parse command line arguments.
|
|
411
|
+
*
|
|
412
|
+
* The view option defaults to `true` when not given and is `''` when given
|
|
413
|
+
* without an argument; `_` holds positionals and `!` holds unknown options.
|
|
414
|
+
*
|
|
415
|
+
* @param {string[]} argv
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
function parseOptions (argv) {
|
|
419
|
+
const { values, positionals, tokens } = util.parseArgs({
|
|
420
|
+
args: argv,
|
|
421
|
+
options: OPTIONS,
|
|
422
|
+
allowPositionals: true,
|
|
423
|
+
strict: false,
|
|
424
|
+
tokens: true
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const options = { view: true, ...values, _: positionals, '!': [] }
|
|
428
|
+
|
|
429
|
+
for (const token of tokens) {
|
|
430
|
+
if (token.kind !== 'option') continue
|
|
431
|
+
|
|
432
|
+
if (!Object.hasOwn(OPTIONS, token.name)) {
|
|
433
|
+
options['!'].push(token.rawName)
|
|
434
|
+
} else if (OPTIONS[token.name].type === 'string' &&
|
|
435
|
+
(token.value === undefined || (!token.inlineValue && token.value.startsWith('-')))) {
|
|
436
|
+
// argument missing, or the next option was taken as the argument
|
|
437
|
+
options[token.name] = ''
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (options['no-view']) {
|
|
442
|
+
options.view = false
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// a .gitignore is added unless --no-git is given
|
|
446
|
+
options.git = !options['no-git']
|
|
447
|
+
options.keepConfig = Boolean(options['keep-config'])
|
|
448
|
+
options.rateLimit = Boolean(options['rate-limit'])
|
|
449
|
+
options.logger ??= 'morgan'
|
|
450
|
+
options.pm ??= DEFAULT_PACKAGE_MANAGER
|
|
451
|
+
|
|
452
|
+
return options
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Display the usage.
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
function usage () {
|
|
460
|
+
console.log('')
|
|
461
|
+
console.log(' Usage: pnpm create express-new@latest [dir] [options]')
|
|
462
|
+
console.log(' npm create express-new@latest [dir] -- [options]')
|
|
463
|
+
console.log(' npx create-express-new [options] [dir]')
|
|
464
|
+
console.log(' express [options] [dir] (when installed globally)')
|
|
465
|
+
console.log('')
|
|
466
|
+
console.log(' Run without arguments in a terminal to choose the options interactively.')
|
|
467
|
+
console.log('')
|
|
468
|
+
console.log(' Options:')
|
|
469
|
+
console.log('')
|
|
470
|
+
console.log(' -v, --view <engine> add view <engine> support (ejs|hbs|pug|twig) (defaults to pug)')
|
|
471
|
+
console.log(' --no-view use static html instead of view engine')
|
|
472
|
+
console.log(' --api generate a JSON API, without views or static files')
|
|
473
|
+
console.log(' --cjs generate CommonJS modules instead of ES modules')
|
|
474
|
+
console.log(' --ts generate TypeScript, run directly by Node.js')
|
|
475
|
+
console.log(' --helmet add helmet middleware for security headers')
|
|
476
|
+
console.log(' --compression add compression middleware for gzip/brotli responses')
|
|
477
|
+
console.log(' --cookies add cookie-parser middleware')
|
|
478
|
+
console.log(' --cors add cors middleware for cross-origin requests')
|
|
479
|
+
console.log(' --rate-limit add express-rate-limit to limit requests per client')
|
|
480
|
+
console.log(' --session add express-session for sessions (not with --api)')
|
|
481
|
+
console.log(' --csrf add CSRF protection for forms (needs --session)')
|
|
482
|
+
console.log(' --uploads add multer and an upload route at /uploads')
|
|
483
|
+
console.log(' --logger <name> request logger (morgan|pino) (defaults to morgan)')
|
|
484
|
+
console.log(' --docker add a Dockerfile for a production image')
|
|
485
|
+
console.log(' --lint add ESLint and a lint script')
|
|
486
|
+
console.log(' --pm <name> package manager the app requires (pnpm|npm) (defaults to pnpm)')
|
|
487
|
+
console.log(' --install install the dependencies after creating the app')
|
|
488
|
+
console.log(' --no-git skip the .gitignore')
|
|
489
|
+
console.log(' -f, --force force on non-empty directory')
|
|
490
|
+
console.log(' --keep-config keep existing config files (.env.example, Dockerfile,')
|
|
491
|
+
console.log(' .dockerignore, tsconfig.json, eslint.config.*,')
|
|
492
|
+
console.log(' pnpm-workspace.yaml, .npmrc)')
|
|
493
|
+
console.log(' --version output the version number')
|
|
494
|
+
console.log(' -h, --help output usage information')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Display the version.
|
|
499
|
+
*/
|
|
500
|
+
|
|
501
|
+
function version () {
|
|
502
|
+
console.log(VERSION)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Display a warning.
|
|
507
|
+
*
|
|
508
|
+
* @param {String} message
|
|
509
|
+
*/
|
|
510
|
+
|
|
511
|
+
function warning (message) {
|
|
512
|
+
console.error()
|
|
513
|
+
message.split('\n').forEach((line) => {
|
|
514
|
+
console.error(' warning: %s', line)
|
|
515
|
+
})
|
|
516
|
+
console.error()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Make an existing file executable when its planned mode is, as writing it
|
|
521
|
+
* only sets the mode of new files. Adds execute where read is allowed.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} file
|
|
524
|
+
* @param {number} mode
|
|
525
|
+
*/
|
|
526
|
+
|
|
527
|
+
function executable (file, mode) {
|
|
528
|
+
if (!(mode & 0o111)) return
|
|
529
|
+
|
|
530
|
+
const current = fs.statSync(file).mode & 0o777
|
|
531
|
+
const wanted = current | ((current & 0o444) >> 2)
|
|
532
|
+
|
|
533
|
+
if (wanted !== current) fs.chmodSync(file, wanted)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Log what happened to a file.
|
|
538
|
+
*
|
|
539
|
+
* @param {string} action
|
|
540
|
+
* @param {string} file
|
|
541
|
+
*/
|
|
542
|
+
|
|
543
|
+
function log (action, file) {
|
|
544
|
+
const color = action === 'overwrite' ? '33' : action === 'create' || action === 'update' ? '36' : '2'
|
|
545
|
+
console.log(' \x1b[' + color + 'm' + action + '\x1b[0m : ' + file)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Read a file, or null when it does not exist.
|
|
550
|
+
*
|
|
551
|
+
* @param {string} file
|
|
552
|
+
*/
|
|
553
|
+
|
|
554
|
+
function read (file) {
|
|
555
|
+
try {
|
|
556
|
+
return fs.readFileSync(file, 'utf-8')
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (err.code === 'ENOENT') return null
|
|
559
|
+
throw err
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* echo str > file, logging it as `action`.
|
|
565
|
+
*
|
|
566
|
+
* @param {String} file
|
|
567
|
+
* @param {String} str
|
|
568
|
+
* @param {number} mode
|
|
569
|
+
* @param {string} action
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
function write (file, str, mode, action = 'create') {
|
|
573
|
+
fs.writeFileSync(file, str, { mode })
|
|
574
|
+
log(action, file)
|
|
575
|
+
}
|