Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codemods/v5-migration-recipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ This codemod migration recipe helps you update your Express.js v4 applications t

Included transformations:

- **Package JSON Dependencies**: Updates existing package entries in `package.json` that match Express.js v5 direct dependencies and related Express type packages.
- **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).
- **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).
- **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).
- **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).
- **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).
- **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).
- **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).
- **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).
- **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).
- **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).

## References

Expand Down
3 changes: 2 additions & 1 deletion codemods/v5-migration-recipe/codemod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ category: migration

targets:
languages:
- json
- javascript
- typescript

Expand All @@ -23,4 +24,4 @@ keywords:

registry:
access: public
visibility: public
visibility: public
3 changes: 3 additions & 0 deletions codemods/v5-migration-recipe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"version": "1.0.0",
"description": "This codemod migration recipe helps you update your Express.js v4 applications to be compatible with Express.js v5 by addressing deprecated APIs.",
"type": "module",
"scripts": {
"test": "npx codemod jssg test -l json ./src/package-json.ts ./"
},
"repository": {
"type": "git",
"url": "git+https://github.com/expressjs/codemod.git",
Expand Down
132 changes: 132 additions & 0 deletions codemods/v5-migration-recipe/src/package-json.ts
Comment thread
Vinayak1337 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type Json from '@codemod.com/jssg-types/src/langs/json'
import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main'

const PACKAGE_UPDATES = {
'@types/express': '^5.0.0',
'@types/express-serve-static-core': '^5.0.0',
'@types/serve-static': '^2.2.0',
accepts: '^2.0.0',
'body-parser': '^2.2.1',
'content-disposition': '^1.0.0',
'content-type': '^1.0.5',
cookie: '^0.7.1',
'cookie-signature': '^1.2.1',
debug: '^4.4.0',
depd: '^2.0.0',
encodeurl: '^2.0.0',
'escape-html': '^1.0.3',
etag: '^1.8.1',
express: '^5.0.0',
finalhandler: '^2.1.0',
fresh: '^2.0.0',
'http-errors': '^2.0.0',
'merge-descriptors': '^2.0.0',
'mime-types': '^3.0.0',
'on-finished': '^2.4.1',
once: '^1.4.0',
parseurl: '^1.3.3',
'proxy-addr': '^2.0.7',
qs: '^6.14.0',
'range-parser': '^1.2.1',
router: '^2.2.0',
send: '^1.1.0',
'serve-static': '^2.2.0',
statuses: '^2.0.1',
'type-is': '^2.0.1',
vary: '^1.1.2',
} as const
const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const

type PackageJson = {
[key: string]: unknown
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function parseVersion(range: string): [number, number, number] | null {
// Range operators (^, ~, >=, ...) are ignored and missing parts default to 0,
// so `>=4` parses as 4.0.0. Unparseable ranges such as `*`, `x`, `workspace:*`
// or `latest` yield null and are left untouched.
const match = range.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
if (!match) return null

return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)]
}

function isGreater(a: [number, number, number], b: [number, number, number]): boolean {
for (let index = 0; index < 3; index++) {
if (a[index] !== b[index]) return a[index] > b[index]
}

return false
}

function updateDependency(dependencies: unknown, packageName: string, version: string): boolean {
if (!isRecord(dependencies)) {
return false
}

const current = dependencies[packageName]
if (typeof current !== 'string' || current === version) {
return false
}

// Never downgrade: skip when the declared version is already newer than the
// Express 5 target, so re-running the recipe is idempotent and projects ahead
// of 5.2.1 keep their versions. Equal versions are still normalized to the
// target range (e.g. `~2.0.0` -> `^2.0.0`).
const existing = parseVersion(current)
const target = parseVersion(version)
if (!existing || !target || isGreater(existing, target)) {
return false
}

dependencies[packageName] = version
return true
}

function detectIndent(source: string): string | number {
const match = source.match(/\n([ \t]+)"/)

return match?.[1] ?? 2
}

function detectLineEnding(source: string): string {
return source.includes('\r\n') ? '\r\n' : '\n'
}

async function transform(root: SgRoot<Json>): Promise<string | null> {
const rootNode = root.root()
const source = rootNode.text()
let packageJson: PackageJson

try {
packageJson = JSON.parse(source) as PackageJson
} catch {
return null
}

let changed = false

for (const section of DEPENDENCY_SECTIONS) {
const dependencies = packageJson[section]

for (const [packageName, version] of Object.entries(PACKAGE_UPDATES)) {
changed = updateDependency(dependencies, packageName, version) || changed
}
}

if (!changed) {
return null
}

const lineEnding = detectLineEnding(source)
const nextSource = `${JSON.stringify(packageJson, null, detectIndent(source)).replace(/\n/g, lineEnding)}${source.endsWith('\n') ? lineEnding : ''}`
const edits: Edit[] = [rootNode.replace(nextSource)]

return rootNode.commitEdits(edits)
}

export default transform
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "already-on-v5",
"dependencies": {
"express": "^5.2.1",
"qs": "^6.15.0",
"send": "^1.2.0",
"body-parser": "^2.2.1"
}
}
12 changes: 12 additions & 0 deletions codemods/v5-migration-recipe/tests/expected/dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "express-app",
"dependencies": {
"body-parser": "^2.2.1",
"express": "^5.0.0",
"serve-static": "^2.2.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"typescript": "^5.7.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "dev-only",
"devDependencies": {
"express": "^5.0.0"
}
}
9 changes: 9 additions & 0 deletions codemods/v5-migration-recipe/tests/expected/no-express.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "no-express",
"dependencies": {
"koa": "^2.15.3"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}
11 changes: 11 additions & 0 deletions codemods/v5-migration-recipe/tests/expected/peer-and-optional.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "express-plugin",
"peerDependencies": {
"body-parser": "^2.2.1",
"express": "^5.0.0"
},
"optionalDependencies": {
"@types/express": "^5.0.0",
"serve-static": "^2.2.0"
}
}
43 changes: 43 additions & 0 deletions codemods/v5-migration-recipe/tests/expected/sub-dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "express-sub-dependencies",
"dependencies": {
"accepts": "^2.0.0",
"array-flatten": "1.1.1",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"express": "^5.0.0",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "^2.0.7",
"qs": "~6.15.1",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"utils-merge": "1.0.1",
"vary": "^1.1.2"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "^2.2.0",
"typescript": "^5.7.2"
}
}
9 changes: 9 additions & 0 deletions codemods/v5-migration-recipe/tests/input/already-on-v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "already-on-v5",
"dependencies": {
"express": "^5.2.1",
"qs": "^6.15.0",
"send": "^1.2.0",
"body-parser": "^2.2.1"
}
}
12 changes: 12 additions & 0 deletions codemods/v5-migration-recipe/tests/input/dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "express-app",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.18.2",
"serve-static": "^1.16.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"typescript": "^5.7.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "dev-only",
"devDependencies": {
"express": "~4.21.0"
}
}
9 changes: 9 additions & 0 deletions codemods/v5-migration-recipe/tests/input/no-express.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "no-express",
"dependencies": {
"koa": "^2.15.3"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}
11 changes: 11 additions & 0 deletions codemods/v5-migration-recipe/tests/input/peer-and-optional.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "express-plugin",
"peerDependencies": {
"body-parser": "^1.20.2",
"express": ">=4"
},
"optionalDependencies": {
"@types/express": "^4.17.0",
"serve-static": "^1.15.0"
}
}
43 changes: 43 additions & 0 deletions codemods/v5-migration-recipe/tests/input/sub-dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "express-sub-dependencies",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"express": "^4.18.2",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"mime-types": "^2.1.35",
"on-finished": "~2.4.1",
"once": "^1.3.3",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"router": "^1.3.8",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.19.6",
"@types/serve-static": "^1.15.7",
"typescript": "^5.7.2"
}
}
12 changes: 11 additions & 1 deletion codemods/v5-migration-recipe/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ nodes:
runtime:
type: direct
steps:
- name: Updates package.json Express dependencies for Express.js v5 migrations
js-ast-grep:
js_file: src/package-json.ts
base_path: .
semantic_analysis: file
include:
- "**/package.json"
exclude:
- "**/node_modules/**"
language: json
- name: Migrates usage of the legacy APIs `res.redirect('back')` and `res.location('back')` to the current recommended approaches
codemod:
source: "@expressjs/back-redirect-deprecated"
Expand Down Expand Up @@ -38,4 +48,4 @@ nodes:
source: "@expressjs/static-mime"
- name: Adds explicit dotfiles option to res.sendFile() calls to preserve Express 4 behavior
codemod:
source: "@expressjs/sendfile-options"
source: "@expressjs/sendfile-options"