create-express-new 0.0.0-stage → 1.0.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +24 -0
  3. package/README.md +281 -2
  4. package/bin/express-cli.js +577 -0
  5. package/lib/app.js +607 -0
  6. package/lib/prompts.js +338 -0
  7. package/lib/wizard.js +397 -0
  8. package/package.json +65 -4
  9. package/templates/app/Dockerfile.ejs +64 -0
  10. package/templates/app/app.js.ejs +163 -0
  11. package/templates/app/csrf.js.ejs +21 -0
  12. package/templates/app/dockerignore +15 -0
  13. package/templates/app/env.example.ejs +40 -0
  14. package/templates/app/eslint.config.js.ejs +28 -0
  15. package/templates/app/gitignore +22 -0
  16. package/templates/app/npmrc +8 -0
  17. package/templates/app/pnpm-workspace.yaml +24 -0
  18. package/templates/app/routes/index.js.ejs +22 -0
  19. package/templates/app/routes/uploads.js.ejs +94 -0
  20. package/templates/app/routes/users.js.ejs +42 -0
  21. package/templates/app/test/app.test.js.ejs +199 -0
  22. package/templates/app/www.js.ejs +46 -0
  23. package/templates/public/index.html +13 -0
  24. package/templates/public/stylesheets/style.css +8 -0
  25. package/templates/versions.json +49 -0
  26. package/templates/views/error.ejs +12 -0
  27. package/templates/views/error.hbs +3 -0
  28. package/templates/views/error.pug +6 -0
  29. package/templates/views/error.twig +7 -0
  30. package/templates/views/index.ejs +11 -0
  31. package/templates/views/index.hbs +2 -0
  32. package/templates/views/index.pug +5 -0
  33. package/templates/views/index.twig +6 -0
  34. package/templates/views/layout.hbs +10 -0
  35. package/templates/views/layout.pug +7 -0
  36. package/templates/views/layout.twig +10 -0
  37. package/templates/views/new-user.ejs +18 -0
  38. package/templates/views/new-user.hbs +9 -0
  39. package/templates/views/new-user.pug +13 -0
  40. package/templates/views/new-user.twig +13 -0
  41. package/templates/views/uploads.ejs +16 -0
  42. package/templates/views/uploads.hbs +7 -0
  43. package/templates/views/uploads.pug +10 -0
  44. package/templates/views/uploads.twig +11 -0
package/lib/wizard.js ADDED
@@ -0,0 +1,397 @@
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
+
370
+ // npm create keeps the options before `--` for itself
371
+ if (flags.length && pm === 'npm') args.push('--')
372
+ args.push(...flags)
373
+
374
+ return args.join(' ')
375
+ }
376
+
377
+ /**
378
+ * Check if a directory is missing or empty.
379
+ */
380
+
381
+ function isEmpty (dir) {
382
+ try {
383
+ return fs.readdirSync(dir).length === 0
384
+ } catch (err) {
385
+ if (err.code === 'ENOENT') return true
386
+ if (err.code === 'ENOTDIR') return false
387
+ throw err
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Quote a shell argument when needed.
393
+ */
394
+
395
+ function quote (arg) {
396
+ return /^[\w./@:-]+$/.test(arg) ? arg : JSON.stringify(arg)
397
+ }
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "create-express-new",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "Express 5 application generator: ESM, TypeScript and CommonJS app templates. A modern successor to express-generator.",
4
+ "version": "1.0.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
+ <% } -%>