express-generator-typescript 3.1.0 โ 3.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/README.md +10 -11
- package/lib/template/.prettierrc.json +1 -1
- package/lib/template/README.md +11 -14
- package/lib/template/eslint.config.ts +10 -1
- package/lib/template/gitignore +0 -1
- package/lib/template/package.json +16 -26
- package/lib/template/src/common/classes/route-errors.ts +1 -1
- package/lib/template/src/common/constants/HttpStatusCodes.ts +1 -1
- package/lib/template/src/common/constants/Paths/Paths.ts +1 -1
- package/lib/template/src/common/constants/Paths/index.ts +1 -1
- package/lib/template/src/common/utils/dev-only.ts +1 -1
- package/lib/template/src/entities/User/index.ts +2 -2
- package/lib/template/src/entities/User/module.ts +2 -2
- package/lib/template/src/entities/User/types.ts +1 -1
- package/lib/template/src/entities/common/entity-base-types.ts +1 -1
- package/lib/template/src/main.ts +2 -2
- package/lib/template/src/repos/MockOrm.ts +5 -5
- package/lib/template/src/repos/UserRepo.ts +2 -2
- package/lib/template/src/routers/api.router.ts +2 -2
- package/lib/template/src/routers/user.router.ts +4 -4
- package/lib/template/src/server.ts +6 -6
- package/lib/template/src/services/UserService.ts +6 -3
- package/lib/template/tests/frontend.test.ts +28 -17
- package/lib/template/tests/support/agent.ts +4 -4
- package/lib/template/tests/users.test.ts +56 -52
- package/lib/template/tsconfig.json +10 -13
- package/package.json +4 -5
- package/lib/template/tsconfig.prod.json +0 -30
- package/lib/template/vitest.config.ts +0 -25
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A command-line tool that generates production-ready Express projects with TypeSc
|
|
|
14
14
|
|
|
15
15
|
## ๐งญ Overview
|
|
16
16
|
|
|
17
|
-
`express-generator-typescript` works like the classic `express-generator` package, but the project it creates is fully set up for TypeScript. You get strict typing, linting, hot reloading,
|
|
17
|
+
`express-generator-typescript` works like the classic `express-generator` package, but the project it creates is fully set up for TypeScript. You get strict typing, linting, hot reloading, and testing, with defaults aimed at APIs and no build step. The project is an ES module and comes with a `#src/*` import alias, so imports stay clean as the app grows.
|
|
18
18
|
|
|
19
19
|
<p align="center">ยท ยท ยท</p>
|
|
20
20
|
|
|
@@ -22,15 +22,15 @@ A command-line tool that generates production-ready Express projects with TypeSc
|
|
|
22
22
|
|
|
23
23
|
- **TypeScript-first** โ strict compiler settings, linting, and sensible tsconfig defaults, ready to go.
|
|
24
24
|
- **Built for APIs** โ ideal for SPAs, mobile backends, or services.
|
|
25
|
-
- **Fast development** โ runs TypeScript directly with
|
|
26
|
-
- **Path aliases** โ import from
|
|
25
|
+
- **Fast development** โ runs TypeScript directly with Node's built-in type stripping (no build step, in development or production), restarts the server when you change it, and refreshes the browser when you change front-end files. Node's built-in test runner and ESLint are included.
|
|
26
|
+
- **Path aliases** โ import from `#src/*` anywhere. It works in development, tests, and production.
|
|
27
27
|
- **Lean dependencies** โ no view engine, ORM, or UI layer; only the essentials for Express + TypeScript.
|
|
28
28
|
|
|
29
29
|
<p align="center">ยท ยท ยท</p>
|
|
30
30
|
|
|
31
31
|
## ๐ฆ Installation
|
|
32
32
|
|
|
33
|
-
Requires Node.js 22.
|
|
33
|
+
Requires Node.js 22.18+ (22.x), 24.x, or 26+.
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npx express-generator-typescript
|
|
@@ -70,20 +70,19 @@ npm run dev
|
|
|
70
70
|
|
|
71
71
|
## ๐งฉ Generated Template
|
|
72
72
|
|
|
73
|
-
The generated project is a small CRUD app for a `User` record. It shows how to structure models, services, and routes in Express + TypeScript. Linting, formatting,
|
|
73
|
+
The generated project is a small CRUD app for a `User` record. It shows how to structure models, services, and routes in Express + TypeScript. Linting, formatting, testing, and hot reloading are all set up for you.
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
77
|
#### Available `package.json` scripts
|
|
78
78
|
|
|
79
79
|
- `npm run dev` โ Run the server in development with live reload and browser refresh.
|
|
80
|
-
- `npm test` โ Run the tests with
|
|
81
|
-
- `npm test -- users.test.ts` โ Run a single test file.
|
|
80
|
+
- `npm test` โ Run the tests with Node's built-in test runner.
|
|
81
|
+
- `npm test -- tests/users.test.ts` โ Run a single test file.
|
|
82
82
|
- `npm run lint` โ Check the code with ESLint.
|
|
83
83
|
- `npm run format` โ Format the code with Prettier.
|
|
84
|
-
- `npm
|
|
85
|
-
- `npm
|
|
86
|
-
- `npm run typecheck` โ Check for TypeScript errors without building.
|
|
84
|
+
- `npm start` โ Run the server in production mode (no build step).
|
|
85
|
+
- `npm run typecheck` โ Check for TypeScript errors.
|
|
87
86
|
- `npm run install:clean` โ Delete `node_modules` and the lockfile, then reinstall.
|
|
88
87
|
|
|
89
88
|
---
|
|
@@ -187,7 +186,7 @@ To debug with breakpoints in VS Code, start the app or tests from `.vscode/launc
|
|
|
187
186
|
"console": "integratedTerminal"
|
|
188
187
|
},
|
|
189
188
|
{
|
|
190
|
-
"name": "Test
|
|
189
|
+
"name": "Test",
|
|
191
190
|
"type": "node",
|
|
192
191
|
"request": "launch",
|
|
193
192
|
"runtimeExecutable": "npm",
|
package/lib/template/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## About
|
|
2
2
|
|
|
3
|
-
This project was created with [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript). It requires Node.js 22.
|
|
3
|
+
This project was created with [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript). It requires Node.js 22.18+ (22.x), 24.x, or 26+.
|
|
4
4
|
|
|
5
5
|
The original template follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices).
|
|
6
6
|
|
|
@@ -12,11 +12,11 @@ The original template follows the [TypeScript best practices](https://github.com
|
|
|
12
12
|
|
|
13
13
|
Run the server in development at http://localhost:3000. The server restarts when you change server code, and the browser refreshes when you change server code or anything in `src/public` or `src/views`.
|
|
14
14
|
|
|
15
|
-
> **Note:**
|
|
15
|
+
> **Note:** Node runs your `.ts` files directly with Node's built-in [type stripping](https://nodejs.org/api/typescript.html), which doesn't check for TypeScript errors. Run `npm run typecheck` to check for them, and let your editor catch most of them as you work.
|
|
16
16
|
|
|
17
17
|
### `npm test`
|
|
18
18
|
|
|
19
|
-
Run the tests with [
|
|
19
|
+
Run the tests with Node's built-in [test runner](https://nodejs.org/api/test.html). Pass a file to run only that file (`npm test -- tests/users.test.ts`), or `--test-name-pattern` to run only matching tests (`npm test -- --test-name-pattern="UserRouter"`).
|
|
20
20
|
|
|
21
21
|
### `npm run lint`
|
|
22
22
|
|
|
@@ -26,17 +26,13 @@ Check the code with ESLint.
|
|
|
26
26
|
|
|
27
27
|
Format `src/` and `tests/` with Prettier.
|
|
28
28
|
|
|
29
|
-
### `npm run build`
|
|
30
|
-
|
|
31
|
-
Build the project for production.
|
|
32
|
-
|
|
33
29
|
### `npm start`
|
|
34
30
|
|
|
35
|
-
Run the production
|
|
31
|
+
Run the server in production mode. There's no build step: like `npm run dev`, this runs the `.ts` files in `src/` directly, so run `npm run typecheck` and `npm run lint` before you deploy.
|
|
36
32
|
|
|
37
33
|
### `npm run typecheck`
|
|
38
34
|
|
|
39
|
-
Check for TypeScript errors
|
|
35
|
+
Check for TypeScript errors.
|
|
40
36
|
|
|
41
37
|
### `npm run install:clean`
|
|
42
38
|
|
|
@@ -54,16 +50,17 @@ Delete `node_modules/` and `package-lock.json`, then reinstall all dependencies.
|
|
|
54
50
|
- **General logging**: [jet-logger](https://github.com/seanpmaxwell/jet-logger)
|
|
55
51
|
- **Validation**: [zod](https://github.com/colinhacks/zod)
|
|
56
52
|
- **Middleware Validation**: [express-zod-safe](https://github.com/AngaBlue/express-zod-safe)
|
|
57
|
-
- **Environment variables**: [
|
|
58
|
-
- **Reloading**: [
|
|
59
|
-
- **Testing**: [
|
|
53
|
+
- **Environment variables**: Node's built-in [`--env-file`](https://nodejs.org/api/cli.html#--env-fileconfig) loads `config/.env.*`
|
|
54
|
+
- **Reloading**: Node's built-in [`--watch`](https://nodejs.org/api/cli.html#--watch) (restarts the server) and [livereload](https://github.com/napcs/node-livereload) + [connect-livereload](https://github.com/intesso/connect-livereload) (refreshes the browser)
|
|
55
|
+
- **Testing**: Node's built-in [`node:test`](https://nodejs.org/api/test.html) + [Supertest](https://github.com/ladjs/supertest)
|
|
60
56
|
- **Linting**: [ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) and [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n)
|
|
61
57
|
- **Formatting**: [Prettier](https://prettier.io) with [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports)
|
|
62
|
-
- **
|
|
58
|
+
- **Type checking**: `tsc` (`npm run typecheck`). There's no build step.
|
|
63
59
|
|
|
64
60
|
<p align="center">ยท ยท ยท</p>
|
|
65
61
|
|
|
66
62
|
## Additional Notes
|
|
67
63
|
|
|
64
|
+
- Imports name the `.ts` file (`./server.ts`), because Node runs the source files directly. `#src/*` imports are [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) set up in `package.json` that point at `src/*`.
|
|
68
65
|
- `config/.env.production` is in `.gitignore` so production secrets don't get committed. Keep it out of version control and supply its values through your deployment tooling.
|
|
69
|
-
- The database is a JSON file meant only for the demo: `src/repos/common/database.json
|
|
66
|
+
- The database is a JSON file meant only for the demo: `src/repos/common/database.json`. It's created automatically if missing, but it isn't safe for simultaneous writes. Replace `src/repos/MockOrm.ts` with a real database before going to production.
|
|
@@ -10,7 +10,7 @@ import tseslint from 'typescript-eslint';
|
|
|
10
10
|
|
|
11
11
|
export default defineConfig([
|
|
12
12
|
// global ignores
|
|
13
|
-
globalIgnores(['**/
|
|
13
|
+
globalIgnores(['**/public/**/lib/**']),
|
|
14
14
|
// linting rules (code quality only)
|
|
15
15
|
{
|
|
16
16
|
files: ['**/*.{ts,tsx}'],
|
|
@@ -38,6 +38,15 @@ export default defineConfig([
|
|
|
38
38
|
ignoreRestSiblings: true,
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
|
+
// node:test's `describe`/`it` return promises the runner already tracks
|
|
42
|
+
'@typescript-eslint/no-floating-promises': [
|
|
43
|
+
'error',
|
|
44
|
+
{
|
|
45
|
+
allowForKnownSafeCalls: [
|
|
46
|
+
{ from: 'package', name: ['describe', 'it'], package: 'node:test' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
41
50
|
'no-console': 'warn',
|
|
42
51
|
'no-extra-boolean-cast': 'off',
|
|
43
52
|
'no-process-env': 'warn',
|
package/lib/template/gitignore
CHANGED
|
@@ -3,29 +3,27 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"install:clean": "shx rm -rf ./node_modules package-lock.json && npm i",
|
|
9
|
-
"dev": "cross-env DOTENV_CONFIG_PATH=./config/.env.development tsx watch --import dotenv/config ./src/main.ts",
|
|
6
|
+
"install:clean": "node -e \"fs.rmSync('node_modules', { recursive: true, force: true }); fs.rmSync('package-lock.json', { force: true })\" && npm i",
|
|
7
|
+
"dev": "node --env-file=config/.env.development --watch src/main.ts",
|
|
10
8
|
"lint": "eslint .",
|
|
11
9
|
"format": "prettier --write .",
|
|
12
|
-
"start": "
|
|
13
|
-
"test": "
|
|
10
|
+
"start": "node --env-file-if-exists=config/.env.production src/main.ts",
|
|
11
|
+
"test": "node --env-file=config/.env.test --test --test-concurrency=1",
|
|
14
12
|
"typecheck": "tsc -b --noEmit"
|
|
15
13
|
},
|
|
14
|
+
"imports": {
|
|
15
|
+
"#src/*": "./src/*"
|
|
16
|
+
},
|
|
16
17
|
"engines": {
|
|
17
|
-
"node": "^22.
|
|
18
|
+
"node": "^22.18.0 || ^24.0.0 || >=26.0.0"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"
|
|
21
|
-
"dotenv": "^18.0.4",
|
|
22
|
-
"express": "^5.2.1",
|
|
21
|
+
"express": "^5.3.0",
|
|
23
22
|
"express-zod-safe": "^3.2.1",
|
|
24
23
|
"helmet": "^8.3.0",
|
|
25
24
|
"jet-logger": "3.0.0",
|
|
26
|
-
"jsonfile": "^6.2.1",
|
|
27
25
|
"morgan": "^1.12.1",
|
|
28
|
-
"uuid": "^14.0.
|
|
26
|
+
"uuid": "^14.0.3",
|
|
29
27
|
"zod": "^4.6.5"
|
|
30
28
|
},
|
|
31
29
|
"devDependencies": {
|
|
@@ -33,27 +31,19 @@
|
|
|
33
31
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
34
32
|
"@types/connect-livereload": "^0.6.3",
|
|
35
33
|
"@types/express": "^5.0.6",
|
|
36
|
-
"@types/jsonfile": "^6.1.4",
|
|
37
34
|
"@types/livereload": "^0.9.5",
|
|
38
35
|
"@types/morgan": "^1.9.10",
|
|
39
|
-
"@types/node": "^26.6.
|
|
36
|
+
"@types/node": "^26.6.5",
|
|
40
37
|
"@types/supertest": "^7.2.1",
|
|
41
38
|
"connect-livereload": "^0.6.1",
|
|
42
|
-
"eslint": "^10.
|
|
39
|
+
"eslint": "^10.12.0",
|
|
43
40
|
"eslint-config-prettier": "^10.1.8",
|
|
44
|
-
"eslint-plugin-n": "^18.
|
|
41
|
+
"eslint-plugin-n": "^18.4.1",
|
|
45
42
|
"jiti": "^2.7.0",
|
|
46
43
|
"livereload": "^0.10.3",
|
|
47
|
-
"prettier": "^3.9.
|
|
48
|
-
"
|
|
49
|
-
"supertest": "^7.3.0",
|
|
50
|
-
"tsc-alias": "^1.9.5",
|
|
51
|
-
"tsx": "^4.23.15",
|
|
44
|
+
"prettier": "^3.9.10",
|
|
45
|
+
"supertest": "^7.3.1",
|
|
52
46
|
"typescript": "^6.0.3",
|
|
53
|
-
"typescript-eslint": "^8.
|
|
54
|
-
"vitest": "^5.0.2"
|
|
55
|
-
},
|
|
56
|
-
"allowScripts": {
|
|
57
|
-
"esbuild@0.28.2": true
|
|
47
|
+
"typescript-eslint": "^8.71.1"
|
|
58
48
|
}
|
|
59
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as default } from './Paths';
|
|
1
|
+
export { default as default } from './Paths.ts';
|
|
@@ -8,7 +8,7 @@ import livereload from 'livereload';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Development only: refresh the browser when front-end files change, and
|
|
11
|
-
* after `
|
|
11
|
+
* after `node --watch` restarts the server for a TypeScript change.
|
|
12
12
|
*
|
|
13
13
|
* Must be registered before the routes that serve html so the reload script
|
|
14
14
|
* gets injected into those pages.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as default } from './module';
|
|
2
|
-
export type { UserEntity, UserInput } from './types';
|
|
1
|
+
export { default as default } from './module.ts';
|
|
2
|
+
export type { UserEntity, UserInput } from './types.ts';
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
getISOString,
|
|
6
6
|
type ISOString,
|
|
7
7
|
ISOStringSchema,
|
|
8
|
-
} from '
|
|
8
|
+
} from '#src/common/utils/date.utils.ts';
|
|
9
9
|
|
|
10
|
-
import type { UserEntity, UserInput } from './types';
|
|
10
|
+
import type { UserEntity, UserInput } from './types.ts';
|
|
11
11
|
|
|
12
12
|
// ========================================================================= //
|
|
13
13
|
// CONSTANTS //
|
package/lib/template/src/main.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logger from 'jet-logger';
|
|
2
2
|
|
|
3
|
-
import { Env } from './common/constants/env.constants';
|
|
4
|
-
import server from './server';
|
|
3
|
+
import { Env } from './common/constants/env.constants.ts';
|
|
4
|
+
import server from './server.ts';
|
|
5
5
|
|
|
6
6
|
// ========================================================================= //
|
|
7
7
|
// CONSTANTS //
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import jsonfile from 'jsonfile';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
|
|
5
|
-
import { Env, NodeEnvs } from '
|
|
6
|
-
import type { UserEntity } from '
|
|
4
|
+
import { Env, NodeEnvs } from '#src/common/constants/env.constants.ts';
|
|
5
|
+
import type { UserEntity } from '#src/entities/User/index.ts';
|
|
7
6
|
|
|
8
7
|
// ========================================================================= //
|
|
9
8
|
// CONSTANTS //
|
|
@@ -37,7 +36,8 @@ type Database = {
|
|
|
37
36
|
async function openDb(): Promise<Database> {
|
|
38
37
|
let db: Partial<Database>;
|
|
39
38
|
try {
|
|
40
|
-
|
|
39
|
+
const json = await fs.readFile(DATABASE_FILE_PATH, 'utf8');
|
|
40
|
+
db = JSON.parse(json) as Partial<Database>;
|
|
41
41
|
} catch (err) {
|
|
42
42
|
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
43
43
|
db = {};
|
|
@@ -50,7 +50,7 @@ async function openDb(): Promise<Database> {
|
|
|
50
50
|
*/
|
|
51
51
|
async function saveDb(db: Database): Promise<void> {
|
|
52
52
|
await fs.mkdir(path.dirname(DATABASE_FILE_PATH), { recursive: true });
|
|
53
|
-
|
|
53
|
+
await fs.writeFile(DATABASE_FILE_PATH, JSON.stringify(db, null, 2) + '\n');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type UserEntity } from '
|
|
1
|
+
import { type UserEntity } from '#src/entities/User/index.ts';
|
|
2
2
|
|
|
3
|
-
import orm from './MockOrm';
|
|
3
|
+
import orm from './MockOrm.ts';
|
|
4
4
|
|
|
5
5
|
// ========================================================================= //
|
|
6
6
|
// FUNCTIONS //
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
|
|
3
|
-
import Paths from '
|
|
3
|
+
import Paths from '#src/common/constants/Paths/index.ts';
|
|
4
4
|
|
|
5
|
-
import userRouter from './user.router';
|
|
5
|
+
import userRouter from './user.router.ts';
|
|
6
6
|
|
|
7
7
|
// ========================================================================= //
|
|
8
8
|
// EXEC //
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import validate from 'express-zod-safe';
|
|
3
3
|
|
|
4
|
-
import HttpStatusCodes from '
|
|
5
|
-
import Paths from '
|
|
6
|
-
import User from '
|
|
7
|
-
import UserService from '
|
|
4
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
5
|
+
import Paths from '#src/common/constants/Paths/index.ts';
|
|
6
|
+
import User from '#src/entities/User/index.ts';
|
|
7
|
+
import UserService from '#src/services/UserService.ts';
|
|
8
8
|
|
|
9
9
|
// ========================================================================= //
|
|
10
10
|
// CONSTANTS //
|
|
@@ -8,12 +8,12 @@ import logger from 'jet-logger';
|
|
|
8
8
|
import morgan from 'morgan';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
|
|
11
|
-
import { RouteError } from '
|
|
12
|
-
import HttpStatusCodes from '
|
|
13
|
-
import Paths from '
|
|
14
|
-
import BaseRouter from '
|
|
11
|
+
import { RouteError } from '#src/common/classes/route-errors.ts';
|
|
12
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
13
|
+
import Paths from '#src/common/constants/Paths/index.ts';
|
|
14
|
+
import BaseRouter from '#src/routers/api.router.ts';
|
|
15
15
|
|
|
16
|
-
import { Env, NodeEnvs } from './common/constants/env.constants';
|
|
16
|
+
import { Env, NodeEnvs } from './common/constants/env.constants.ts';
|
|
17
17
|
|
|
18
18
|
// ========================================================================= //
|
|
19
19
|
// EXEC //
|
|
@@ -78,7 +78,7 @@ const staticDir = path.join(import.meta.dirname, 'public');
|
|
|
78
78
|
// Refresh the browser on changes. Imported lazily because livereload is a
|
|
79
79
|
// dev dependency and isn't installed in production.
|
|
80
80
|
if (Env.NODE_ENV === NodeEnvs.DEVELOPMENT) {
|
|
81
|
-
const { setupLiveReload } = await import('
|
|
81
|
+
const { setupLiveReload } = await import('#src/common/utils/dev-only.ts');
|
|
82
82
|
setupLiveReload(app, [staticDir, viewsDir]);
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { NotFoundError } from '
|
|
2
|
-
import User, {
|
|
3
|
-
|
|
1
|
+
import { NotFoundError } from '#src/common/classes/route-errors.ts';
|
|
2
|
+
import User, {
|
|
3
|
+
type UserEntity,
|
|
4
|
+
type UserInput,
|
|
5
|
+
} from '#src/entities/User/index.ts';
|
|
6
|
+
import UserRepo from '#src/repos/UserRepo.ts';
|
|
4
7
|
|
|
5
8
|
// ========================================================================= //
|
|
6
9
|
// CONSTANTS //
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
5
|
+
|
|
6
|
+
import { agent } from './support/agent.ts';
|
|
7
|
+
|
|
8
|
+
// ========================================================================= //
|
|
9
|
+
// CONSTANTS //
|
|
10
|
+
// ========================================================================= //
|
|
11
|
+
|
|
12
|
+
const STATIC_ASSETS = [
|
|
13
|
+
'/scripts/HttpClient.js',
|
|
14
|
+
'/scripts/renderUsers.js',
|
|
15
|
+
'/scripts/users.js',
|
|
16
|
+
'/scripts/lib/bootstrap.bundle.min.js',
|
|
17
|
+
'/stylesheets/users.css',
|
|
18
|
+
'/stylesheets/lib/bootstrap.min.css',
|
|
19
|
+
] as const;
|
|
4
20
|
|
|
5
21
|
// ========================================================================= //
|
|
6
22
|
// TESTS //
|
|
@@ -9,25 +25,20 @@ import { agent } from './support/agent';
|
|
|
9
25
|
describe('Front-end content', () => {
|
|
10
26
|
it('should redirect "/" to the users page.', async () => {
|
|
11
27
|
const res = await agent.get('/');
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
assert.equal(res.status, HttpStatusCodes.FOUND);
|
|
29
|
+
assert.equal(res.headers.location, '/users');
|
|
14
30
|
});
|
|
15
31
|
|
|
16
32
|
it('should serve the users page.', async () => {
|
|
17
33
|
const res = await agent.get('/users');
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
assert.equal(res.status, HttpStatusCodes.OK);
|
|
35
|
+
assert.match(res.headers['content-type'], /html/);
|
|
20
36
|
});
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'/stylesheets/lib/bootstrap.min.css',
|
|
29
|
-
])('should serve the static asset "%s".', async (asset) => {
|
|
30
|
-
const res = await agent.get(asset);
|
|
31
|
-
expect(res.status).toBe(HttpStatusCodes.OK);
|
|
32
|
-
});
|
|
38
|
+
for (const asset of STATIC_ASSETS) {
|
|
39
|
+
it(`should serve the static asset "${asset}".`, async () => {
|
|
40
|
+
const res = await agent.get(asset);
|
|
41
|
+
assert.equal(res.status, HttpStatusCodes.OK);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
33
44
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { before } from 'node:test';
|
|
1
2
|
import supertest, { type Test } from 'supertest';
|
|
2
3
|
import type TestAgent from 'supertest/lib/agent.js';
|
|
3
|
-
import { beforeAll } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import MockOrm from '
|
|
6
|
-
import app from '
|
|
5
|
+
import MockOrm from '#src/repos/MockOrm.ts';
|
|
6
|
+
import app from '#src/server.ts';
|
|
7
7
|
|
|
8
8
|
// ========================================================================= //
|
|
9
9
|
// EXEC //
|
|
@@ -11,7 +11,7 @@ import app from '@src/server';
|
|
|
11
11
|
|
|
12
12
|
let agent: TestAgent<Test>;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
before(async () => {
|
|
15
15
|
agent = supertest.agent(app);
|
|
16
16
|
await MockOrm.cleanDb();
|
|
17
17
|
});
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import User, { type UserEntity } from '@src/entities/User';
|
|
4
|
-
import UserRepo, { UserRepoTestOnly } from '@src/repos/UserRepo';
|
|
5
|
-
import UserService from '@src/services/UserService';
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { beforeEach, describe, it } from 'node:test';
|
|
6
3
|
|
|
7
|
-
import
|
|
4
|
+
import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
|
|
5
|
+
import Paths from '#src/common/constants/Paths/index.ts';
|
|
6
|
+
import User, { type UserEntity } from '#src/entities/User/index.ts';
|
|
7
|
+
import UserRepo, { UserRepoTestOnly } from '#src/repos/UserRepo.ts';
|
|
8
|
+
import UserService from '#src/services/UserService.ts';
|
|
9
|
+
|
|
10
|
+
import { agent } from './support/agent.ts';
|
|
8
11
|
import {
|
|
9
12
|
extractErrMsg,
|
|
10
13
|
type TypedErrRes,
|
|
11
14
|
type TypedRes,
|
|
12
|
-
} from './support/utils';
|
|
15
|
+
} from './support/utils.ts';
|
|
13
16
|
|
|
14
17
|
// ========================================================================= //
|
|
15
18
|
// CONSTANTS //
|
|
@@ -66,10 +69,6 @@ describe('UserRouter', () => {
|
|
|
66
69
|
dbUsers = await UserRepoTestOnly.insertMultiple(DUMMY_USERS);
|
|
67
70
|
});
|
|
68
71
|
|
|
69
|
-
afterEach(() => {
|
|
70
|
-
vi.restoreAllMocks();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
72
|
// --- `Get` ---
|
|
74
73
|
describe(P.Get.DISPLAY, () => {
|
|
75
74
|
describe('on success', () => {
|
|
@@ -77,19 +76,19 @@ describe('UserRouter', () => {
|
|
|
77
76
|
const res: TypedRes<{ users: UserEntity[] }> = await agent.get(
|
|
78
77
|
P.Get.CALL,
|
|
79
78
|
);
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
assert.equal(res.status, OK);
|
|
80
|
+
assert.deepEqual(res.body.users, DUMMY_USERS);
|
|
82
81
|
});
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
describe('when an unexpected error is thrown', () => {
|
|
86
|
-
it(`returns status "${INTERNAL_SERVER_ERROR}"`, async () => {
|
|
85
|
+
it(`returns status "${INTERNAL_SERVER_ERROR}"`, async (t) => {
|
|
87
86
|
const error = new Error('boom');
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Mocks made through `t.mock` are restored when the test ends
|
|
88
|
+
t.mock.method(UserService, 'getAll', () => Promise.reject(error));
|
|
90
89
|
const res: TypedErrRes = await agent.get(P.Get.CALL);
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
assert.equal(res.status, INTERNAL_SERVER_ERROR);
|
|
91
|
+
assert.equal(res.body.error, 'Internal Server Error');
|
|
93
92
|
});
|
|
94
93
|
});
|
|
95
94
|
});
|
|
@@ -103,21 +102,22 @@ describe('UserRouter', () => {
|
|
|
103
102
|
const res: TypedRes<{ user: UserEntity }> = await agent
|
|
104
103
|
.post(P.Add.CALL)
|
|
105
104
|
.send({ user: input });
|
|
106
|
-
|
|
105
|
+
assert.equal(res.status, CREATED);
|
|
107
106
|
const isId = User.is.id(res.body.user.id);
|
|
108
|
-
|
|
107
|
+
assert.equal(isId, true);
|
|
109
108
|
const all = await UserRepo.getAll();
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
assert.equal(all.length, DUMMY_USERS.length + 1);
|
|
110
|
+
const saved = all.find((u) => u.id === res.body.user.id);
|
|
111
|
+
assert.deepEqual(saved, res.body.user);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
describe('when the user param is missing', () => {
|
|
116
116
|
it('returns a validation error', async () => {
|
|
117
117
|
const res = await agent.post(P.Add.CALL).send({ user: null });
|
|
118
|
-
|
|
118
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
119
119
|
const errMsg = extractErrMsg(res);
|
|
120
|
-
|
|
120
|
+
assert.equal(errMsg, 'Invalid input: expected object, received null');
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
@@ -126,10 +126,10 @@ describe('UserRouter', () => {
|
|
|
126
126
|
const res = await agent
|
|
127
127
|
.post(P.Add.CALL)
|
|
128
128
|
.send({ user: { name: '', email: 'a@a.com' } });
|
|
129
|
-
|
|
129
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
130
130
|
// Verify it didn't persist in the database
|
|
131
131
|
const allUsers = await UserRepo.getAll();
|
|
132
|
-
|
|
132
|
+
assert.equal(allUsers.length, DUMMY_USERS.length);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
@@ -139,12 +139,12 @@ describe('UserRouter', () => {
|
|
|
139
139
|
.post(P.Add.CALL)
|
|
140
140
|
.set('Content-Type', 'application/json')
|
|
141
141
|
.send('{"user":');
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
143
|
+
assert.deepEqual(res.body, { error: 'Invalid JSON' });
|
|
144
|
+
assert.deepEqual(await UserRepo.getAll(), DUMMY_USERS);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
const oversizedBodies = [
|
|
148
148
|
[
|
|
149
149
|
'application/json',
|
|
150
150
|
JSON.stringify({ user: { name: 'a'.repeat(110000) } }),
|
|
@@ -153,15 +153,19 @@ describe('UserRouter', () => {
|
|
|
153
153
|
'application/x-www-form-urlencoded',
|
|
154
154
|
`user[name]=${'a'.repeat(110000)}`,
|
|
155
155
|
],
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
] as const;
|
|
157
|
+
|
|
158
|
+
for (const [contentType, body] of oversizedBodies) {
|
|
159
|
+
it(`returns 413 for an oversized ${contentType} body`, async () => {
|
|
160
|
+
const res: TypedErrRes = await agent
|
|
161
|
+
.post(P.Add.CALL)
|
|
162
|
+
.set('Content-Type', contentType)
|
|
163
|
+
.send(body);
|
|
164
|
+
assert.equal(res.status, HttpStatusCodes.PAYLOAD_TOO_LARGE);
|
|
165
|
+
assert.deepEqual(res.body, { error: 'Request body too large' });
|
|
166
|
+
assert.deepEqual(await UserRepo.getAll(), DUMMY_USERS);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
165
169
|
});
|
|
166
170
|
});
|
|
167
171
|
|
|
@@ -171,10 +175,10 @@ describe('UserRouter', () => {
|
|
|
171
175
|
it(`saves the change and returns status "${OK}"`, async () => {
|
|
172
176
|
const user = { ...dbUsers[0], name: 'Bill' };
|
|
173
177
|
const res = await agent.put(P.Update.CALL).send({ user });
|
|
174
|
-
|
|
178
|
+
assert.equal(res.status, OK);
|
|
175
179
|
const allUsers = await UserRepo.getAll();
|
|
176
180
|
const saved = allUsers.find((u) => u.id === user.id);
|
|
177
|
-
|
|
181
|
+
assert.equal(saved?.name, 'Bill');
|
|
178
182
|
});
|
|
179
183
|
});
|
|
180
184
|
|
|
@@ -188,9 +192,9 @@ describe('UserRouter', () => {
|
|
|
188
192
|
id: '5',
|
|
189
193
|
};
|
|
190
194
|
const res = await agent.put(P.Update.CALL).send({ user });
|
|
191
|
-
|
|
195
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
192
196
|
const errMsg = extractErrMsg(res);
|
|
193
|
-
|
|
197
|
+
assert.equal(errMsg, 'Not a valid uuid7 id');
|
|
194
198
|
});
|
|
195
199
|
});
|
|
196
200
|
|
|
@@ -198,8 +202,8 @@ describe('UserRouter', () => {
|
|
|
198
202
|
it(`returns status "${NOT_FOUND}"`, async () => {
|
|
199
203
|
const user = User.create({ name: 'a', email: 'a@a.com' });
|
|
200
204
|
const res: TypedErrRes = await agent.put(P.Update.CALL).send({ user });
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
assert.equal(res.status, NOT_FOUND);
|
|
206
|
+
assert.equal(res.body.error, Errors.USER_NOT_FOUND);
|
|
203
207
|
});
|
|
204
208
|
});
|
|
205
209
|
});
|
|
@@ -211,9 +215,9 @@ describe('UserRouter', () => {
|
|
|
211
215
|
const id = dbUsers[0].id;
|
|
212
216
|
const path = P.Delete.CALL(id);
|
|
213
217
|
const res = await agent.delete(path);
|
|
214
|
-
|
|
218
|
+
assert.equal(res.status, OK);
|
|
215
219
|
const persists = await UserRepo.persists(id);
|
|
216
|
-
|
|
220
|
+
assert.equal(persists, false);
|
|
217
221
|
});
|
|
218
222
|
});
|
|
219
223
|
|
|
@@ -222,8 +226,8 @@ describe('UserRouter', () => {
|
|
|
222
226
|
const id = User.genId();
|
|
223
227
|
const path = P.Delete.CALL(id);
|
|
224
228
|
const res: TypedErrRes = await agent.delete(path);
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
assert.equal(res.status, NOT_FOUND);
|
|
230
|
+
assert.equal(res.body.error, Errors.USER_NOT_FOUND);
|
|
227
231
|
});
|
|
228
232
|
});
|
|
229
233
|
|
|
@@ -231,16 +235,16 @@ describe('UserRouter', () => {
|
|
|
231
235
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
232
236
|
const path = P.Delete.CALL('asdf');
|
|
233
237
|
const res = await agent.delete(path);
|
|
234
|
-
|
|
238
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
235
239
|
});
|
|
236
240
|
});
|
|
237
241
|
|
|
238
242
|
describe('when the id path param is missing', () => {
|
|
239
243
|
it(`returns status "${BAD_REQUEST}"`, async () => {
|
|
240
244
|
const res = await agent.delete(P.Delete.FULL_PATH);
|
|
241
|
-
|
|
245
|
+
assert.equal(res.status, BAD_REQUEST);
|
|
242
246
|
const errMsg = extractErrMsg(res);
|
|
243
|
-
|
|
247
|
+
assert.equal(errMsg, 'Not a valid uuid7 id');
|
|
244
248
|
});
|
|
245
249
|
});
|
|
246
250
|
});
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "bundler",
|
|
4
|
+
"module": "nodenext",
|
|
6
5
|
"esModuleInterop": true,
|
|
7
6
|
"lib": ["ES2022"],
|
|
8
7
|
"strict": true,
|
|
9
8
|
"skipLibCheck": true,
|
|
10
9
|
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// Node runs the .ts files directly and tsc only typechecks, so imports
|
|
11
|
+
// name the .ts files
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
// Node strips types one file at a time, so type-only imports must say so
|
|
15
|
+
// and syntax that needs compiling (enums, namespaces...) isn't allowed
|
|
15
16
|
"verbatimModuleSyntax": true,
|
|
17
|
+
"erasableSyntaxOnly": true,
|
|
16
18
|
"useUnknownInCatchVariables": false,
|
|
17
|
-
"types": ["node"
|
|
19
|
+
"types": ["node"]
|
|
18
20
|
},
|
|
19
|
-
"include": [
|
|
20
|
-
"src/**/*.ts",
|
|
21
|
-
"tests/**/*.ts",
|
|
22
|
-
"eslint.config.ts",
|
|
23
|
-
"vitest.config.ts"
|
|
24
|
-
],
|
|
21
|
+
"include": ["src/**/*.ts", "tests/**/*.ts", "eslint.config.ts"],
|
|
25
22
|
"exclude": ["src/public/*"]
|
|
26
23
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-generator-typescript",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Generate new Express applications similar to express-generate which but sets it up to use TypeScript instead",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"install:clean": "rm -rf node_modules && rm -rf package-lock.json && npm i",
|
|
7
7
|
"test": "node --test \"tests/**/*.test.js\"",
|
|
8
8
|
"test:e2e": "node scripts/e2e.js",
|
|
9
|
-
"_local:commit": "
|
|
9
|
+
"_local:commit": "node ../my-tools/src/workflows/commit/run.ts",
|
|
10
10
|
"_local:commit:push": "npm run _local:commit -- -p",
|
|
11
|
-
"_local:squash-history": "
|
|
11
|
+
"_local:squash-history": "node ../my-tools/src/workflows/squash-history/run.ts"
|
|
12
12
|
},
|
|
13
13
|
"main": "lib/express-generator-typescript.js",
|
|
14
14
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"!lib/template/src/repos/common/database.test.json"
|
|
27
27
|
],
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": "^22.
|
|
29
|
+
"node": "^22.18.0 || ^24.0.0 || >=26.0.0"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
"express5",
|
|
55
55
|
"server",
|
|
56
56
|
"tooling",
|
|
57
|
-
"vitest",
|
|
58
57
|
"eslint"
|
|
59
58
|
],
|
|
60
59
|
"author": "sean maxwell",
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "esnext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"lib": ["ES2022"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"rootDir": "./src",
|
|
10
|
-
"outDir": "dist",
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"paths": {
|
|
14
|
-
"@src/*": ["./src/*"]
|
|
15
|
-
},
|
|
16
|
-
"verbatimModuleSyntax": true,
|
|
17
|
-
"useUnknownInCatchVariables": false,
|
|
18
|
-
"types": ["node"],
|
|
19
|
-
"sourceMap": false,
|
|
20
|
-
"removeComments": true
|
|
21
|
-
},
|
|
22
|
-
// After tsc, tsc-alias rewrites "@src/..." imports in dist/ to relative
|
|
23
|
-
// paths and adds the ".js" extensions Node's ESM loader requires (source
|
|
24
|
-
// imports are written without extensions)
|
|
25
|
-
"tsc-alias": {
|
|
26
|
-
"resolveFullPaths": true
|
|
27
|
-
},
|
|
28
|
-
"include": ["src/**/*.ts"],
|
|
29
|
-
"exclude": ["src/public/"]
|
|
30
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { defineConfig } from 'vitest/config';
|
|
3
|
-
|
|
4
|
-
// ========================================================================= //
|
|
5
|
-
// EXPORT //
|
|
6
|
-
// ========================================================================= //
|
|
7
|
-
|
|
8
|
-
export default defineConfig({
|
|
9
|
-
test: {
|
|
10
|
-
globals: true,
|
|
11
|
-
environment: 'node',
|
|
12
|
-
setupFiles: ['dotenv/config', './tests/support/agent.ts'],
|
|
13
|
-
isolate: true,
|
|
14
|
-
// All test files share one JSON database file, so run them one at a time
|
|
15
|
-
fileParallelism: false,
|
|
16
|
-
env: {
|
|
17
|
-
DOTENV_CONFIG_PATH: 'config/.env.test',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
resolve: {
|
|
21
|
-
alias: {
|
|
22
|
-
'@src': path.resolve(import.meta.dirname, './src'),
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
});
|