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
package/lib/wizard.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Interactive wizard that asks for the same choices as the command line
|
|
2
|
+
// options and returns them in the same shape as the parsed options. Esc (or
|
|
3
|
+
// the left arrow) goes back to the previous question, keeping the answers.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { DEFAULT_PACKAGE_MANAGER, checkDestination, createAppName, planApp } from './app.js'
|
|
8
|
+
import { BACK, CancelError, confirm, erase, hinted, multiselect, rows, select, style, text } from './prompts.js'
|
|
9
|
+
|
|
10
|
+
// existing files listed before asking about them
|
|
11
|
+
const MAX_LISTED = 5
|
|
12
|
+
|
|
13
|
+
const VIEWS = [
|
|
14
|
+
{ label: 'pug', value: 'pug', hint: 'default' },
|
|
15
|
+
{ label: 'ejs', value: 'ejs' },
|
|
16
|
+
{ label: 'hbs', value: 'hbs', hint: 'Handlebars' },
|
|
17
|
+
{ label: 'twig', value: 'twig' },
|
|
18
|
+
{ label: 'none', value: false, hint: 'static HTML' }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Walk through the options and return them, or throw CancelError.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} io { input, output, cwd }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export async function wizard (io) {
|
|
28
|
+
const { output, cwd = process.cwd() } = io
|
|
29
|
+
const answers = {}
|
|
30
|
+
|
|
31
|
+
// the destination checked against the answers, before asking about it
|
|
32
|
+
let check = null
|
|
33
|
+
|
|
34
|
+
output.write('\n' + hinted(style.bold(' create-express-new'), ' create an Express 5 app', output.columns || Infinity) + '\n\n')
|
|
35
|
+
|
|
36
|
+
// Each step asks one question, starting from its previous answer, and
|
|
37
|
+
// keeps the text it prints so going back can erase it.
|
|
38
|
+
const steps = [
|
|
39
|
+
{
|
|
40
|
+
name: 'directory',
|
|
41
|
+
ask: (step) => askDirectory(io, step, answers.directory, cwd)
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'kind',
|
|
45
|
+
ask: (step) => step.ask(select, {
|
|
46
|
+
message: 'What are you building?',
|
|
47
|
+
initial: answers.kind,
|
|
48
|
+
choices: [
|
|
49
|
+
{ label: 'Web app', value: 'web', hint: 'server-rendered views' },
|
|
50
|
+
{ label: 'JSON API', value: 'api', hint: 'no views or static files' }
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'view',
|
|
56
|
+
when: () => answers.kind === 'web',
|
|
57
|
+
ask: (step) => step.ask(select, { message: 'View engine', initial: answers.view, choices: VIEWS })
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'language',
|
|
61
|
+
ask: (step) => step.ask(select, {
|
|
62
|
+
message: 'Language',
|
|
63
|
+
initial: answers.language,
|
|
64
|
+
choices: [
|
|
65
|
+
{ label: 'JavaScript', value: 'esm', hint: 'ES modules' },
|
|
66
|
+
{ label: 'TypeScript', value: 'ts', hint: 'runs directly on Node.js 22.18+' },
|
|
67
|
+
{ label: 'JavaScript (CommonJS)', value: 'cjs', hint: 'require() and module.exports' }
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'middleware',
|
|
73
|
+
ask: (step) => step.ask(multiselect, {
|
|
74
|
+
message: 'Optional middleware',
|
|
75
|
+
initial: answers.middleware,
|
|
76
|
+
choices: [
|
|
77
|
+
{ label: 'helmet', value: 'helmet', hint: 'security headers' },
|
|
78
|
+
{ label: 'compression', value: 'compression', hint: 'gzip/brotli responses' },
|
|
79
|
+
{ label: 'cookie-parser', value: 'cookies', hint: 'req.cookies' },
|
|
80
|
+
{ label: 'cors', value: 'cors', hint: 'cross-origin requests' },
|
|
81
|
+
{ label: 'express-rate-limit', value: 'rate-limit', hint: 'limit requests per client' },
|
|
82
|
+
// sessions are for web apps
|
|
83
|
+
...(answers.kind === 'web' ? [{ label: 'express-session', value: 'session', hint: 'req.session' }] : []),
|
|
84
|
+
{ label: 'multer', value: 'uploads', hint: 'file uploads at /uploads' }
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'csrf',
|
|
90
|
+
when: () => answers.kind === 'web' && answers.view !== false && answers.middleware.includes('session'),
|
|
91
|
+
ask: (step) => step.ask(confirm, { message: 'Protect forms with CSRF tokens?', initial: answers.csrf ?? true })
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'logger',
|
|
95
|
+
ask: (step) => step.ask(select, {
|
|
96
|
+
message: 'Request logger',
|
|
97
|
+
initial: answers.logger,
|
|
98
|
+
choices: [
|
|
99
|
+
{ label: 'morgan', value: 'morgan', hint: 'one readable line per request' },
|
|
100
|
+
{ label: 'pino', value: 'pino', hint: 'JSON logs for log collectors' }
|
|
101
|
+
]
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'extras',
|
|
106
|
+
ask: (step) => step.ask(multiselect, {
|
|
107
|
+
message: 'Extras',
|
|
108
|
+
initial: answers.extras,
|
|
109
|
+
choices: [
|
|
110
|
+
{ label: 'Dockerfile', value: 'docker', hint: 'production image with a /health check' },
|
|
111
|
+
{ label: 'ESLint', value: 'lint', hint: 'with a lint script' }
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'git',
|
|
117
|
+
ask: (step) => step.ask(confirm, { message: 'Add a .gitignore?', initial: answers.git ?? true })
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'pm',
|
|
121
|
+
ask: (step) => step.ask(select, {
|
|
122
|
+
message: 'Package manager',
|
|
123
|
+
initial: answers.pm,
|
|
124
|
+
choices: [
|
|
125
|
+
{ label: 'pnpm', value: 'pnpm', hint: 'recommended, safer installs by default' },
|
|
126
|
+
{ label: 'npm', value: 'npm', hint: 'needs npm 12 or newer' }
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'install',
|
|
132
|
+
ask: (step) => step.ask(confirm, { message: 'Install dependencies now?', initial: answers.install ?? true })
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'existing',
|
|
136
|
+
when: () => {
|
|
137
|
+
check = checkApp(answers, cwd)
|
|
138
|
+
|
|
139
|
+
// forget a choice about existing files that are no longer generated
|
|
140
|
+
if (!check.blockers.length && !check.conflicts.length) delete answers.existing
|
|
141
|
+
return check.blockers.length > 0 || check.conflicts.length > 0
|
|
142
|
+
},
|
|
143
|
+
ask: (step) => askExisting(step, check, answers.existing)
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'create',
|
|
147
|
+
ask: async (step) => {
|
|
148
|
+
step.say('\n Equivalent command:\n ' + style.cyan(toCommand(toOptions(answers))) + '\n')
|
|
149
|
+
|
|
150
|
+
const create = await step.ask(confirm, { message: 'Create the app?', initial: true })
|
|
151
|
+
if (create === false) throw new CancelError()
|
|
152
|
+
return create
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
// completed steps, with the text each printed
|
|
158
|
+
const history = []
|
|
159
|
+
let index = 0
|
|
160
|
+
|
|
161
|
+
while (index < steps.length) {
|
|
162
|
+
const { name, ask, when } = steps[index]
|
|
163
|
+
|
|
164
|
+
if (when && !when()) {
|
|
165
|
+
index++
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const step = {
|
|
170
|
+
back: history.length > 0,
|
|
171
|
+
printed: [],
|
|
172
|
+
// ask a question, keeping its answer
|
|
173
|
+
async ask (fn, options) {
|
|
174
|
+
const value = await fn(io, { ...options, back: this.back })
|
|
175
|
+
if (value !== BACK) this.printed.push(io.printed)
|
|
176
|
+
return value
|
|
177
|
+
},
|
|
178
|
+
// print text, keeping it
|
|
179
|
+
say (str) {
|
|
180
|
+
output.write(str + '\n')
|
|
181
|
+
this.printed.push(str)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const value = await ask(step)
|
|
186
|
+
|
|
187
|
+
if (value === BACK) {
|
|
188
|
+
// erase what this step printed, and the previous answer, and ask again
|
|
189
|
+
const previous = history.pop()
|
|
190
|
+
eraseText(io, [...previous.printed, ...step.printed])
|
|
191
|
+
index = previous.index
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
answers[name] = value
|
|
196
|
+
history.push({ index, printed: step.printed })
|
|
197
|
+
index++
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return toOptions(answers)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ask for the project directory, confirming a directory that is not empty.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
async function askDirectory (io, step, previous, cwd) {
|
|
208
|
+
let value = previous ? previous.dir : ''
|
|
209
|
+
|
|
210
|
+
for (;;) {
|
|
211
|
+
const answer = await step.ask(text, {
|
|
212
|
+
message: 'Project directory',
|
|
213
|
+
hint: '(. for the current directory)',
|
|
214
|
+
initial: 'my-app',
|
|
215
|
+
value,
|
|
216
|
+
validate: (str) => str.trim() ? null : 'Enter a directory'
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (answer === BACK) return BACK
|
|
220
|
+
|
|
221
|
+
const dir = answer.trim()
|
|
222
|
+
|
|
223
|
+
const resolved = path.resolve(cwd, dir)
|
|
224
|
+
|
|
225
|
+
if (isEmpty(resolved)) {
|
|
226
|
+
return { dir, force: false }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const name = resolved === path.resolve(cwd) ? 'The current directory' : 'Directory'
|
|
230
|
+
const force = await confirm(io, { message: name + ' is not empty. Continue anyway?', initial: false, back: true })
|
|
231
|
+
|
|
232
|
+
if (force === true) {
|
|
233
|
+
step.printed.push(io.printed)
|
|
234
|
+
return { dir, force: true }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ask for the directory again, in place: empty after declining it, or
|
|
238
|
+
// starting from it after going back
|
|
239
|
+
eraseText(io, force === BACK ? step.printed : [...step.printed, io.printed])
|
|
240
|
+
step.printed = []
|
|
241
|
+
value = force === BACK ? dir : ''
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Ask what to do with the existing files the app would overwrite, or show
|
|
247
|
+
* what is in the way of creating it, leaving only going back or cancelling.
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
async function askExisting (step, { blockers, conflicts }, previous) {
|
|
251
|
+
if (blockers.length) {
|
|
252
|
+
step.say('\n' + blockers.map((blocker) => style.red(' ' + blocker)).join('\n'))
|
|
253
|
+
|
|
254
|
+
await step.ask(select, {
|
|
255
|
+
message: 'Cannot create the app here',
|
|
256
|
+
choices: [{ label: 'Cancel', value: 'cancel' }]
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
throw new CancelError()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const shown = conflicts.slice(0, MAX_LISTED).map((entry) => ' ' + entry.path)
|
|
263
|
+
if (conflicts.length > MAX_LISTED) shown.push(' and ' + (conflicts.length - MAX_LISTED) + ' more')
|
|
264
|
+
step.say('\n These existing files will be overwritten:\n' + style.dim(shown.join('\n')))
|
|
265
|
+
|
|
266
|
+
const config = conflicts.filter((entry) => entry.config)
|
|
267
|
+
const choices = [{ label: 'Overwrite them', value: 'overwrite' }]
|
|
268
|
+
|
|
269
|
+
if (config.length === conflicts.length) {
|
|
270
|
+
choices.push({ label: 'Keep them', value: 'keep' })
|
|
271
|
+
} else if (config.length) {
|
|
272
|
+
choices.push({ label: 'Keep my config files', value: 'keep', hint: config.map((entry) => entry.path).join(', ') })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
choices.push({ label: 'Cancel', value: 'cancel' })
|
|
276
|
+
|
|
277
|
+
const answer = await step.ask(select, { message: 'Existing files', initial: previous, choices })
|
|
278
|
+
if (answer === 'cancel') throw new CancelError()
|
|
279
|
+
return answer
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check the project directory against the app the answers create.
|
|
284
|
+
*/
|
|
285
|
+
|
|
286
|
+
function checkApp (answers, cwd) {
|
|
287
|
+
const options = toOptions(answers)
|
|
288
|
+
const dir = path.resolve(cwd, options._[0])
|
|
289
|
+
|
|
290
|
+
return checkDestination(dir, planApp(createAppName(dir) || 'hello-world', options))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Erase printed text, counting its rows at the current terminal width, as
|
|
295
|
+
* the terminal may have been resized since it was printed.
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
function eraseText (io, printed) {
|
|
299
|
+
if (printed.length) erase(io, rows(printed.join('\n'), io.output.columns || Infinity))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Options in the shape of the parsed command line options.
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
function toOptions (answers) {
|
|
307
|
+
const { directory, kind, language, middleware = [], extras = [] } = answers
|
|
308
|
+
const view = kind === 'web' ? answers.view : false
|
|
309
|
+
const session = kind === 'web' && middleware.includes('session')
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
_: [directory.dir],
|
|
313
|
+
'!': [],
|
|
314
|
+
api: kind === 'api',
|
|
315
|
+
cjs: language === 'cjs',
|
|
316
|
+
compression: middleware.includes('compression'),
|
|
317
|
+
cookies: middleware.includes('cookies'),
|
|
318
|
+
cors: middleware.includes('cors'),
|
|
319
|
+
csrf: session && view !== false && answers.csrf === true,
|
|
320
|
+
docker: extras.includes('docker'),
|
|
321
|
+
force: directory.force,
|
|
322
|
+
git: answers.git,
|
|
323
|
+
helmet: middleware.includes('helmet'),
|
|
324
|
+
install: answers.install,
|
|
325
|
+
keepConfig: answers.existing === 'keep',
|
|
326
|
+
lint: extras.includes('lint'),
|
|
327
|
+
logger: answers.logger ?? 'morgan',
|
|
328
|
+
pm: answers.pm ?? DEFAULT_PACKAGE_MANAGER,
|
|
329
|
+
rateLimit: middleware.includes('rate-limit'),
|
|
330
|
+
session,
|
|
331
|
+
ts: language === 'ts',
|
|
332
|
+
uploads: middleware.includes('uploads'),
|
|
333
|
+
view
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The command line that generates the same app as the given options.
|
|
339
|
+
*
|
|
340
|
+
* @param {object} options
|
|
341
|
+
*/
|
|
342
|
+
|
|
343
|
+
export function toCommand (options) {
|
|
344
|
+
const pm = options.pm ?? DEFAULT_PACKAGE_MANAGER
|
|
345
|
+
const args = [pm, 'create', 'express-new@latest', quote(options._[0])]
|
|
346
|
+
const flags = []
|
|
347
|
+
|
|
348
|
+
if (options.api) flags.push('--api')
|
|
349
|
+
else if (options.view === false) flags.push('--no-view')
|
|
350
|
+
else if (options.view !== 'pug') flags.push('--view=' + options.view)
|
|
351
|
+
|
|
352
|
+
if (options.ts) flags.push('--ts')
|
|
353
|
+
if (options.cjs) flags.push('--cjs')
|
|
354
|
+
if (options.helmet) flags.push('--helmet')
|
|
355
|
+
if (options.compression) flags.push('--compression')
|
|
356
|
+
if (options.cookies) flags.push('--cookies')
|
|
357
|
+
if (options.cors) flags.push('--cors')
|
|
358
|
+
if (options.rateLimit) flags.push('--rate-limit')
|
|
359
|
+
if (options.session) flags.push('--session')
|
|
360
|
+
if (options.csrf) flags.push('--csrf')
|
|
361
|
+
if (options.uploads) flags.push('--uploads')
|
|
362
|
+
if (options.logger && options.logger !== 'morgan') flags.push('--logger=' + options.logger)
|
|
363
|
+
if (options.docker) flags.push('--docker')
|
|
364
|
+
if (options.lint) flags.push('--lint')
|
|
365
|
+
if (pm !== DEFAULT_PACKAGE_MANAGER) flags.push('--pm=' + pm)
|
|
366
|
+
if (!options.git) flags.push('--no-git')
|
|
367
|
+
if (options.force) flags.push('--force')
|
|
368
|
+
if (options.keepConfig) flags.push('--keep-config')
|
|
369
|
+
if (options.install) flags.push('--install')
|
|
370
|
+
|
|
371
|
+
// npm create keeps the options before `--` for itself
|
|
372
|
+
if (flags.length && pm === 'npm') args.push('--')
|
|
373
|
+
args.push(...flags)
|
|
374
|
+
|
|
375
|
+
return args.join(' ')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Check if a directory is missing or empty.
|
|
380
|
+
*/
|
|
381
|
+
|
|
382
|
+
function isEmpty (dir) {
|
|
383
|
+
try {
|
|
384
|
+
return fs.readdirSync(dir).length === 0
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (err.code === 'ENOENT') return true
|
|
387
|
+
if (err.code === 'ENOTDIR') return false
|
|
388
|
+
throw err
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Quote a shell argument when needed.
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
function quote (arg) {
|
|
397
|
+
return /^[\w./@:-]+$/.test(arg) ? arg : JSON.stringify(arg)
|
|
398
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-express-new",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "Express 5 application generator: ESM, TypeScript and CommonJS app templates. A modern successor to express-generator.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"author": "Michael Caines (https://github.com/w0244079)",
|
|
6
|
+
"contributors": [
|
|
7
|
+
"TJ Holowaychuk <tj@vision-media.ca>",
|
|
8
|
+
"Aaron Heckmann <aaron.heckmann+github@gmail.com>",
|
|
9
|
+
"Ciaran Jessup <ciaranj@gmail.com>",
|
|
10
|
+
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
11
|
+
"Guillermo Rauch <rauchg@gmail.com>",
|
|
12
|
+
"Jonathan Ong <me@jongleberry.com>",
|
|
13
|
+
"Roman Shtylman <shtylman+expressjs@gmail.com>"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"express",
|
|
17
|
+
"express-generator",
|
|
18
|
+
"express5",
|
|
19
|
+
"generator",
|
|
20
|
+
"template",
|
|
21
|
+
"starter",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"boilerplate",
|
|
24
|
+
"cli",
|
|
25
|
+
"esm",
|
|
26
|
+
"typescript",
|
|
27
|
+
"web",
|
|
28
|
+
"api"
|
|
29
|
+
],
|
|
30
|
+
"repository": "github:w0244079/create-express-new",
|
|
31
|
+
"homepage": "https://github.com/w0244079/create-express-new#readme",
|
|
32
|
+
"bugs": "https://github.com/w0244079/create-express-new/issues",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"type": "module",
|
|
35
|
+
"bin": {
|
|
36
|
+
"create-express-new": "./bin/express-cli.js",
|
|
37
|
+
"express": "./bin/express-cli.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ejs": "^6.0.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"eslint": "^9.39.5",
|
|
44
|
+
"mocha": "^12.0.3",
|
|
45
|
+
"neostandard": "^0.13.0",
|
|
46
|
+
"supertest": "^7.3.0",
|
|
47
|
+
"tree-kill": "^1.2.2",
|
|
48
|
+
"validate-npm-package-name": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.9"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"CHANGELOG.md",
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"bin/",
|
|
57
|
+
"lib/",
|
|
58
|
+
"templates/"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"lint": "eslint .",
|
|
62
|
+
"test": "mocha --reporter spec --bail --check-leaks test/",
|
|
63
|
+
"test-ci": "mocha --reporter spec --check-leaks test/",
|
|
64
|
+
"versions": "node scripts/versions.js",
|
|
65
|
+
"prepublishOnly": "npm run lint && npm run test-ci"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
<% if (pm === 'pnpm') { -%>
|
|
4
|
+
# Production image. Build it after `pnpm install`, which creates the
|
|
5
|
+
# pnpm-lock.yaml that the image installs from:
|
|
6
|
+
<% } else { -%>
|
|
7
|
+
# Production image. Build it after `npm install`, which creates the
|
|
8
|
+
# package-lock.json that `npm ci` needs:
|
|
9
|
+
<% } -%>
|
|
10
|
+
#
|
|
11
|
+
# docker build -t <%- name %> .
|
|
12
|
+
# docker run -p 3000:3000 <%- name %>
|
|
13
|
+
|
|
14
|
+
# Dependencies are installed in their own stage, which keeps the package
|
|
15
|
+
# manager out of the final image.
|
|
16
|
+
|
|
17
|
+
# the latest Node.js 22 release, matching the minimum version in package.json
|
|
18
|
+
FROM node:22-slim AS dependencies
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
<% if (pm === 'pnpm') { -%>
|
|
23
|
+
# the package manager this app requires in package.json; it switches to the
|
|
24
|
+
# pnpm version recorded in pnpm-lock.yaml
|
|
25
|
+
<% } else { -%>
|
|
26
|
+
# the package manager this app requires in package.json
|
|
27
|
+
<% } -%>
|
|
28
|
+
RUN npm install -g <%- pm %>@<%- pmVersion %>
|
|
29
|
+
|
|
30
|
+
# install production dependencies first, so they are cached between builds
|
|
31
|
+
<% if (pm === 'pnpm') { -%>
|
|
32
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
33
|
+
RUN pnpm install --frozen-lockfile --prod
|
|
34
|
+
<% } else { -%>
|
|
35
|
+
COPY package*.json .npmrc ./
|
|
36
|
+
RUN npm ci --omit=dev
|
|
37
|
+
<% } -%>
|
|
38
|
+
|
|
39
|
+
FROM node:22-slim
|
|
40
|
+
|
|
41
|
+
ENV NODE_ENV=production
|
|
42
|
+
WORKDIR /app
|
|
43
|
+
|
|
44
|
+
# the dependencies installed above
|
|
45
|
+
COPY --from=dependencies /app/node_modules ./node_modules
|
|
46
|
+
|
|
47
|
+
# copy the app owned by the unprivileged node user, which runs it, so it can
|
|
48
|
+
# read the files whatever their permissions on the build machine
|
|
49
|
+
COPY --chown=node:node . .
|
|
50
|
+
<% if (uploads) { -%>
|
|
51
|
+
|
|
52
|
+
# a folder for uploaded files that the node user can write to; files saved
|
|
53
|
+
# in a container are lost when it is replaced, so mount a volume here
|
|
54
|
+
RUN mkdir -p uploads && chown node:node uploads
|
|
55
|
+
<% } -%>
|
|
56
|
+
USER node
|
|
57
|
+
|
|
58
|
+
EXPOSE 3000
|
|
59
|
+
|
|
60
|
+
HEALTHCHECK --interval=30s --timeout=3s \
|
|
61
|
+
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/health').then((res) => process.exit(res.ok ? 0 : 1), () => process.exit(1))"
|
|
62
|
+
|
|
63
|
+
# run node directly, so it receives SIGTERM and shuts down gracefully
|
|
64
|
+
CMD ["node", "<%- www %>"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<% if (esm) { -%>
|
|
2
|
+
<% if (httpErrors) { -%>
|
|
3
|
+
import createError from 'http-errors';
|
|
4
|
+
<% } -%>
|
|
5
|
+
import express from 'express';
|
|
6
|
+
<% if (ts) { -%>
|
|
7
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
8
|
+
<% if (httpErrors) { -%>
|
|
9
|
+
import type { HttpError } from 'http-errors';
|
|
10
|
+
<% } -%>
|
|
11
|
+
<% } -%>
|
|
12
|
+
<% if (!api) { -%>
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
<% } -%>
|
|
15
|
+
<% Object.keys(modules).sort().forEach(function (variable) { -%>
|
|
16
|
+
import <%- named.has(variable) ? '{ ' + variable + ' }' : variable %> from '<%- modules[variable] %>';
|
|
17
|
+
<% }); -%>
|
|
18
|
+
|
|
19
|
+
<% Object.keys(localModules).sort().forEach(function (variable) { -%>
|
|
20
|
+
import <%- variable %> from '<%- localModules[variable] %>';
|
|
21
|
+
<% }); -%>
|
|
22
|
+
<% } else { -%>
|
|
23
|
+
<% if (httpErrors) { -%>
|
|
24
|
+
const createError = require('http-errors');
|
|
25
|
+
<% } -%>
|
|
26
|
+
const express = require('express');
|
|
27
|
+
<% if (!api) { -%>
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
<% } -%>
|
|
30
|
+
<% Object.keys(modules).sort().forEach(function (variable) { -%>
|
|
31
|
+
const <%- named.has(variable) ? '{ ' + variable + ' }' : variable %> = require('<%- modules[variable] %>');
|
|
32
|
+
<% }); -%>
|
|
33
|
+
|
|
34
|
+
<% Object.keys(localModules).sort().forEach(function (variable) { -%>
|
|
35
|
+
const <%- variable %> = require('<%- localModules[variable] %>');
|
|
36
|
+
<% }); -%>
|
|
37
|
+
<% } -%>
|
|
38
|
+
|
|
39
|
+
const app = express();
|
|
40
|
+
|
|
41
|
+
<% if (trustProxy) { -%>
|
|
42
|
+
// behind a load balancer or other proxy, trust the X-Forwarded-* headers it
|
|
43
|
+
// sets, so req.ip and req.secure describe the client: TRUST_PROXY is the
|
|
44
|
+
// number of proxies, or their addresses
|
|
45
|
+
if (process.env.TRUST_PROXY) {
|
|
46
|
+
const hops = Number(process.env.TRUST_PROXY);
|
|
47
|
+
app.set('trust proxy', Number.isInteger(hops) ? hops : process.env.TRUST_PROXY);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
<% } -%>
|
|
51
|
+
<% if (session) { -%>
|
|
52
|
+
// session cookies are signed with SESSION_SECRET, which production requires
|
|
53
|
+
if (!process.env.SESSION_SECRET && app.get('env') === 'production') {
|
|
54
|
+
throw new Error('Set SESSION_SECRET to use sessions in production');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
<% } -%>
|
|
58
|
+
// health check for load balancers and containers, defined before the
|
|
59
|
+
// middleware so it stays fast and out of the request logs
|
|
60
|
+
app.get('/health', (req, res) => {
|
|
61
|
+
res.json({ status: 'ok' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
<% if (view) { -%>
|
|
65
|
+
// view engine setup
|
|
66
|
+
<% if (view.render) { -%>
|
|
67
|
+
app.engine('<%- view.engine %>', <%- view.render %>);
|
|
68
|
+
<% } -%>
|
|
69
|
+
app.set('views', path.join(<%- dirname %>, 'views'));
|
|
70
|
+
app.set('view engine', '<%- view.engine %>');
|
|
71
|
+
|
|
72
|
+
<% } -%>
|
|
73
|
+
<% uses.forEach(function (use) { -%>
|
|
74
|
+
app.use(<%- use %>);
|
|
75
|
+
<% }); -%>
|
|
76
|
+
|
|
77
|
+
<% mounts.forEach(function (mount) { -%>
|
|
78
|
+
app.use(<%= mount.path %>, <%- mount.code %>);
|
|
79
|
+
<% }); -%>
|
|
80
|
+
|
|
81
|
+
<% if (view) { -%>
|
|
82
|
+
// catch 404 and forward to error handler
|
|
83
|
+
app.use((req, res, next) => {
|
|
84
|
+
next(createError(404));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// error handler
|
|
88
|
+
<% if (ts) { -%>
|
|
89
|
+
app.use((err: HttpError, req: Request, res: Response, next: NextFunction) => {
|
|
90
|
+
<% } else { -%>
|
|
91
|
+
app.use((err, req, res, next) => {
|
|
92
|
+
<% } -%>
|
|
93
|
+
const status = err.status || 500;
|
|
94
|
+
|
|
95
|
+
// log server errors, which are bugs, but not client errors
|
|
96
|
+
if (status >= 500) console.error(err);
|
|
97
|
+
|
|
98
|
+
const development = req.app.get('env') === 'development';
|
|
99
|
+
|
|
100
|
+
// set locals, only exposing client error messages, or every error in
|
|
101
|
+
// development, and only providing the error in development
|
|
102
|
+
res.locals.message = err.expose || development ? err.message : 'Internal Server Error';
|
|
103
|
+
res.locals.title = res.locals.message;
|
|
104
|
+
res.locals.status = status;
|
|
105
|
+
res.locals.error = development ? err : {};
|
|
106
|
+
|
|
107
|
+
// render the error page
|
|
108
|
+
res.status(status);
|
|
109
|
+
res.render('error');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
<% } else if (api) { -%>
|
|
113
|
+
// catch 404 and forward to error handler
|
|
114
|
+
app.use((req, res, next) => {
|
|
115
|
+
next(createError(404));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// error handler
|
|
119
|
+
<% if (ts) { -%>
|
|
120
|
+
app.use((err: HttpError, req: Request, res: Response, next: NextFunction) => {
|
|
121
|
+
<% } else { -%>
|
|
122
|
+
app.use((err, req, res, next) => {
|
|
123
|
+
<% } -%>
|
|
124
|
+
const status = err.status || 500;
|
|
125
|
+
const development = req.app.get('env') === 'development';
|
|
126
|
+
|
|
127
|
+
// log server errors, which are bugs, but not client errors
|
|
128
|
+
if (status >= 500) console.error(err);
|
|
129
|
+
|
|
130
|
+
// only expose client error messages, or every error in development
|
|
131
|
+
res.status(status).json({
|
|
132
|
+
error: err.expose || development ? err.message : 'Internal Server Error',
|
|
133
|
+
...(development && { stack: err.stack })
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
<% } else { -%>
|
|
138
|
+
// error handler, responding with plain text
|
|
139
|
+
<% if (ts) { -%>
|
|
140
|
+
app.use((err: Error & { status?: number; expose?: boolean }, req: Request, res: Response, next: NextFunction) => {
|
|
141
|
+
<% } else { -%>
|
|
142
|
+
app.use((err, req, res, next) => {
|
|
143
|
+
<% } -%>
|
|
144
|
+
const status = err.status || 500;
|
|
145
|
+
const development = req.app.get('env') === 'development';
|
|
146
|
+
|
|
147
|
+
// log server errors, which are bugs, but not client errors
|
|
148
|
+
if (status >= 500) console.error(err);
|
|
149
|
+
|
|
150
|
+
// only expose client error messages, or every error and its stack in development
|
|
151
|
+
let message = 'Internal Server Error';
|
|
152
|
+
if (development) message = err.stack ?? err.message;
|
|
153
|
+
else if (err.expose) message = err.message;
|
|
154
|
+
|
|
155
|
+
res.status(status).type('text').send(message);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
<% } -%>
|
|
159
|
+
<% if (esm) { -%>
|
|
160
|
+
export default app;
|
|
161
|
+
<% } else { -%>
|
|
162
|
+
module.exports = app;
|
|
163
|
+
<% } -%>
|