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 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, testing, and production builds, with defaults aimed at APIs. The project is an ES module and comes with an `@src/*` import alias, so imports stay clean as the app grows.
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 tsx (no build step), restarts the server when you change it, and refreshes the browser when you change front-end files. Vitest, ESLint, and production builds are included.
26
- - **Path aliases** โ€“ import from `@src/*` anywhere. It works in development, tests, and production builds.
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.13+ (22.x), 24.x, or 26+.
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, building, and hot reloading are all set up for you.
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 Vitest.
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 run build` โ€“ Build the project for production.
85
- - `npm start` โ€“ Run the production build.
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 - Vitest",
189
+ "name": "Test",
191
190
  "type": "node",
192
191
  "request": "launch",
193
192
  "runtimeExecutable": "npm",
@@ -2,7 +2,7 @@
2
2
  "plugins": ["@trivago/prettier-plugin-sort-imports"],
3
3
  "importOrder": [
4
4
  "<THIRD_PARTY_MODULES>",
5
- "^@src/[^/]+(/.*)?$",
5
+ "^#src/[^/]+(/.*)?$",
6
6
  "^\\.(?!.*\\.css$).*$",
7
7
  "^.+\\.css$"
8
8
  ],
@@ -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.13+ (22.x), 24.x, or 26+.
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:** development mode runs your `.ts` files directly with `tsx`, 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.
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 [Vitest](https://vitest.dev/guide/).
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 build. Run `npm run build` first.
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 without building.
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**: [dotenv](https://github.com/motdotla/dotenv) loads `config/.env.*`
58
- - **Reloading**: [tsx](https://tsx.hirok.io) (`tsx watch` restarts the server) and [livereload](https://github.com/napcs/node-livereload) + [connect-livereload](https://github.com/intesso/connect-livereload) (refreshes the browser)
59
- - **Testing**: [Vitest](https://vitest.dev) + [Supertest](https://github.com/ladjs/supertest)
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
- - **Building**: `tsc` + [tsc-alias](https://github.com/justkey007/tsc-alias) (rewrites `@src/*` imports in `dist/`)
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` in development, or `dist/repos/common/database.json` in production. 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.
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(['**/dist/**', '**/public/**/lib/**']),
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',
@@ -1,5 +1,4 @@
1
1
  **/node_modules
2
- dist/
3
2
  **/**/*.log
4
3
  tsconfig.tsbuildinfo
5
4
 
@@ -3,29 +3,27 @@
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "build": "shx rm -rf dist && npm run lint && tsc --project tsconfig.prod.json && tsc-alias -p tsconfig.prod.json && npm run build:copy-static",
7
- "build:copy-static": "shx mkdir -p dist/public dist/views && shx cp -r src/public/* dist/public && shx cp -r src/views/* dist/views",
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": "cross-env DOTENV_CONFIG_PATH=./config/.env.production node --import dotenv/config ./dist/main.js",
13
- "test": "cross-env NODE_ENV=test vitest",
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.13.0 || ^24.0.0 || >=26.0.0"
18
+ "node": "^22.18.0 || ^24.0.0 || >=26.0.0"
18
19
  },
19
20
  "dependencies": {
20
- "cross-env": "^10.1.0",
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.2",
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.2",
36
+ "@types/node": "^26.6.5",
40
37
  "@types/supertest": "^7.2.1",
41
38
  "connect-livereload": "^0.6.1",
42
- "eslint": "^10.11.0",
39
+ "eslint": "^10.12.0",
43
40
  "eslint-config-prettier": "^10.1.8",
44
- "eslint-plugin-n": "^18.3.0",
41
+ "eslint-plugin-n": "^18.4.1",
45
42
  "jiti": "^2.7.0",
46
43
  "livereload": "^0.10.3",
47
- "prettier": "^3.9.9",
48
- "shx": "^0.4.0",
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.70.1",
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,4 +1,4 @@
1
- import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
1
+ import HttpStatusCodes from '#src/common/constants/HttpStatusCodes.ts';
2
2
 
3
3
  // ========================================================================= //
4
4
  // CLASSES //
@@ -1,4 +1,4 @@
1
- import type { ValueOf } from '../utils/validators';
1
+ import type { ValueOf } from '../utils/validators.ts';
2
2
 
3
3
  // ========================================================================= //
4
4
  // CONSTANTS //
@@ -1,4 +1,4 @@
1
- import { ep, pp } from './helpers';
1
+ import { ep, pp } from './helpers.ts';
2
2
 
3
3
  // ========================================================================= //
4
4
  // CONSTANTS //
@@ -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 `tsx watch` restarts the server for a TypeScript change.
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 '@src/common/utils/date.utils';
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 //
@@ -1,4 +1,4 @@
1
- import type { Entity } from '@src/entities/common/entity-base-types';
1
+ import type { Entity } from '#src/entities/common/entity-base-types.ts';
2
2
 
3
3
  // ========================================================================= //
4
4
  // TYPES //
@@ -1,4 +1,4 @@
1
- import type { ISOString } from '@src/common/utils/date.utils';
1
+ import type { ISOString } from '#src/common/utils/date.utils.ts';
2
2
 
3
3
  // ========================================================================= //
4
4
  // TYPES //
@@ -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 '@src/common/constants/env.constants';
6
- import type { UserEntity } from '@src/entities/User';
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
- db = (await jsonfile.readFile(DATABASE_FILE_PATH)) as Partial<Database>;
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
- return jsonfile.writeFile(DATABASE_FILE_PATH, db, { spaces: 2 });
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 '@src/entities/User';
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 '@src/common/constants/Paths';
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 '@src/common/constants/HttpStatusCodes';
5
- import Paths from '@src/common/constants/Paths';
6
- import User from '@src/entities/User';
7
- import UserService from '@src/services/UserService';
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 '@src/common/classes/route-errors';
12
- import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
13
- import Paths from '@src/common/constants/Paths';
14
- import BaseRouter from '@src/routers/api.router';
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('@src/common/utils/dev-only');
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 '@src/common/classes/route-errors';
2
- import User, { type UserEntity, type UserInput } from '@src/entities/User';
3
- import UserRepo from '@src/repos/UserRepo';
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 HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
2
3
 
3
- import { agent } from './support/agent';
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
- expect(res.status).toBe(HttpStatusCodes.FOUND);
13
- expect(res.headers.location).toBe('/users');
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
- expect(res.status).toBe(HttpStatusCodes.OK);
19
- expect(res.headers['content-type']).toMatch(/html/);
34
+ assert.equal(res.status, HttpStatusCodes.OK);
35
+ assert.match(res.headers['content-type'], /html/);
20
36
  });
21
37
 
22
- it.each([
23
- '/scripts/HttpClient.js',
24
- '/scripts/renderUsers.js',
25
- '/scripts/users.js',
26
- '/scripts/lib/bootstrap.bundle.min.js',
27
- '/stylesheets/users.css',
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 '@src/repos/MockOrm';
6
- import app from '@src/server';
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
- beforeAll(async () => {
14
+ before(async () => {
15
15
  agent = supertest.agent(app);
16
16
  await MockOrm.cleanDb();
17
17
  });
@@ -1,15 +1,18 @@
1
- import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
2
- import Paths from '@src/common/constants/Paths';
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 { agent } from './support/agent';
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
- expect(res.status).toBe(OK);
81
- expect(res.body.users).toStrictEqual(DUMMY_USERS);
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
- const getAllSpy = vi.spyOn(UserService, 'getAll');
89
- getAllSpy.mockRejectedValueOnce(error);
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
- expect(res.status).toBe(INTERNAL_SERVER_ERROR);
92
- expect(res.body.error).toBe('Internal Server Error');
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
- expect(res.status).toBe(CREATED);
105
+ assert.equal(res.status, CREATED);
107
106
  const isId = User.is.id(res.body.user.id);
108
- expect(isId).toBe(true);
107
+ assert.equal(isId, true);
109
108
  const all = await UserRepo.getAll();
110
- expect(all).toHaveLength(DUMMY_USERS.length + 1);
111
- expect(all).toContainEqual(res.body.user);
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
- expect(res.status).toBe(BAD_REQUEST);
118
+ assert.equal(res.status, BAD_REQUEST);
119
119
  const errMsg = extractErrMsg(res);
120
- expect(errMsg).toBe('Invalid input: expected object, received null');
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
- expect(res.status).toBe(BAD_REQUEST);
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
- expect(allUsers).toHaveLength(DUMMY_USERS.length);
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
- expect(res.status).toBe(BAD_REQUEST);
143
- expect(res.body).toStrictEqual({ error: 'Invalid JSON' });
144
- expect(await UserRepo.getAll()).toStrictEqual(DUMMY_USERS);
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
- it.each([
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
- ])('returns 413 for an oversized %s body', async (contentType, body) => {
157
- const res: TypedErrRes = await agent
158
- .post(P.Add.CALL)
159
- .set('Content-Type', contentType)
160
- .send(body);
161
- expect(res.status).toBe(HttpStatusCodes.PAYLOAD_TOO_LARGE);
162
- expect(res.body).toStrictEqual({ error: 'Request body too large' });
163
- expect(await UserRepo.getAll()).toStrictEqual(DUMMY_USERS);
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
- expect(res.status).toBe(OK);
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
- expect(saved?.name).toBe('Bill');
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
- expect(res.status).toBe(BAD_REQUEST);
195
+ assert.equal(res.status, BAD_REQUEST);
192
196
  const errMsg = extractErrMsg(res);
193
- expect(errMsg).toBe(errMsg);
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
- expect(res.status).toBe(NOT_FOUND);
202
- expect(res.body.error).toBe(Errors.USER_NOT_FOUND);
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
- expect(res.status).toBe(OK);
218
+ assert.equal(res.status, OK);
215
219
  const persists = await UserRepo.persists(id);
216
- expect(persists).toBe(false);
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
- expect(res.status).toBe(NOT_FOUND);
226
- expect(res.body.error).toBe(Errors.USER_NOT_FOUND);
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
- expect(res.status).toBe(BAD_REQUEST);
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
- expect(res.status).toBe(BAD_REQUEST);
245
+ assert.equal(res.status, BAD_REQUEST);
242
246
  const errMsg = extractErrMsg(res);
243
- expect(errMsg).toStrictEqual('Not a valid uuid7 id');
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": "esnext",
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
- "paths": {
12
- "@src/*": ["./src/*"]
13
- },
14
- // tsx transpiles one file at a time, so type-only imports must say so
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", "vitest/globals"]
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.1.0",
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": "tsx --tsconfig ../my-tools/tsconfig.json ../my-tools/src/workflows/commit/run.ts",
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": "tsx --tsconfig ../my-tools/tsconfig.json ../my-tools/src/workflows/squash-history/run.ts"
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.13.0 || ^24.0.0 || >=26.0.0"
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
- });