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.
Files changed (44) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +24 -0
  3. package/README.md +282 -2
  4. package/bin/express-cli.js +575 -0
  5. package/lib/app.js +607 -0
  6. package/lib/prompts.js +338 -0
  7. package/lib/wizard.js +398 -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
@@ -0,0 +1,21 @@
1
+ <% if (esm) { -%>
2
+ import { csrfSync } from 'csrf-sync';
3
+ <% } else { -%>
4
+ const { csrfSync } = require('csrf-sync');
5
+ <% } -%>
6
+
7
+ // CSRF protection for requests that change data, which must send the token
8
+ // in a _csrf form field or an x-csrf-token header (GET is not checked)
9
+ const csrf = csrfSync({
10
+ getTokenFromRequest: (req) => req.body?._csrf ?? req.get('x-csrf-token')<%- uploads ? ',' : '' %>
11
+ <% if (uploads) { -%>
12
+ // uploads check the token themselves, as multer reads their form
13
+ skipCsrfProtection: (req) => req.path === '/uploads'
14
+ <% } -%>
15
+ });
16
+
17
+ <% if (esm) { -%>
18
+ export default csrf;
19
+ <% } else { -%>
20
+ module.exports = csrf;
21
+ <% } -%>
@@ -0,0 +1,15 @@
1
+ # Dependencies are installed in the image
2
+ node_modules
3
+
4
+ # Environment variables are set by the container platform
5
+ .env
6
+ .env.*
7
+
8
+ # Not needed to run the app
9
+ .git
10
+ .gitignore
11
+ coverage
12
+ test
13
+ Dockerfile
14
+ .dockerignore
15
+ *.log
@@ -0,0 +1,40 @@
1
+ # Environment variables for local development.
2
+ #
3
+ # Copy this file to .env, which `<%- start %>` and `<%- dev %>` load automatically.
4
+ # Variables already set in the environment take precedence over .env.
5
+ # Do not commit .env.
6
+
7
+ PORT=3000
8
+
9
+ # Set to production when you deploy
10
+ # NODE_ENV=production
11
+ <% if (cors) { -%>
12
+
13
+ # Comma-separated origins allowed to call the app from a browser
14
+ # (defaults to any origin)
15
+ # CORS_ORIGIN=https://example.com,https://admin.example.com
16
+ <% } -%>
17
+ <% if (trustProxy) { -%>
18
+
19
+ # Behind a load balancer or other proxy, the number of proxies (or their
20
+ # addresses), so the app sees each client's IP address and protocol
21
+ # TRUST_PROXY=1
22
+ <% } -%>
23
+ <% if (rateLimit) { -%>
24
+
25
+ # Requests each client may make in each window of milliseconds
26
+ # (defaults to 100 every 15 minutes)
27
+ # RATE_LIMIT_MAX=100
28
+ # RATE_LIMIT_WINDOW_MS=900000
29
+ <% } -%>
30
+ <% if (session) { -%>
31
+
32
+ # Long random secret that signs the session cookie, required in production,
33
+ # such as the output of: node -e "console.log(crypto.randomBytes(32).toString('hex'))"
34
+ # SESSION_SECRET=
35
+ <% } -%>
36
+ <% if (uploads) { -%>
37
+
38
+ # Largest file that can be uploaded, in bytes (defaults to 5 MB)
39
+ # UPLOAD_MAX_SIZE=5242880
40
+ <% } -%>
@@ -0,0 +1,28 @@
1
+ import js from '@eslint/js';
2
+ import { defineConfig } from 'eslint/config';
3
+ import globals from 'globals';
4
+ <% if (ts) { -%>
5
+ import tseslint from 'typescript-eslint';
6
+ <% } -%>
7
+
8
+ export default defineConfig([
9
+ js.configs.recommended,
10
+ <% if (ts) { -%>
11
+ tseslint.configs.recommended,
12
+ <% } -%>
13
+ <% if (!esm) { -%>
14
+ {
15
+ files: ['**/*.js'],
16
+ languageOptions: { sourceType: 'commonjs' }
17
+ },
18
+ <% } -%>
19
+ {
20
+ languageOptions: {
21
+ globals: globals.node
22
+ },
23
+ rules: {
24
+ // Express error handlers need all four arguments, even when next is unused
25
+ '<%- ts ? '@typescript-eslint/' : '' %>no-unused-vars': ['error', { argsIgnorePattern: '^next$' }]
26
+ }
27
+ }
28
+ ]);
@@ -0,0 +1,22 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Environment variables, except the example
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ # Logs
10
+ logs/
11
+ *.log
12
+ npm-debug.log*
13
+
14
+ # Test coverage
15
+ coverage/
16
+
17
+ # TypeScript
18
+ *.tsbuildinfo
19
+
20
+ # Operating system files
21
+ .DS_Store
22
+ Thumbs.db
@@ -0,0 +1,8 @@
1
+ # npm settings that protect installs from compromised packages.
2
+ # npm 12 and newer, which this app requires in package.json, also refuses
3
+ # dependencies from git and skips the install scripts of dependencies unless
4
+ # they are approved with `npm approve-scripts`.
5
+
6
+ # Wait 3 days before installing a newly published version, so a compromised
7
+ # release can be found and removed from the registry first.
8
+ min-release-age=3
@@ -0,0 +1,24 @@
1
+ # pnpm settings that protect installs from compromised packages.
2
+ # https://pnpm.io/supply-chain-security
3
+
4
+ # Wait 3 days (in minutes) before installing a newly published version, so a
5
+ # compromised release can be found and removed from the registry first.
6
+ minimumReleaseAge: 4320
7
+
8
+ # Fail when a dependency wants to run an install script, instead of skipping
9
+ # it. None of this app's dependencies need one; approve a package you trust
10
+ # with `pnpm approve-builds`.
11
+ strictDepBuilds: true
12
+
13
+ # Dependencies of dependencies must come from the registry, not git or a URL.
14
+ blockExoticSubdeps: true
15
+
16
+ # Fail when a version was published with weaker proof of where it came from
17
+ # than an earlier version of the package, a sign of a stolen account. Versions
18
+ # over a year old (in minutes) are not checked.
19
+ trustPolicy: no-downgrade
20
+ trustPolicyIgnoreAfter: 525600
21
+
22
+ # Run the pnpm version recorded in pnpm-lock.yaml, downloading it when another
23
+ # version is installed, so everyone installs with the same pnpm.
24
+ pmOnFail: download
@@ -0,0 +1,22 @@
1
+ <% if (esm) { -%>
2
+ import express from 'express';
3
+ <% } else { -%>
4
+ const express = require('express');
5
+ <% } -%>
6
+
7
+ const router = express.Router();
8
+
9
+ /* GET home page. */
10
+ router.get('/', (req, res) => {
11
+ <% if (api) { -%>
12
+ res.json({ message: 'Welcome to Express' });
13
+ <% } else { -%>
14
+ res.render('index', { title: 'Express' });
15
+ <% } -%>
16
+ });
17
+
18
+ <% if (esm) { -%>
19
+ export default router;
20
+ <% } else { -%>
21
+ module.exports = router;
22
+ <% } -%>
@@ -0,0 +1,94 @@
1
+ <% if (esm) { -%>
2
+ import express from 'express';
3
+ <% if (ts) { -%>
4
+ import type { NextFunction, Request, Response } from 'express';
5
+ <% } -%>
6
+ import createError from 'http-errors';
7
+ import multer from 'multer';
8
+ import path from 'node:path';
9
+ <% if (csrf) { -%>
10
+ import csrf from '../csrf.<%- ext %>';
11
+ <% } -%>
12
+ <% } else { -%>
13
+ const express = require('express');
14
+ const createError = require('http-errors');
15
+ const multer = require('multer');
16
+ const path = require('node:path');
17
+ <% if (csrf) { -%>
18
+ const csrf = require('../csrf.js');
19
+ <% } -%>
20
+ <% } -%>
21
+
22
+ const router = express.Router();
23
+
24
+ // file types to accept, as sent by the client, so check a file's contents
25
+ // too before trusting it
26
+ const TYPES = ['application/pdf', 'image/gif', 'image/jpeg', 'image/png', 'image/webp', 'text/plain'];
27
+
28
+ // files are saved in uploads/ under random names, never the name the client
29
+ // sends, and are not served as static files, where they could run scripts
30
+ const upload = multer({
31
+ dest: path.join(<%- dirname %>, '..', 'uploads'),
32
+ limits: {
33
+ // bytes per file, 5 MB unless set
34
+ fileSize: Number(process.env.UPLOAD_MAX_SIZE) || 5 * 1024 * 1024,
35
+ files: 1
36
+ },
37
+ fileFilter: (req, file, cb) => {
38
+ <% if (csrf) { -%>
39
+ // check the CSRF token, from the _csrf field before the file, so
40
+ // nothing is saved without it
41
+ if (!csrf.isRequestValid(req)) return cb(csrf.invalidCsrfTokenError);
42
+ <% } -%>
43
+ if (!TYPES.includes(file.mimetype)) return cb(createError(415, 'Unsupported file type'));
44
+ cb(null, true);
45
+ }
46
+ });
47
+
48
+ <% if (view) { -%>
49
+ /* GET the upload form. */
50
+ router.get('/', (req, res) => {
51
+ res.render('uploads', { title: 'Upload a file', uploaded: req.query.uploaded });
52
+ });
53
+
54
+ <% } -%>
55
+ /* POST a file in the `file` field. */
56
+ router.post('/', upload.single('file'), (req, res, next) => {
57
+ <% if (csrf) { -%>
58
+ // without a file, the CSRF token is not checked yet
59
+ if (!csrf.isRequestValid(req)) return next(csrf.invalidCsrfTokenError);
60
+ <% } -%>
61
+ if (!req.file) return next(createError(400, 'Choose a file to upload'));
62
+
63
+ <% if (view) { -%>
64
+ // redirect, so refreshing the page does not upload again
65
+ res.redirect(303, `/uploads?uploaded=${encodeURIComponent(req.file.originalname)}`);
66
+ <% } else { -%>
67
+ res.status(201).json({
68
+ // the file's name in uploads/
69
+ id: req.file.filename,
70
+ name: req.file.originalname,
71
+ type: req.file.mimetype,
72
+ size: req.file.size
73
+ });
74
+ <% } -%>
75
+ });
76
+
77
+ // multer's errors, such as a file over the size limit, are client errors
78
+ <% if (ts) { -%>
79
+ router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
80
+ <% } else { -%>
81
+ router.use((err, req, res, next) => {
82
+ <% } -%>
83
+ if (err instanceof multer.MulterError) {
84
+ next(createError(err.code === 'LIMIT_FILE_SIZE' ? 413 : 400, err.message));
85
+ } else {
86
+ next(err);
87
+ }
88
+ });
89
+
90
+ <% if (esm) { -%>
91
+ export default router;
92
+ <% } else { -%>
93
+ module.exports = router;
94
+ <% } -%>
@@ -0,0 +1,42 @@
1
+ <% if (esm) { -%>
2
+ import express from 'express';
3
+ <% } else { -%>
4
+ const express = require('express');
5
+ <% } -%>
6
+
7
+ const router = express.Router();
8
+
9
+ /* GET users listing. */
10
+ router.get('/', (req, res) => {
11
+ <% if (api) { -%>
12
+ res.json([]);
13
+ <% } else { -%>
14
+ res.send('respond with a resource');
15
+ <% } -%>
16
+ });
17
+
18
+ <% if (form) { -%>
19
+ /* GET the new user form. */
20
+ router.get('/new', (req, res) => {
21
+ res.render('new-user', { title: 'New user', added: req.query.added });
22
+ });
23
+
24
+ /* POST the new user form, after the app checks its CSRF token. */
25
+ router.post('/new', (req, res) => {
26
+ const name = String(req.body?.name ?? '').trim();
27
+
28
+ if (!name) {
29
+ res.status(400).render('new-user', { title: 'New user', error: 'Enter a name' });
30
+ return;
31
+ }
32
+
33
+ // save the user here, then redirect, so refreshing the page does not post again
34
+ res.redirect(303, `/users/new?added=${encodeURIComponent(name)}`);
35
+ });
36
+
37
+ <% } -%>
38
+ <% if (esm) { -%>
39
+ export default router;
40
+ <% } else { -%>
41
+ module.exports = router;
42
+ <% } -%>
@@ -0,0 +1,199 @@
1
+ <% if (esm) { -%>
2
+ import assert from 'node:assert';
3
+ import { once } from 'node:events';
4
+ <% if (uploads) { -%>
5
+ import fs from 'node:fs';
6
+ <% } -%>
7
+ <% if (ts) { -%>
8
+ import type { Server } from 'node:http';
9
+ import type { AddressInfo } from 'node:net';
10
+ <% } -%>
11
+ import { after, before, describe, it } from 'node:test';
12
+ <% if (uploads) { -%>
13
+ import path from 'node:path';
14
+ <% } -%>
15
+ import app from '../app.<%- ext %>';
16
+ <% } else { -%>
17
+ const assert = require('node:assert');
18
+ const { once } = require('node:events');
19
+ <% if (uploads) { -%>
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+ <% } -%>
23
+ const { after, before, describe, it } = require('node:test');
24
+ const app = require('../app.js');
25
+ <% } -%>
26
+
27
+ // skip request logs while testing (see app.<%- ext %>)
28
+ process.env.NODE_ENV = 'test';
29
+
30
+ describe('app', () => {
31
+ <% if (ts) { -%>
32
+ let server: Server;
33
+ let baseUrl: string;
34
+ <% } else { -%>
35
+ let server;
36
+ let baseUrl;
37
+ <% } -%>
38
+
39
+ before(async () => {
40
+ // listen on a random free port
41
+ server = app.listen(0);
42
+ await once(server, 'listening');
43
+ <% if (ts) { -%>
44
+ baseUrl = `http://localhost:${(server.address() as AddressInfo).port}`;
45
+ <% } else { -%>
46
+ baseUrl = `http://localhost:${server.address().port}`;
47
+ <% } -%>
48
+ });
49
+
50
+ after(() => {
51
+ server.close();
52
+ });
53
+ <% if (csrf) { -%>
54
+
55
+ // a session cookie and its CSRF token, from a page with a form
56
+ <% if (ts) { -%>
57
+ async function csrfSession(url: string) {
58
+ <% } else { -%>
59
+ async function csrfSession(url) {
60
+ <% } -%>
61
+ const res = await fetch(url);
62
+ const cookie = res.headers.getSetCookie().map((header) => header.split(';')[0]).join('; ');
63
+ const token = /<meta name="csrf-token" content="([^"]+)">/.exec(await res.text())?.[1] ?? '';
64
+ return { cookie, token };
65
+ }
66
+ <% } -%>
67
+
68
+ it('responds to GET /health', async () => {
69
+ const res = await fetch(`${baseUrl}/health`);
70
+ assert.strictEqual(res.status, 200);
71
+ assert.deepStrictEqual(await res.json(), { status: 'ok' });
72
+ });
73
+
74
+ <% if (rateLimit) { -%>
75
+ it('sends rate limit headers', async () => {
76
+ const res = await fetch(`${baseUrl}/`);
77
+ assert.ok(res.headers.has('ratelimit-policy'));
78
+ });
79
+
80
+ <% } -%>
81
+ <% if (csrf) { -%>
82
+ it('rejects posts without a CSRF token', async () => {
83
+ const res = await fetch(`${baseUrl}/users/new`, { method: 'POST', body: new URLSearchParams({ name: 'Ada' }) });
84
+ assert.strictEqual(res.status, 403);
85
+ });
86
+
87
+ it('adds a user with the form and its CSRF token', async () => {
88
+ const { cookie, token } = await csrfSession(`${baseUrl}/users/new`);
89
+ const res = await fetch(`${baseUrl}/users/new`, {
90
+ method: 'POST',
91
+ headers: { cookie },
92
+ body: new URLSearchParams({ _csrf: token, name: 'Ada' }),
93
+ redirect: 'manual'
94
+ });
95
+ assert.strictEqual(res.status, 303);
96
+ assert.strictEqual(res.headers.get('location'), '/users/new?added=Ada');
97
+ });
98
+
99
+ it('asks again for a user without a name', async () => {
100
+ const { cookie, token } = await csrfSession(`${baseUrl}/users/new`);
101
+ const res = await fetch(`${baseUrl}/users/new`, {
102
+ method: 'POST',
103
+ headers: { cookie },
104
+ body: new URLSearchParams({ _csrf: token, name: ' ' })
105
+ });
106
+ assert.strictEqual(res.status, 400);
107
+ assert.match(await res.text(), /Enter a name/);
108
+ });
109
+
110
+ <% } -%>
111
+ <% if (uploads) { -%>
112
+ it('uploads a file', async () => {
113
+ const uploads = path.join(<%- esm ? 'import.meta.dirname' : '__dirname' %>, '..', 'uploads');
114
+ const existing = new Set(fs.readdirSync(uploads));
115
+ <% if (csrf) { -%>
116
+ const { cookie, token } = await csrfSession(`${baseUrl}/uploads`);
117
+ <% } -%>
118
+ const body = new FormData();
119
+ <% if (csrf) { -%>
120
+ // the token comes before the file, so it is checked before saving
121
+ body.append('_csrf', token);
122
+ <% } -%>
123
+ body.append('file', new Blob(['hello'], { type: 'text/plain' }), 'hello.txt');
124
+
125
+ <% if (csrf) { -%>
126
+ const res = await fetch(`${baseUrl}/uploads`, { method: 'POST', headers: { cookie }, body, redirect: 'manual' });
127
+ <% } else { -%>
128
+ const res = await fetch(`${baseUrl}/uploads`, { method: 'POST', body, redirect: 'manual' });
129
+ <% } -%>
130
+ <% if (view) { -%>
131
+ assert.strictEqual(res.status, 303);
132
+ assert.strictEqual(res.headers.get('location'), '/uploads?uploaded=hello.txt');
133
+ <% } else { -%>
134
+ assert.strictEqual(res.status, 201);
135
+ assert.strictEqual((await res.json()).name, 'hello.txt');
136
+ <% } -%>
137
+
138
+ // remove the uploaded file
139
+ const added = fs.readdirSync(uploads).filter((file) => !existing.has(file));
140
+ assert.strictEqual(added.length, 1);
141
+ for (const file of added) fs.rmSync(path.join(uploads, file));
142
+ });
143
+
144
+ it('rejects unsupported file types', async () => {
145
+ <% if (csrf) { -%>
146
+ const { cookie, token } = await csrfSession(`${baseUrl}/uploads`);
147
+ <% } -%>
148
+ const body = new FormData();
149
+ <% if (csrf) { -%>
150
+ body.append('_csrf', token);
151
+ <% } -%>
152
+ body.append('file', new Blob(['<html>'], { type: 'text/html' }), 'page.html');
153
+
154
+ <% if (csrf) { -%>
155
+ const res = await fetch(`${baseUrl}/uploads`, { method: 'POST', headers: { cookie }, body });
156
+ <% } else { -%>
157
+ const res = await fetch(`${baseUrl}/uploads`, { method: 'POST', body });
158
+ <% } -%>
159
+ assert.strictEqual(res.status, 415);
160
+ });
161
+
162
+ <% } -%><% if (api) { -%>
163
+ it('responds to GET /', async () => {
164
+ const res = await fetch(`${baseUrl}/`);
165
+ assert.strictEqual(res.status, 200);
166
+ assert.deepStrictEqual(await res.json(), { message: 'Welcome to Express' });
167
+ });
168
+
169
+ it('responds to GET /users', async () => {
170
+ const res = await fetch(`${baseUrl}/users`);
171
+ assert.strictEqual(res.status, 200);
172
+ assert.deepStrictEqual(await res.json(), []);
173
+ });
174
+
175
+ it('responds with a JSON 404 for unknown routes', async () => {
176
+ const res = await fetch(`${baseUrl}/does-not-exist`);
177
+ assert.strictEqual(res.status, 404);
178
+ assert.match(res.headers.get('content-type') ?? '', /application\/json/);
179
+ assert.strictEqual((await res.json()).error, 'Not Found');
180
+ });
181
+ });
182
+ <% } else { -%>
183
+ it('responds to GET /', async () => {
184
+ const res = await fetch(`${baseUrl}/`);
185
+ assert.strictEqual(res.status, 200);
186
+ });
187
+
188
+ it('responds to GET /users', async () => {
189
+ const res = await fetch(`${baseUrl}/users`);
190
+ assert.strictEqual(res.status, 200);
191
+ assert.strictEqual(await res.text(), 'respond with a resource');
192
+ });
193
+
194
+ it('responds with 404 for unknown routes', async () => {
195
+ const res = await fetch(`${baseUrl}/does-not-exist`);
196
+ assert.strictEqual(res.status, 404);
197
+ });
198
+ });
199
+ <% } -%>
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ <% if (esm) { -%>
4
+ import app from '../app.<%- ext %>';
5
+ <% } else { -%>
6
+ const app = require('../app.js');
7
+ <% } -%>
8
+
9
+ const port = process.env.PORT || 3000;
10
+
11
+ const server = app.listen(port, (err) => {
12
+ if (err) {
13
+ // handle specific listen errors with friendly messages
14
+ <% if (ts) { -%>
15
+ switch ((err as NodeJS.ErrnoException).code) {
16
+ <% } else { -%>
17
+ switch (err.code) {
18
+ <% } -%>
19
+ case 'EACCES':
20
+ console.error(`Port ${port} requires elevated privileges`);
21
+ process.exit(1);
22
+ break;
23
+ case 'EADDRINUSE':
24
+ console.error(`Port ${port} is already in use`);
25
+ process.exit(1);
26
+ break;
27
+ default:
28
+ throw err;
29
+ }
30
+ }
31
+
32
+ console.log(`Listening on port ${port}`);
33
+ });
34
+
35
+ // close the server gracefully when asked to stop
36
+ <% if (ts) { -%>
37
+ function shutdown(signal: NodeJS.Signals) {
38
+ <% } else { -%>
39
+ function shutdown(signal) {
40
+ <% } -%>
41
+ console.log(`${signal} received, shutting down`);
42
+ server.close(() => process.exit(0));
43
+ }
44
+
45
+ process.on('SIGINT', shutdown);
46
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,13 @@
1
+ <html>
2
+
3
+ <head>
4
+ <title>Express</title>
5
+ <link rel="stylesheet" href="/stylesheets/style.css">
6
+ </head>
7
+
8
+ <body>
9
+ <h1>Express</h1>
10
+ <p>Welcome to Express</p>
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1,8 @@
1
+ body {
2
+ padding: 50px;
3
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4
+ }
5
+
6
+ a {
7
+ color: #00B7FF;
8
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "versions": {
3
+ "@eslint/js": "^10.0.1",
4
+ "@types/compression": "^1.8.1",
5
+ "@types/cookie-parser": "^1.4.10",
6
+ "@types/cors": "^2.8.19",
7
+ "@types/express": "^5.0.6",
8
+ "@types/express-session": "^1.19.0",
9
+ "@types/http-errors": "^2.0.5",
10
+ "@types/morgan": "^1.9.10",
11
+ "@types/multer": "^2.3.0",
12
+ "@types/node": "^22.20.5",
13
+ "compression": "^1.8.2",
14
+ "cookie-parser": "^1.4.7",
15
+ "cors": "^2.8.6",
16
+ "csrf-sync": "^4.2.1",
17
+ "ejs": "^6.0.1",
18
+ "eslint": "^10.12.0",
19
+ "express": "^5.2.1",
20
+ "express-rate-limit": "^8.7.0",
21
+ "express-session": "^1.19.0",
22
+ "globals": "^17.13.0",
23
+ "hbs": "^4.3.1",
24
+ "helmet": "^8.3.0",
25
+ "http-errors": "^2.0.1",
26
+ "morgan": "^1.12.1",
27
+ "multer": "^2.4.0",
28
+ "npm": "^12.2.0",
29
+ "pino-http": "^11.0.0",
30
+ "pino-pretty": "^13.1.3",
31
+ "pnpm": "^12.9.1",
32
+ "pug": "^3.0.4",
33
+ "twig": "^3.0.0",
34
+ "typescript": "^7.0.2",
35
+ "typescript-eslint": "^8.71.0",
36
+ "typescript@6": "^6.0.3"
37
+ },
38
+ "hold": {
39
+ "@types/node": "kept on the major of the minimum supported Node.js version (22)",
40
+ "typescript@6": {
41
+ "reason": "used with --ts --lint until typescript-eslint supports TypeScript 7",
42
+ "until": {
43
+ "package": "typescript-eslint",
44
+ "peer": "typescript",
45
+ "supports": "7.0.0"
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= title %></title>
5
+ <link rel='stylesheet' href='/stylesheets/style.css' />
6
+ </head>
7
+ <body>
8
+ <h1><%= message %></h1>
9
+ <h2><%= status %></h2>
10
+ <pre><%= error.stack %></pre>
11
+ </body>
12
+ </html>
@@ -0,0 +1,3 @@
1
+ <h1>{{message}}</h1>
2
+ <h2>{{status}}</h2>
3
+ <pre>{{error.stack}}</pre>
@@ -0,0 +1,6 @@
1
+ extends layout
2
+
3
+ block content
4
+ h1= message
5
+ h2= status
6
+ pre #{error.stack}
@@ -0,0 +1,7 @@
1
+ {% extends 'layout.twig' %}
2
+
3
+ {% block body %}
4
+ <h1>{{message|e}}</h1>
5
+ <h2>{{status}}</h2>
6
+ <pre>{{error.stack|e}}</pre>
7
+ {% endblock %}