Skip to content

Commit 2f7f1e5

Browse files
feat: update package.json in v5 migration recipe (#143)
* feat: add package.json Express v5 codemod * feat: add transformations for static dotfiles, static mime, and sendfile options in Express 5 codemod * feat: add version parsing and update logic for dependencies in Express 5 codemod * feat: update version to 1.1.0 in codemod.yaml and package.json --------- Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>
1 parent d6c9ddd commit 2f7f1e5

17 files changed

Lines changed: 334 additions & 4 deletions

codemods/v5-migration-recipe/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ This codemod migration recipe helps you update your Express.js v4 applications t
44

55
Included transformations:
66

7+
- **Package JSON Dependencies**: Updates existing package entries in `package.json` that match Express.js v5 direct dependencies and related Express type packages.
78
- **Back Redirect Deprecated**: This transformation updates instances of `res.redirect('back')` and `res.location('back')` to use the recommended alternatives. Registry entry: [https://app.codemod.com/registry/@expressjs/back-redirect-deprecated](https://app.codemod.com/registry/@expressjs/back-redirect-deprecated).
89
- **Explicit Request Params**: Migrates usage of the legacy API `req.param(name)` to the current recommended alternatives. Registry entry: [https://app.codemod.com/registry/@expressjs/explicit-request-params](https://app.codemod.com/registry/@expressjs/explicit-request-params).
910
- **Pluralize Method Names**: Migrates deprecated singular request methods to their pluralized counterparts where applicable. Registry entry: [https://app.codemod.com/registry/@expressjs/pluralize-method-names](https://app.codemod.com/registry/@expressjs/pluralize-method-names).
1011
- **Status Send Order**: Migrates usages of `res.send(status)`, `res.send(obj, status)`, `res.json(obj, status)`, and `res.jsonp(obj, status)` to the recommended argument ordering. Registry entry: [https://app.codemod.com/registry/@expressjs/status-send-order](https://app.codemod.com/registry/@expressjs/status-send-order).
1112
- **Redirect Arg Order**: Converts `res.redirect(url, status)` calls to the recommended `res.redirect(status, url)` ordering. Registry entry: [https://app.codemod.com/registry/@expressjs/redirect-arg-order](https://app.codemod.com/registry/@expressjs/redirect-arg-order).
1213
- **Camelcase Sendfile**: Replaces legacy `res.sendfile(file)` usages with the camel-cased `res.sendFile(file)` API. Registry entry: [https://app.codemod.com/registry/@expressjs/camelcase-sendfile](https://app.codemod.com/registry/@expressjs/camelcase-sendfile).
1314
- **Route Del to Delete**: Migrates usage of the legacy APIs `app.del()` to `app.delete()`. Registry entry: [https://app.codemod.com/registry/@expressjs/route-del-to-delete](https://app.codemod.com/registry/@expressjs/route-del-to-delete).
15+
- **Static Dotfiles**: Adds an explicit `dotfiles` option to `express.static()` calls and renames the removed `hidden` and `from` options to preserve Express 4 behavior. Registry entry: [https://app.codemod.com/registry/@expressjs/static-dotfiles](https://app.codemod.com/registry/@expressjs/static-dotfiles).
16+
- **Static Mime**: Migrates `express.static.mime` (removed in Express 5) to the `mime-types` package. Registry entry: [https://app.codemod.com/registry/@expressjs/static-mime](https://app.codemod.com/registry/@expressjs/static-mime).
17+
- **Sendfile Options**: Adds an explicit `dotfiles` option to `res.sendFile()` calls and renames the removed `hidden` and `from` options to preserve Express 4 behavior. Registry entry: [https://app.codemod.com/registry/@expressjs/sendfile-options](https://app.codemod.com/registry/@expressjs/sendfile-options).
1418

1519
## References
1620

codemods/v5-migration-recipe/codemod.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
schema_version: "1.0"
22
name: "@expressjs/v5-migration-recipe"
3-
version: "1.0.0"
3+
version: "1.1.0"
44
description: This codemod migration recipe helps you update your Express.js v4 applications to be compatible with Express.js v5 by addressing deprecated APIs.
55
author: bjohansebas (Sebastian Beltran)
66
license: MIT
@@ -10,6 +10,7 @@ category: migration
1010

1111
targets:
1212
languages:
13+
- json
1314
- javascript
1415
- typescript
1516

@@ -23,4 +24,4 @@ keywords:
2324

2425
registry:
2526
access: public
26-
visibility: public
27+
visibility: public

codemods/v5-migration-recipe/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
{
22
"name": "@expressjs/v5-migration-recipe",
33
"private": true,
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"description": "This codemod migration recipe helps you update your Express.js v4 applications to be compatible with Express.js v5 by addressing deprecated APIs.",
66
"type": "module",
7+
"scripts": {
8+
"test": "npx codemod jssg test -l json ./src/package-json.ts ./"
9+
},
710
"repository": {
811
"type": "git",
912
"url": "git+https://github.com/expressjs/codemod.git",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type Json from '@codemod.com/jssg-types/src/langs/json'
2+
import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main'
3+
4+
const PACKAGE_UPDATES = {
5+
'@types/express': '^5.0.0',
6+
'@types/express-serve-static-core': '^5.0.0',
7+
'@types/serve-static': '^2.2.0',
8+
accepts: '^2.0.0',
9+
'body-parser': '^2.2.1',
10+
'content-disposition': '^1.0.0',
11+
'content-type': '^1.0.5',
12+
cookie: '^0.7.1',
13+
'cookie-signature': '^1.2.1',
14+
debug: '^4.4.0',
15+
depd: '^2.0.0',
16+
encodeurl: '^2.0.0',
17+
'escape-html': '^1.0.3',
18+
etag: '^1.8.1',
19+
express: '^5.0.0',
20+
finalhandler: '^2.1.0',
21+
fresh: '^2.0.0',
22+
'http-errors': '^2.0.0',
23+
'merge-descriptors': '^2.0.0',
24+
'mime-types': '^3.0.0',
25+
'on-finished': '^2.4.1',
26+
once: '^1.4.0',
27+
parseurl: '^1.3.3',
28+
'proxy-addr': '^2.0.7',
29+
qs: '^6.14.0',
30+
'range-parser': '^1.2.1',
31+
router: '^2.2.0',
32+
send: '^1.1.0',
33+
'serve-static': '^2.2.0',
34+
statuses: '^2.0.1',
35+
'type-is': '^2.0.1',
36+
vary: '^1.1.2',
37+
} as const
38+
const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const
39+
40+
type PackageJson = {
41+
[key: string]: unknown
42+
}
43+
44+
function isRecord(value: unknown): value is Record<string, unknown> {
45+
return typeof value === 'object' && value !== null && !Array.isArray(value)
46+
}
47+
48+
function parseVersion(range: string): [number, number, number] | null {
49+
// Range operators (^, ~, >=, ...) are ignored and missing parts default to 0,
50+
// so `>=4` parses as 4.0.0. Unparseable ranges such as `*`, `x`, `workspace:*`
51+
// or `latest` yield null and are left untouched.
52+
const match = range.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
53+
if (!match) return null
54+
55+
return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)]
56+
}
57+
58+
function isGreater(a: [number, number, number], b: [number, number, number]): boolean {
59+
for (let index = 0; index < 3; index++) {
60+
if (a[index] !== b[index]) return a[index] > b[index]
61+
}
62+
63+
return false
64+
}
65+
66+
function updateDependency(dependencies: unknown, packageName: string, version: string): boolean {
67+
if (!isRecord(dependencies)) {
68+
return false
69+
}
70+
71+
const current = dependencies[packageName]
72+
if (typeof current !== 'string' || current === version) {
73+
return false
74+
}
75+
76+
// Never downgrade: skip when the declared version is already newer than the
77+
// Express 5 target, so re-running the recipe is idempotent and projects ahead
78+
// of 5.2.1 keep their versions. Equal versions are still normalized to the
79+
// target range (e.g. `~2.0.0` -> `^2.0.0`).
80+
const existing = parseVersion(current)
81+
const target = parseVersion(version)
82+
if (!existing || !target || isGreater(existing, target)) {
83+
return false
84+
}
85+
86+
dependencies[packageName] = version
87+
return true
88+
}
89+
90+
function detectIndent(source: string): string | number {
91+
const match = source.match(/\n([ \t]+)"/)
92+
93+
return match?.[1] ?? 2
94+
}
95+
96+
function detectLineEnding(source: string): string {
97+
return source.includes('\r\n') ? '\r\n' : '\n'
98+
}
99+
100+
async function transform(root: SgRoot<Json>): Promise<string | null> {
101+
const rootNode = root.root()
102+
const source = rootNode.text()
103+
let packageJson: PackageJson
104+
105+
try {
106+
packageJson = JSON.parse(source) as PackageJson
107+
} catch {
108+
return null
109+
}
110+
111+
let changed = false
112+
113+
for (const section of DEPENDENCY_SECTIONS) {
114+
const dependencies = packageJson[section]
115+
116+
for (const [packageName, version] of Object.entries(PACKAGE_UPDATES)) {
117+
changed = updateDependency(dependencies, packageName, version) || changed
118+
}
119+
}
120+
121+
if (!changed) {
122+
return null
123+
}
124+
125+
const lineEnding = detectLineEnding(source)
126+
const nextSource = `${JSON.stringify(packageJson, null, detectIndent(source)).replace(/\n/g, lineEnding)}${source.endsWith('\n') ? lineEnding : ''}`
127+
const edits: Edit[] = [rootNode.replace(nextSource)]
128+
129+
return rootNode.commitEdits(edits)
130+
}
131+
132+
export default transform
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "already-on-v5",
3+
"dependencies": {
4+
"express": "^5.2.1",
5+
"qs": "^6.15.0",
6+
"send": "^1.2.0",
7+
"body-parser": "^2.2.1"
8+
}
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "express-app",
3+
"dependencies": {
4+
"body-parser": "^2.2.1",
5+
"express": "^5.0.0",
6+
"serve-static": "^2.2.0"
7+
},
8+
"devDependencies": {
9+
"@types/express": "^5.0.0",
10+
"typescript": "^5.7.2"
11+
}
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "dev-only",
3+
"devDependencies": {
4+
"express": "^5.0.0"
5+
}
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "no-express",
3+
"dependencies": {
4+
"koa": "^2.15.3"
5+
},
6+
"devDependencies": {
7+
"typescript": "^5.7.2"
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "express-plugin",
3+
"peerDependencies": {
4+
"body-parser": "^2.2.1",
5+
"express": "^5.0.0"
6+
},
7+
"optionalDependencies": {
8+
"@types/express": "^5.0.0",
9+
"serve-static": "^2.2.0"
10+
}
11+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "express-sub-dependencies",
3+
"dependencies": {
4+
"accepts": "^2.0.0",
5+
"array-flatten": "1.1.1",
6+
"body-parser": "^2.2.1",
7+
"content-disposition": "^1.0.0",
8+
"content-type": "^1.0.5",
9+
"cookie": "^0.7.1",
10+
"cookie-signature": "^1.2.1",
11+
"debug": "^4.4.0",
12+
"depd": "^2.0.0",
13+
"encodeurl": "^2.0.0",
14+
"escape-html": "^1.0.3",
15+
"etag": "^1.8.1",
16+
"express": "^5.0.0",
17+
"finalhandler": "^2.1.0",
18+
"fresh": "^2.0.0",
19+
"http-errors": "^2.0.0",
20+
"merge-descriptors": "^2.0.0",
21+
"mime-types": "^3.0.0",
22+
"on-finished": "^2.4.1",
23+
"once": "^1.4.0",
24+
"parseurl": "^1.3.3",
25+
"path-to-regexp": "~0.1.12",
26+
"proxy-addr": "^2.0.7",
27+
"qs": "~6.15.1",
28+
"range-parser": "^1.2.1",
29+
"router": "^2.2.0",
30+
"send": "^1.1.0",
31+
"serve-static": "^2.2.0",
32+
"statuses": "^2.0.1",
33+
"type-is": "^2.0.1",
34+
"utils-merge": "1.0.1",
35+
"vary": "^1.1.2"
36+
},
37+
"devDependencies": {
38+
"@types/express": "^5.0.0",
39+
"@types/express-serve-static-core": "^5.0.0",
40+
"@types/serve-static": "^2.2.0",
41+
"typescript": "^5.7.2"
42+
}
43+
}

0 commit comments

Comments
 (0)