Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions codemods/v5-migration-recipe/codemod.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: "1.0"
name: "@expressjs/v5-migration-recipe"
version: "1.0.0"
version: "1.1.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.
author: bjohansebas (Sebastian Beltran)
license: MIT
Expand All @@ -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
5 changes: 4 additions & 1 deletion codemods/v5-migration-recipe/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"name": "@expressjs/v5-migration-recipe",
"private": true,
"version": "1.0.0",
"version": "1.1.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"
}
}
Loading
Loading