create-express-new 1.0.0 → 1.2.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 +30 -1
- package/README.md +53 -10
- package/bin/express-cli.js +73 -29
- package/lib/app.js +32 -6
- package/lib/wizard.js +48 -10
- package/package.json +15 -5
- package/templates/app/app.js.ejs +24 -0
- package/templates/app/csrf.js.ejs +11 -2
- package/templates/app/routes/api.js.ejs +26 -0
- package/templates/app/test/app.test.js.ejs +48 -0
- package/templates/versions.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-10-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `--api` with a view engine, such as `--api --view=pug`, generates a web app with a JSON API: a
|
|
14
|
+
`routes/api.js` router at `/api`, and JSON responses for every error under `/api`. The wizard offers
|
|
15
|
+
it as "Web app with a JSON API"
|
|
16
|
+
- `--cors-scope` and `--csrf-scope`, for a web app with a JSON API, choose whether other origins can
|
|
17
|
+
call only the API (default) or the whole app, and whether the API needs the CSRF token (default) or
|
|
18
|
+
only the pages do; the wizard asks both
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `--api` with `--view` is no longer an error, and `--session` and `--csrf` can be used with them
|
|
23
|
+
|
|
24
|
+
## [1.1.0] - 2026-10-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `--install` installs the dependencies after creating the app, with the app's package manager, as
|
|
29
|
+
the wizard already offered; the wizard's equivalent command includes it
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Generated apps use newer versions of `eslint`, `globals` and `@types/node`, and generated
|
|
34
|
+
Dockerfiles install newer versions of pnpm and npm
|
|
35
|
+
|
|
9
36
|
## [1.0.0] - 2026-10-10
|
|
10
37
|
|
|
11
38
|
The first release of `create-express-new`, a fork of
|
|
@@ -109,5 +136,7 @@ the full list and a table for migrating commands.
|
|
|
109
136
|
- The `-e/--ejs`, `--hbs`, `--pug` and `-H/--hogan` aliases; use `--view=<engine>`
|
|
110
137
|
- The `debug` package and `DEBUG=...` start instructions
|
|
111
138
|
|
|
112
|
-
[Unreleased]: https://github.com/w0244079/create-express-new/compare/v1.
|
|
139
|
+
[Unreleased]: https://github.com/w0244079/create-express-new/compare/v1.2.0...HEAD
|
|
140
|
+
[1.2.0]: https://github.com/w0244079/create-express-new/compare/v1.1.0...v1.2.0
|
|
141
|
+
[1.1.0]: https://github.com/w0244079/create-express-new/compare/v1.0.0...v1.1.0
|
|
113
142
|
[1.0.0]: https://github.com/w0244079/create-express-new/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ $ pnpm create express-new@latest
|
|
|
26
26
|
`npm create express-new@latest` starts the same wizard. It asks for:
|
|
27
27
|
|
|
28
28
|
- the project directory (`.` for the current directory)
|
|
29
|
-
- a web app or a JSON API, and the view engine
|
|
29
|
+
- a web app, a JSON API or a web app with a JSON API, and the view engine
|
|
30
30
|
- the language: JavaScript, TypeScript or CommonJS
|
|
31
31
|
- optional middleware, and CSRF protection with sessions
|
|
32
32
|
- the request logger
|
|
@@ -57,7 +57,7 @@ $ pnpm install
|
|
|
57
57
|
|
|
58
58
|
With `npm create`, put the options after `--`: `npm create express-new@latest my-app -- --ts --helmet`.
|
|
59
59
|
`npx create-express-new my-app --ts --helmet` needs no `--`. Either way the app requires pnpm unless
|
|
60
|
-
you add `--pm=npm`.
|
|
60
|
+
you add `--pm=npm`. Add `--install` to install the dependencies too, with the app's package manager.
|
|
61
61
|
|
|
62
62
|
Start it at `http://localhost:3000/`, and run its tests, written with the built-in
|
|
63
63
|
[`node:test`](https://nodejs.org/api/test.html) runner:
|
|
@@ -122,7 +122,40 @@ client errors include their message, server errors only say `Internal Server Err
|
|
|
122
122
|
and development adds the stack trace. Express runs in development mode unless `NODE_ENV` is set, so
|
|
123
123
|
set `NODE_ENV=production` when you deploy.
|
|
124
124
|
|
|
125
|
-
`--api` works with `--cjs`, `--ts` and the optional middleware,
|
|
125
|
+
`--api` works with `--cjs`, `--ts` and the optional middleware, except `--session`.
|
|
126
|
+
|
|
127
|
+
### Web apps with a JSON API
|
|
128
|
+
|
|
129
|
+
`--api` with a view engine, such as `--api --view=pug`, generates a web app that has a JSON API too:
|
|
130
|
+
everything a web app has, and a `routes/api.js` router mounted at `/api`.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
my-app
|
|
134
|
+
├── app.js # the web app, with JSON errors under /api
|
|
135
|
+
├── routes/
|
|
136
|
+
│ ├── api.js # JSON routes: GET /api and GET /api/users
|
|
137
|
+
│ ├── index.js # the home page
|
|
138
|
+
│ └── users.js
|
|
139
|
+
├── views/
|
|
140
|
+
└── ...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every error of a request under `/api` is JSON, in the same shape as a JSON API's: the 404 of an
|
|
144
|
+
unknown route, an error a route throws, and the errors of the middleware before the routes, such as a
|
|
145
|
+
request body that is not valid JSON. Every other error renders the `error` view.
|
|
146
|
+
|
|
147
|
+
With `--cors` and `--csrf`, you choose which routes they apply to:
|
|
148
|
+
|
|
149
|
+
- `--cors-scope=api` (default) lets other origins call the API only, not read the pages;
|
|
150
|
+
`--cors-scope=all` lets them call the whole app.
|
|
151
|
+
- `--csrf-scope=all` (default) requires the CSRF token under `/api` too, for an API your own pages
|
|
152
|
+
call; `--csrf-scope=pages` leaves the API out, for an API other clients call without the session
|
|
153
|
+
cookie.
|
|
154
|
+
|
|
155
|
+
The wizard asks both.
|
|
156
|
+
|
|
157
|
+
The view engine has to be given, as `--api` alone is a JSON API without views: use `--view=pug` for
|
|
158
|
+
the default one. `--api` does not add an API to a `--no-view` app.
|
|
126
159
|
|
|
127
160
|
### Module formats
|
|
128
161
|
|
|
@@ -158,7 +191,8 @@ files with `--api`). Add more with:
|
|
|
158
191
|
- `--cors`: [cors](https://github.com/expressjs/cors), allowing requests from other origins
|
|
159
192
|
- `--rate-limit`: [express-rate-limit](https://express-rate-limit.mintlify.app/), limiting each client
|
|
160
193
|
to 100 requests every 15 minutes
|
|
161
|
-
- `--session` (
|
|
194
|
+
- `--session` (not for JSON APIs without views): [express-session](https://github.com/expressjs/session),
|
|
195
|
+
for `req.session`
|
|
162
196
|
- `--csrf` (with `--session` and a view engine): [csrf-sync](https://github.com/Psifi-Solutions/csrf-sync)
|
|
163
197
|
CSRF protection for forms
|
|
164
198
|
- `--uploads`: [multer](https://github.com/expressjs/multer) and a `POST /uploads` route that takes one
|
|
@@ -221,20 +255,28 @@ support TypeScript 7 yet, these apps use TypeScript 6.
|
|
|
221
255
|
-v, --view <engine> add view <engine> support (ejs|hbs|pug|twig) (defaults to pug)
|
|
222
256
|
--no-view use static html instead of view engine
|
|
223
257
|
--api generate a JSON API, without views or static files
|
|
258
|
+
(with --view, a web app with a JSON API at /api)
|
|
224
259
|
--cjs generate CommonJS modules instead of ES modules
|
|
225
260
|
--ts generate TypeScript, run directly by Node.js
|
|
226
261
|
--helmet add helmet middleware for security headers
|
|
227
262
|
--compression add compression middleware for gzip/brotli responses
|
|
228
263
|
--cookies add cookie-parser middleware
|
|
229
264
|
--cors add cors middleware for cross-origin requests
|
|
265
|
+
--cors-scope <scope>
|
|
266
|
+
with --api and --view, the routes other origins can
|
|
267
|
+
call (api|all) (defaults to api)
|
|
230
268
|
--rate-limit add express-rate-limit to limit requests per client
|
|
231
|
-
--session add express-session for sessions (not with --api)
|
|
269
|
+
--session add express-session for sessions (not with --api alone)
|
|
232
270
|
--csrf add CSRF protection for forms (needs --session)
|
|
271
|
+
--csrf-scope <scope>
|
|
272
|
+
with --api and --view, the routes that need a CSRF
|
|
273
|
+
token (all|pages) (defaults to all)
|
|
233
274
|
--uploads add multer and an upload route at /uploads
|
|
234
275
|
--logger <name> request logger (morgan|pino) (defaults to morgan)
|
|
235
276
|
--docker add a Dockerfile for a production image
|
|
236
277
|
--lint add ESLint and a lint script
|
|
237
278
|
--pm <name> package manager the app requires (pnpm|npm) (defaults to pnpm)
|
|
279
|
+
--install install the dependencies after creating the app
|
|
238
280
|
--no-git skip the .gitignore
|
|
239
281
|
-f, --force force on non-empty directory
|
|
240
282
|
--keep-config keep existing config files (.env.example, Dockerfile,
|
|
@@ -256,13 +298,14 @@ pug in place of jade, for Node.js 22.9 or newer. `--view=<engine>` replaces the
|
|
|
256
298
|
## Contributing
|
|
257
299
|
|
|
258
300
|
```bash
|
|
259
|
-
$
|
|
260
|
-
$
|
|
261
|
-
$
|
|
301
|
+
$ pnpm install
|
|
302
|
+
$ pnpm test # generates, installs and runs apps for each option
|
|
303
|
+
$ pnpm lint
|
|
262
304
|
```
|
|
263
305
|
|
|
264
|
-
|
|
265
|
-
|
|
306
|
+
This repository requires pnpm 11 or newer, as generated apps do by default, so
|
|
307
|
+
[install pnpm](https://pnpm.io/installation) first; npm refuses to install it or run its scripts.
|
|
308
|
+
The tests that install an npm app are skipped unless your npm is version 12 or newer.
|
|
266
309
|
|
|
267
310
|
See [Maintaining](docs/maintaining.md) for keeping the dependency versions of generated apps current,
|
|
268
311
|
and for releasing.
|
package/bin/express-cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'node:fs'
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import readline from 'node:readline'
|
|
7
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'
|
|
8
|
+
import { CORS_SCOPES, CSRF_SCOPES, DEFAULT_PACKAGE_MANAGER, LOGGERS, PACKAGE_MANAGERS, VIEW_ENGINES, checkDestination, createAppName, mergeLines, planApp, scriptCommand } from '../lib/app.js'
|
|
9
9
|
import { CancelError } from '../lib/prompts.js'
|
|
10
10
|
import { wizard } from '../lib/wizard.js'
|
|
11
11
|
|
|
@@ -19,12 +19,15 @@ const OPTIONS = {
|
|
|
19
19
|
compression: { type: 'boolean' },
|
|
20
20
|
cookies: { type: 'boolean' },
|
|
21
21
|
cors: { type: 'boolean' },
|
|
22
|
+
'cors-scope': { type: 'string' },
|
|
22
23
|
csrf: { type: 'boolean' },
|
|
24
|
+
'csrf-scope': { type: 'string' },
|
|
23
25
|
docker: { type: 'boolean' },
|
|
24
26
|
force: { type: 'boolean', short: 'f' },
|
|
25
27
|
git: { type: 'boolean' },
|
|
26
28
|
helmet: { type: 'boolean' },
|
|
27
29
|
help: { type: 'boolean', short: 'h' },
|
|
30
|
+
install: { type: 'boolean' },
|
|
28
31
|
'keep-config': { type: 'boolean' },
|
|
29
32
|
lint: { type: 'boolean' },
|
|
30
33
|
logger: { type: 'string' },
|
|
@@ -44,7 +47,7 @@ const args = process.argv.slice(2)
|
|
|
44
47
|
|
|
45
48
|
if (args.length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
46
49
|
wizard({ input: process.stdin, output: process.stdout }).then((options) => {
|
|
47
|
-
main(options,
|
|
50
|
+
main(options, exit)
|
|
48
51
|
}, (err) => {
|
|
49
52
|
if (!(err instanceof CancelError)) throw err
|
|
50
53
|
console.error('aborting')
|
|
@@ -130,28 +133,24 @@ function createApplication (dir, entries, leftovers, options, done) {
|
|
|
130
133
|
'remove them if they are from an earlier app with other options')
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
if (options.install) {
|
|
137
|
+
installDependencies(dir, options.pm, done)
|
|
138
|
+
} else {
|
|
135
139
|
printNextSteps(dir, options.pm, true)
|
|
140
|
+
done(0)
|
|
136
141
|
}
|
|
137
|
-
|
|
138
|
-
done(0)
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
/**
|
|
142
|
-
* Install the generated app's dependencies with its package manager,
|
|
143
|
-
* the
|
|
145
|
+
* Install the generated app's dependencies with its package manager, then
|
|
146
|
+
* display the next steps.
|
|
144
147
|
*
|
|
145
|
-
* @param {
|
|
146
|
-
* @param {
|
|
148
|
+
* @param {string} dir
|
|
149
|
+
* @param {string} pm the app's package manager
|
|
150
|
+
* @param {function} done
|
|
147
151
|
*/
|
|
148
152
|
|
|
149
|
-
function installDependencies (
|
|
150
|
-
if (code !== 0 || !options.install) return exit(code)
|
|
151
|
-
|
|
152
|
-
const dir = options._[0]
|
|
153
|
-
const pm = options.pm
|
|
154
|
-
|
|
153
|
+
function installDependencies (dir, pm, done) {
|
|
155
154
|
console.log(' installing dependencies...')
|
|
156
155
|
console.log()
|
|
157
156
|
|
|
@@ -170,14 +169,14 @@ function installDependencies (options, code) {
|
|
|
170
169
|
? pm + ' is not installed, see ' + PACKAGE_MANAGERS[pm].docs
|
|
171
170
|
: pm + ' install failed: ' + err.message)
|
|
172
171
|
printNextSteps(dir, pm, true)
|
|
173
|
-
|
|
172
|
+
done(1)
|
|
174
173
|
})
|
|
175
174
|
|
|
176
175
|
child.on('close', (status) => {
|
|
177
176
|
if (failed) return
|
|
178
177
|
if (status !== 0) error(pm + ' install failed')
|
|
179
178
|
printNextSteps(dir, pm, status !== 0)
|
|
180
|
-
|
|
179
|
+
done(status === 0 ? 0 : 1)
|
|
181
180
|
})
|
|
182
181
|
}
|
|
183
182
|
|
|
@@ -286,6 +285,14 @@ function main (options, done) {
|
|
|
286
285
|
usage()
|
|
287
286
|
error('option `--pm <name>\' argument missing')
|
|
288
287
|
done(1)
|
|
288
|
+
} else if (options.corsScope === '') {
|
|
289
|
+
usage()
|
|
290
|
+
error('option `--cors-scope <scope>\' argument missing')
|
|
291
|
+
done(1)
|
|
292
|
+
} else if (options.csrfScope === '') {
|
|
293
|
+
usage()
|
|
294
|
+
error('option `--csrf-scope <scope>\' argument missing')
|
|
295
|
+
done(1)
|
|
289
296
|
} else {
|
|
290
297
|
// Path
|
|
291
298
|
const destinationPath = options._[0] || '.'
|
|
@@ -293,14 +300,9 @@ function main (options, done) {
|
|
|
293
300
|
// App name
|
|
294
301
|
const appName = createAppName(path.resolve(destinationPath)) || 'hello-world'
|
|
295
302
|
|
|
296
|
-
// A JSON API has no view engine
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
usage()
|
|
300
|
-
error('option `--api\' cannot be used with `--view\'')
|
|
301
|
-
return done(1)
|
|
302
|
-
}
|
|
303
|
-
|
|
303
|
+
// A JSON API has no view engine, unless one is given with `--view`,
|
|
304
|
+
// for a web app with a JSON API
|
|
305
|
+
if (options.api && typeof options.view !== 'string') {
|
|
304
306
|
options.view = false
|
|
305
307
|
}
|
|
306
308
|
|
|
@@ -343,9 +345,9 @@ function main (options, done) {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
// Sessions are for web apps, and CSRF protection needs them and views
|
|
346
|
-
if (options.session && options.api) {
|
|
348
|
+
if (options.session && options.api && !options.view) {
|
|
347
349
|
usage()
|
|
348
|
-
error('option `--session\' cannot be used with `--api\'')
|
|
350
|
+
error('option `--session\' cannot be used with `--api\', unless `--view\' is given too')
|
|
349
351
|
return done(1)
|
|
350
352
|
}
|
|
351
353
|
|
|
@@ -361,6 +363,38 @@ function main (options, done) {
|
|
|
361
363
|
return done(1)
|
|
362
364
|
}
|
|
363
365
|
|
|
366
|
+
// Where CORS and CSRF protection apply is a choice only for a web app
|
|
367
|
+
// with a JSON API
|
|
368
|
+
const webApi = Boolean(options.api && options.view)
|
|
369
|
+
|
|
370
|
+
if (options.corsScope !== undefined) {
|
|
371
|
+
if (!CORS_SCOPES.includes(options.corsScope)) {
|
|
372
|
+
usage()
|
|
373
|
+
error('unsupported CORS scope `' + options.corsScope + "'")
|
|
374
|
+
return done(1)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!webApi || !options.cors) {
|
|
378
|
+
usage()
|
|
379
|
+
error('option `--cors-scope\' requires `--cors\', `--api\' and `--view\'')
|
|
380
|
+
return done(1)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (options.csrfScope !== undefined) {
|
|
385
|
+
if (!CSRF_SCOPES.includes(options.csrfScope)) {
|
|
386
|
+
usage()
|
|
387
|
+
error('unsupported CSRF scope `' + options.csrfScope + "'")
|
|
388
|
+
return done(1)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!webApi || !options.csrf) {
|
|
392
|
+
usage()
|
|
393
|
+
error('option `--csrf-scope\' requires `--csrf\', `--api\' and `--view\'')
|
|
394
|
+
return done(1)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
364
398
|
// Check the destination before writing anything
|
|
365
399
|
const entries = planApp(appName, options)
|
|
366
400
|
const { blockers, conflicts, leftovers } = checkDestination(destinationPath, entries)
|
|
@@ -449,6 +483,8 @@ function parseOptions (argv) {
|
|
|
449
483
|
options.git = !options['no-git']
|
|
450
484
|
options.keepConfig = Boolean(options['keep-config'])
|
|
451
485
|
options.rateLimit = Boolean(options['rate-limit'])
|
|
486
|
+
options.corsScope = options['cors-scope']
|
|
487
|
+
options.csrfScope = options['csrf-scope']
|
|
452
488
|
options.logger ??= 'morgan'
|
|
453
489
|
options.pm ??= DEFAULT_PACKAGE_MANAGER
|
|
454
490
|
|
|
@@ -473,20 +509,28 @@ function usage () {
|
|
|
473
509
|
console.log(' -v, --view <engine> add view <engine> support (ejs|hbs|pug|twig) (defaults to pug)')
|
|
474
510
|
console.log(' --no-view use static html instead of view engine')
|
|
475
511
|
console.log(' --api generate a JSON API, without views or static files')
|
|
512
|
+
console.log(' (with --view, a web app with a JSON API at /api)')
|
|
476
513
|
console.log(' --cjs generate CommonJS modules instead of ES modules')
|
|
477
514
|
console.log(' --ts generate TypeScript, run directly by Node.js')
|
|
478
515
|
console.log(' --helmet add helmet middleware for security headers')
|
|
479
516
|
console.log(' --compression add compression middleware for gzip/brotli responses')
|
|
480
517
|
console.log(' --cookies add cookie-parser middleware')
|
|
481
518
|
console.log(' --cors add cors middleware for cross-origin requests')
|
|
519
|
+
console.log(' --cors-scope <scope>')
|
|
520
|
+
console.log(' with --api and --view, the routes other origins can')
|
|
521
|
+
console.log(' call (api|all) (defaults to api)')
|
|
482
522
|
console.log(' --rate-limit add express-rate-limit to limit requests per client')
|
|
483
|
-
console.log(' --session add express-session for sessions (not with --api)')
|
|
523
|
+
console.log(' --session add express-session for sessions (not with --api alone)')
|
|
484
524
|
console.log(' --csrf add CSRF protection for forms (needs --session)')
|
|
525
|
+
console.log(' --csrf-scope <scope>')
|
|
526
|
+
console.log(' with --api and --view, the routes that need a CSRF')
|
|
527
|
+
console.log(' token (all|pages) (defaults to all)')
|
|
485
528
|
console.log(' --uploads add multer and an upload route at /uploads')
|
|
486
529
|
console.log(' --logger <name> request logger (morgan|pino) (defaults to morgan)')
|
|
487
530
|
console.log(' --docker add a Dockerfile for a production image')
|
|
488
531
|
console.log(' --lint add ESLint and a lint script')
|
|
489
532
|
console.log(' --pm <name> package manager the app requires (pnpm|npm) (defaults to pnpm)')
|
|
533
|
+
console.log(' --install install the dependencies after creating the app')
|
|
490
534
|
console.log(' --no-git skip the .gitignore')
|
|
491
535
|
console.log(' -f, --force force on non-empty directory')
|
|
492
536
|
console.log(' --keep-config keep existing config files (.env.example, Dockerfile,')
|
package/lib/app.js
CHANGED
|
@@ -35,6 +35,11 @@ export const PACKAGE_MANAGERS = {
|
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_PACKAGE_MANAGER = 'pnpm'
|
|
37
37
|
|
|
38
|
+
// where the CORS headers and the CSRF protection of a web app with a JSON
|
|
39
|
+
// API apply, with the default first
|
|
40
|
+
export const CORS_SCOPES = ['api', 'all']
|
|
41
|
+
export const CSRF_SCOPES = ['all', 'pages']
|
|
42
|
+
|
|
38
43
|
// views generated only with some options
|
|
39
44
|
const OPTIONAL_VIEWS = { 'new-user': 'csrf', uploads: 'uploads' }
|
|
40
45
|
|
|
@@ -75,7 +80,7 @@ const TSCONFIG = {
|
|
|
75
80
|
// not use: code in the other language, the other ESLint config, the pnpm
|
|
76
81
|
// settings and the templates of other view engines
|
|
77
82
|
const ALTERNATIVES = [
|
|
78
|
-
...['app', 'bin/www', 'csrf', 'routes/index', 'routes/uploads', 'routes/users', 'test/app.test'].flatMap((file) => [file + '.js', file + '.ts']),
|
|
83
|
+
...['app', 'bin/www', 'csrf', 'routes/api', 'routes/index', 'routes/uploads', 'routes/users', 'test/app.test'].flatMap((file) => [file + '.js', file + '.ts']),
|
|
79
84
|
'eslint.config.js',
|
|
80
85
|
'eslint.config.mjs',
|
|
81
86
|
'pnpm-workspace.yaml',
|
|
@@ -117,13 +122,21 @@ export function planApp (name, options) {
|
|
|
117
122
|
const esm = !options.cjs
|
|
118
123
|
const dirname = esm ? 'import.meta.dirname' : '__dirname'
|
|
119
124
|
|
|
120
|
-
// App kind: a JSON API has no views or static files
|
|
121
|
-
|
|
125
|
+
// App kind: a JSON API has no views or static files, and a web app with
|
|
126
|
+
// a view engine can have a JSON API too, under /api
|
|
127
|
+
const api = Boolean(options.api) && !options.view
|
|
128
|
+
const webApi = Boolean(options.api) && Boolean(options.view)
|
|
122
129
|
|
|
123
130
|
// Sessions, and CSRF protection using them, are for web apps with views
|
|
124
131
|
const session = Boolean(options.session) && !api
|
|
125
132
|
const csrf = Boolean(options.csrf) && session && Boolean(options.view)
|
|
126
133
|
|
|
134
|
+
// In a web app with a JSON API, other origins can call only the API,
|
|
135
|
+
// unless the whole app is chosen, and CSRF protection covers the API,
|
|
136
|
+
// unless only the pages are chosen
|
|
137
|
+
const corsApi = webApi && options.corsScope !== 'all'
|
|
138
|
+
const csrfSkipApi = webApi && csrf && options.csrfScope === 'pages'
|
|
139
|
+
|
|
127
140
|
// File uploads, with an upload form in apps with views
|
|
128
141
|
const uploads = Boolean(options.uploads)
|
|
129
142
|
|
|
@@ -182,6 +195,7 @@ export function planApp (name, options) {
|
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
app.locals.dirname = dirname
|
|
198
|
+
app.locals.webApi = webApi
|
|
185
199
|
app.locals.httpErrors = Boolean(options.view) || api
|
|
186
200
|
app.locals.trustProxy = trustProxy
|
|
187
201
|
app.locals.session = session
|
|
@@ -191,6 +205,8 @@ export function planApp (name, options) {
|
|
|
191
205
|
test.locals.csrf = csrf
|
|
192
206
|
test.locals.uploads = uploads
|
|
193
207
|
test.locals.view = Boolean(options.view)
|
|
208
|
+
test.locals.webApi = webApi
|
|
209
|
+
test.locals.csrfSkipApi = csrfSkipApi
|
|
194
210
|
|
|
195
211
|
// App modules
|
|
196
212
|
app.locals.localModules = Object.create(null)
|
|
@@ -216,7 +232,7 @@ export function planApp (name, options) {
|
|
|
216
232
|
// Cross-origin requests
|
|
217
233
|
if (options.cors) {
|
|
218
234
|
app.locals.modules.cors = 'cors'
|
|
219
|
-
app.locals.uses.push("cors({\n // any origin, or only the comma-separated origins in CORS_ORIGIN\n origin: process.env.CORS_ORIGIN?.split(',').map((origin) => origin.trim()) ?? '*'\n})")
|
|
235
|
+
app.locals.uses.push((corsApi ? "'/api', " : '') + "cors({\n // any origin, or only the comma-separated origins in CORS_ORIGIN\n origin: process.env.CORS_ORIGIN?.split(',').map((origin) => origin.trim()) ?? '*'\n})")
|
|
220
236
|
pkg.dependencies.cors = VERSIONS.cors
|
|
221
237
|
}
|
|
222
238
|
|
|
@@ -304,6 +320,16 @@ export function planApp (name, options) {
|
|
|
304
320
|
pkg.dependencies['http-errors'] = VERSIONS['http-errors']
|
|
305
321
|
}
|
|
306
322
|
|
|
323
|
+
// API router, for the JSON routes of a web app
|
|
324
|
+
if (webApi) {
|
|
325
|
+
const router = loadTemplate('app/routes/api.js')
|
|
326
|
+
router.locals.esm = esm
|
|
327
|
+
file('routes/api.' + ext, router.render())
|
|
328
|
+
|
|
329
|
+
app.locals.localModules.apiRouter = './routes/api.' + ext
|
|
330
|
+
app.locals.mounts.push({ path: '/api', code: 'apiRouter' })
|
|
331
|
+
}
|
|
332
|
+
|
|
307
333
|
// Template support
|
|
308
334
|
if (options.view) {
|
|
309
335
|
const view = VIEW_ENGINES[options.view]
|
|
@@ -373,6 +399,7 @@ export function planApp (name, options) {
|
|
|
373
399
|
const module = loadTemplate('app/csrf.js')
|
|
374
400
|
module.locals.esm = esm
|
|
375
401
|
module.locals.uploads = uploads
|
|
402
|
+
module.locals.skipApi = csrfSkipApi
|
|
376
403
|
file('csrf.' + ext, module.render())
|
|
377
404
|
|
|
378
405
|
app.locals.localModules.csrf = './csrf.' + ext
|
|
@@ -550,8 +577,7 @@ export function scriptCommand (pm, script) {
|
|
|
550
577
|
|
|
551
578
|
export function mergeLines (existing, generated) {
|
|
552
579
|
const have = new Set(existing.split(/\r?\n/).map((line) => line.trim()))
|
|
553
|
-
const missing = generated.split('\n')
|
|
554
|
-
.map((line) => line.trim())
|
|
580
|
+
const missing = [...new Set(generated.split('\n').map((line) => line.trim()))]
|
|
555
581
|
.filter((line) => line && !line.startsWith('#') && !have.has(line))
|
|
556
582
|
|
|
557
583
|
if (!missing.length) return existing
|
package/lib/wizard.js
CHANGED
|
@@ -47,14 +47,20 @@ export async function wizard (io) {
|
|
|
47
47
|
initial: answers.kind,
|
|
48
48
|
choices: [
|
|
49
49
|
{ label: 'Web app', value: 'web', hint: 'server-rendered views' },
|
|
50
|
-
{ label: 'JSON API', value: 'api', hint: 'no views or static files' }
|
|
50
|
+
{ label: 'JSON API', value: 'api', hint: 'no views or static files' },
|
|
51
|
+
{ label: 'Web app with a JSON API', value: 'both', hint: 'under /api' }
|
|
51
52
|
]
|
|
52
53
|
})
|
|
53
54
|
},
|
|
54
55
|
{
|
|
55
56
|
name: 'view',
|
|
56
|
-
when: () => answers.kind
|
|
57
|
-
ask: (step) => step.ask(select, {
|
|
57
|
+
when: () => answers.kind !== 'api',
|
|
58
|
+
ask: (step) => step.ask(select, {
|
|
59
|
+
message: 'View engine',
|
|
60
|
+
initial: answers.view,
|
|
61
|
+
// the pages of a web app with a JSON API need a view engine
|
|
62
|
+
choices: answers.kind === 'both' ? VIEWS.filter((view) => view.value !== false) : VIEWS
|
|
63
|
+
})
|
|
58
64
|
},
|
|
59
65
|
{
|
|
60
66
|
name: 'language',
|
|
@@ -80,16 +86,40 @@ export async function wizard (io) {
|
|
|
80
86
|
{ label: 'cors', value: 'cors', hint: 'cross-origin requests' },
|
|
81
87
|
{ label: 'express-rate-limit', value: 'rate-limit', hint: 'limit requests per client' },
|
|
82
88
|
// sessions are for web apps
|
|
83
|
-
...(answers.kind
|
|
89
|
+
...(answers.kind !== 'api' ? [{ label: 'express-session', value: 'session', hint: 'req.session' }] : []),
|
|
84
90
|
{ label: 'multer', value: 'uploads', hint: 'file uploads at /uploads' }
|
|
85
91
|
]
|
|
86
92
|
})
|
|
87
93
|
},
|
|
94
|
+
{
|
|
95
|
+
name: 'corsScope',
|
|
96
|
+
when: () => answers.kind === 'both' && answers.middleware.includes('cors'),
|
|
97
|
+
ask: (step) => step.ask(select, {
|
|
98
|
+
message: 'Other origins can call',
|
|
99
|
+
initial: answers.corsScope,
|
|
100
|
+
choices: [
|
|
101
|
+
{ label: 'The API only', value: 'api', hint: 'routes under /api' },
|
|
102
|
+
{ label: 'The whole app', value: 'all', hint: 'the pages too' }
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
},
|
|
88
106
|
{
|
|
89
107
|
name: 'csrf',
|
|
90
|
-
when: () => answers.kind
|
|
108
|
+
when: () => answers.kind !== 'api' && answers.view !== false && answers.middleware.includes('session'),
|
|
91
109
|
ask: (step) => step.ask(confirm, { message: 'Protect forms with CSRF tokens?', initial: answers.csrf ?? true })
|
|
92
110
|
},
|
|
111
|
+
{
|
|
112
|
+
name: 'csrfScope',
|
|
113
|
+
when: () => answers.kind === 'both' && answers.middleware.includes('session') && answers.csrf === true,
|
|
114
|
+
ask: (step) => step.ask(select, {
|
|
115
|
+
message: 'CSRF tokens for the API',
|
|
116
|
+
initial: answers.csrfScope,
|
|
117
|
+
choices: [
|
|
118
|
+
{ label: 'Required', value: 'all', hint: 'your pages call the API' },
|
|
119
|
+
{ label: 'Not required', value: 'pages', hint: 'other clients call it, without the session cookie' }
|
|
120
|
+
]
|
|
121
|
+
})
|
|
122
|
+
},
|
|
93
123
|
{
|
|
94
124
|
name: 'logger',
|
|
95
125
|
ask: (step) => step.ask(select, {
|
|
@@ -305,18 +335,22 @@ function eraseText (io, printed) {
|
|
|
305
335
|
|
|
306
336
|
function toOptions (answers) {
|
|
307
337
|
const { directory, kind, language, middleware = [], extras = [] } = answers
|
|
308
|
-
const view = kind === '
|
|
309
|
-
const session = kind
|
|
338
|
+
const view = kind === 'api' ? false : answers.view
|
|
339
|
+
const session = kind !== 'api' && middleware.includes('session')
|
|
340
|
+
const csrf = session && view !== false && answers.csrf === true
|
|
310
341
|
|
|
311
342
|
return {
|
|
312
343
|
_: [directory.dir],
|
|
313
344
|
'!': [],
|
|
314
|
-
api: kind
|
|
345
|
+
api: kind !== 'web',
|
|
315
346
|
cjs: language === 'cjs',
|
|
316
347
|
compression: middleware.includes('compression'),
|
|
317
348
|
cookies: middleware.includes('cookies'),
|
|
318
349
|
cors: middleware.includes('cors'),
|
|
319
|
-
|
|
350
|
+
// the choices of a web app with a JSON API
|
|
351
|
+
...(kind === 'both' && middleware.includes('cors') && { corsScope: answers.corsScope ?? 'api' }),
|
|
352
|
+
csrf,
|
|
353
|
+
...(kind === 'both' && csrf && { csrfScope: answers.csrfScope ?? 'all' }),
|
|
320
354
|
docker: extras.includes('docker'),
|
|
321
355
|
force: directory.force,
|
|
322
356
|
git: answers.git,
|
|
@@ -345,7 +379,8 @@ export function toCommand (options) {
|
|
|
345
379
|
const args = [pm, 'create', 'express-new@latest', quote(options._[0])]
|
|
346
380
|
const flags = []
|
|
347
381
|
|
|
348
|
-
|
|
382
|
+
// a JSON API has no view engine, unless one is given, the default included
|
|
383
|
+
if (options.api) flags.push('--api', ...(options.view ? ['--view=' + options.view] : []))
|
|
349
384
|
else if (options.view === false) flags.push('--no-view')
|
|
350
385
|
else if (options.view !== 'pug') flags.push('--view=' + options.view)
|
|
351
386
|
|
|
@@ -355,9 +390,11 @@ export function toCommand (options) {
|
|
|
355
390
|
if (options.compression) flags.push('--compression')
|
|
356
391
|
if (options.cookies) flags.push('--cookies')
|
|
357
392
|
if (options.cors) flags.push('--cors')
|
|
393
|
+
if (options.corsScope === 'all') flags.push('--cors-scope=all')
|
|
358
394
|
if (options.rateLimit) flags.push('--rate-limit')
|
|
359
395
|
if (options.session) flags.push('--session')
|
|
360
396
|
if (options.csrf) flags.push('--csrf')
|
|
397
|
+
if (options.csrfScope === 'pages') flags.push('--csrf-scope=pages')
|
|
361
398
|
if (options.uploads) flags.push('--uploads')
|
|
362
399
|
if (options.logger && options.logger !== 'morgan') flags.push('--logger=' + options.logger)
|
|
363
400
|
if (options.docker) flags.push('--docker')
|
|
@@ -366,6 +403,7 @@ export function toCommand (options) {
|
|
|
366
403
|
if (!options.git) flags.push('--no-git')
|
|
367
404
|
if (options.force) flags.push('--force')
|
|
368
405
|
if (options.keepConfig) flags.push('--keep-config')
|
|
406
|
+
if (options.install) flags.push('--install')
|
|
369
407
|
|
|
370
408
|
// npm create keeps the options before `--` for itself
|
|
371
409
|
if (flags.length && pm === 'npm') args.push('--')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-express-new",
|
|
3
3
|
"description": "Express 5 application generator: ESM, TypeScript and CommonJS app templates. A modern successor to express-generator.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"author": "Michael Caines (https://github.com/w0244079)",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"TJ Holowaychuk <tj@vision-media.ca>",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"web",
|
|
28
28
|
"api"
|
|
29
29
|
],
|
|
30
|
-
"repository":
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "github:w0244079/create-express-new"
|
|
33
|
+
},
|
|
31
34
|
"homepage": "https://github.com/w0244079/create-express-new#readme",
|
|
32
35
|
"bugs": "https://github.com/w0244079/create-express-new/issues",
|
|
33
36
|
"license": "MIT",
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"eslint": "^9.39.5",
|
|
47
|
+
"fast-check": "^4.10.2",
|
|
44
48
|
"mocha": "^12.0.3",
|
|
45
49
|
"neostandard": "^0.13.0",
|
|
46
50
|
"supertest": "^7.3.0",
|
|
@@ -50,6 +54,13 @@
|
|
|
50
54
|
"engines": {
|
|
51
55
|
"node": ">=22.9"
|
|
52
56
|
},
|
|
57
|
+
"devEngines": {
|
|
58
|
+
"packageManager": {
|
|
59
|
+
"name": "pnpm",
|
|
60
|
+
"version": ">=11",
|
|
61
|
+
"onFail": "error"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
53
64
|
"files": [
|
|
54
65
|
"CHANGELOG.md",
|
|
55
66
|
"LICENSE",
|
|
@@ -61,7 +72,6 @@
|
|
|
61
72
|
"lint": "eslint .",
|
|
62
73
|
"test": "mocha --reporter spec --bail --check-leaks test/",
|
|
63
74
|
"test-ci": "mocha --reporter spec --check-leaks test/",
|
|
64
|
-
"versions": "node scripts/versions.js"
|
|
65
|
-
"prepublishOnly": "npm run lint && npm run test-ci"
|
|
75
|
+
"versions": "node scripts/versions.js"
|
|
66
76
|
}
|
|
67
|
-
}
|
|
77
|
+
}
|
package/templates/app/app.js.ejs
CHANGED
|
@@ -84,7 +84,31 @@ app.use((req, res, next) => {
|
|
|
84
84
|
next(createError(404));
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
<% if (webApi) { -%>
|
|
88
|
+
// error handler for the API, responding with JSON to every error of a
|
|
89
|
+
// request under /api, including those of the middleware above
|
|
90
|
+
<% if (ts) { -%>
|
|
91
|
+
app.use('/api', (err: HttpError, req: Request, res: Response, next: NextFunction) => {
|
|
92
|
+
<% } else { -%>
|
|
93
|
+
app.use('/api', (err, req, res, next) => {
|
|
94
|
+
<% } -%>
|
|
95
|
+
const status = err.status || 500;
|
|
96
|
+
const development = req.app.get('env') === 'development';
|
|
97
|
+
|
|
98
|
+
// log server errors, which are bugs, but not client errors
|
|
99
|
+
if (status >= 500) console.error(err);
|
|
100
|
+
|
|
101
|
+
// only expose client error messages, or every error in development
|
|
102
|
+
res.status(status).json({
|
|
103
|
+
error: err.expose || development ? err.message : 'Internal Server Error',
|
|
104
|
+
...(development && { stack: err.stack })
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// error handler for the pages
|
|
109
|
+
<% } else { -%>
|
|
87
110
|
// error handler
|
|
111
|
+
<% } -%>
|
|
88
112
|
<% if (ts) { -%>
|
|
89
113
|
app.use((err: HttpError, req: Request, res: Response, next: NextFunction) => {
|
|
90
114
|
<% } else { -%>
|
|
@@ -7,8 +7,17 @@ const { csrfSync } = require('csrf-sync');
|
|
|
7
7
|
// CSRF protection for requests that change data, which must send the token
|
|
8
8
|
// in a _csrf form field or an x-csrf-token header (GET is not checked)
|
|
9
9
|
const csrf = csrfSync({
|
|
10
|
-
getTokenFromRequest: (req) => req.body?._csrf ?? req.get('x-csrf-token')<%- uploads ? ',' : '' %>
|
|
11
|
-
<% if (uploads) { -%>
|
|
10
|
+
getTokenFromRequest: (req) => req.body?._csrf ?? req.get('x-csrf-token')<%- uploads || skipApi ? ',' : '' %>
|
|
11
|
+
<% if (uploads && skipApi) { -%>
|
|
12
|
+
// uploads check the token themselves, as multer reads their form, and the
|
|
13
|
+
// API is not protected, so its routes must not trust the session cookie:
|
|
14
|
+
// authenticate them another way, such as a token in a header
|
|
15
|
+
skipCsrfProtection: (req) => req.path === '/uploads' || /^\/api(\/|$)/i.test(req.path)
|
|
16
|
+
<% } else if (skipApi) { -%>
|
|
17
|
+
// the API is not protected, so its routes must not trust the session
|
|
18
|
+
// cookie: authenticate them another way, such as a token in a header
|
|
19
|
+
skipCsrfProtection: (req) => /^\/api(\/|$)/i.test(req.path)
|
|
20
|
+
<% } else if (uploads) { -%>
|
|
12
21
|
// uploads check the token themselves, as multer reads their form
|
|
13
22
|
skipCsrfProtection: (req) => req.path === '/uploads'
|
|
14
23
|
<% } -%>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<% if (esm) { -%>
|
|
2
|
+
import express from 'express';
|
|
3
|
+
<% } else { -%>
|
|
4
|
+
const express = require('express');
|
|
5
|
+
<% } -%>
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// JSON routes, mounted at /api: errors under /api, such as the 404 of a
|
|
10
|
+
// route that is not here, respond with JSON too (see the app's error handlers)
|
|
11
|
+
|
|
12
|
+
/* GET the API's welcome message. */
|
|
13
|
+
router.get('/', (req, res) => {
|
|
14
|
+
res.json({ message: 'Welcome to Express' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/* GET users listing. */
|
|
18
|
+
router.get('/users', (req, res) => {
|
|
19
|
+
res.json([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
<% if (esm) { -%>
|
|
23
|
+
export default router;
|
|
24
|
+
<% } else { -%>
|
|
25
|
+
module.exports = router;
|
|
26
|
+
<% } -%>
|
|
@@ -194,6 +194,54 @@ describe('app', () => {
|
|
|
194
194
|
it('responds with 404 for unknown routes', async () => {
|
|
195
195
|
const res = await fetch(`${baseUrl}/does-not-exist`);
|
|
196
196
|
assert.strictEqual(res.status, 404);
|
|
197
|
+
<% if (webApi) { -%>
|
|
198
|
+
assert.match(res.headers.get('content-type') ?? '', /text\/html/);
|
|
199
|
+
<% } -%>
|
|
200
|
+
});
|
|
201
|
+
<% if (webApi) { -%>
|
|
202
|
+
|
|
203
|
+
it('responds to GET /api', async () => {
|
|
204
|
+
const res = await fetch(`${baseUrl}/api`);
|
|
205
|
+
assert.strictEqual(res.status, 200);
|
|
206
|
+
assert.deepStrictEqual(await res.json(), { message: 'Welcome to Express' });
|
|
197
207
|
});
|
|
208
|
+
|
|
209
|
+
it('responds to GET /api/users', async () => {
|
|
210
|
+
const res = await fetch(`${baseUrl}/api/users`);
|
|
211
|
+
assert.strictEqual(res.status, 200);
|
|
212
|
+
assert.deepStrictEqual(await res.json(), []);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('responds with a JSON 404 for unknown API routes', async () => {
|
|
216
|
+
const res = await fetch(`${baseUrl}/api/does-not-exist`);
|
|
217
|
+
assert.strictEqual(res.status, 404);
|
|
218
|
+
assert.match(res.headers.get('content-type') ?? '', /application\/json/);
|
|
219
|
+
assert.strictEqual((await res.json()).error, 'Not Found');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('responds with a JSON error for an invalid API request body', async () => {
|
|
223
|
+
const res = await fetch(`${baseUrl}/api/users`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'content-type': 'application/json' },
|
|
226
|
+
body: '{'
|
|
227
|
+
});
|
|
228
|
+
assert.strictEqual(res.status, 400);
|
|
229
|
+
assert.match(res.headers.get('content-type') ?? '', /application\/json/);
|
|
230
|
+
});
|
|
231
|
+
<% if (csrf && csrfSkipApi) { -%>
|
|
232
|
+
|
|
233
|
+
it('does not ask API posts for a CSRF token', async () => {
|
|
234
|
+
const res = await fetch(`${baseUrl}/api/does-not-exist`, { method: 'POST' });
|
|
235
|
+
assert.strictEqual(res.status, 404);
|
|
236
|
+
});
|
|
237
|
+
<% } else if (csrf) { -%>
|
|
238
|
+
|
|
239
|
+
it('rejects API posts without a CSRF token, with a JSON error', async () => {
|
|
240
|
+
const res = await fetch(`${baseUrl}/api/users`, { method: 'POST' });
|
|
241
|
+
assert.strictEqual(res.status, 403);
|
|
242
|
+
assert.match(res.headers.get('content-type') ?? '', /application\/json/);
|
|
243
|
+
});
|
|
244
|
+
<% } -%>
|
|
245
|
+
<% } -%>
|
|
198
246
|
});
|
|
199
247
|
<% } -%>
|
package/templates/versions.json
CHANGED
|
@@ -9,26 +9,26 @@
|
|
|
9
9
|
"@types/http-errors": "^2.0.5",
|
|
10
10
|
"@types/morgan": "^1.9.10",
|
|
11
11
|
"@types/multer": "^2.3.0",
|
|
12
|
-
"@types/node": "^22.20.
|
|
12
|
+
"@types/node": "^22.20.5",
|
|
13
13
|
"compression": "^1.8.2",
|
|
14
14
|
"cookie-parser": "^1.4.7",
|
|
15
15
|
"cors": "^2.8.6",
|
|
16
16
|
"csrf-sync": "^4.2.1",
|
|
17
17
|
"ejs": "^6.0.1",
|
|
18
|
-
"eslint": "^10.
|
|
18
|
+
"eslint": "^10.12.0",
|
|
19
19
|
"express": "^5.2.1",
|
|
20
20
|
"express-rate-limit": "^8.7.0",
|
|
21
21
|
"express-session": "^1.19.0",
|
|
22
|
-
"globals": "^17.
|
|
22
|
+
"globals": "^17.13.0",
|
|
23
23
|
"hbs": "^4.3.1",
|
|
24
24
|
"helmet": "^8.3.0",
|
|
25
25
|
"http-errors": "^2.0.1",
|
|
26
26
|
"morgan": "^1.12.1",
|
|
27
27
|
"multer": "^2.4.0",
|
|
28
|
-
"npm": "^12.
|
|
28
|
+
"npm": "^12.2.0",
|
|
29
29
|
"pino-http": "^11.0.0",
|
|
30
30
|
"pino-pretty": "^13.1.3",
|
|
31
|
-
"pnpm": "^12.
|
|
31
|
+
"pnpm": "^12.9.1",
|
|
32
32
|
"pug": "^3.0.4",
|
|
33
33
|
"twig": "^3.0.0",
|
|
34
34
|
"typescript": "^7.0.2",
|