From fbd9b755ac1341a1ff82fe6d56852e2548d9ef27 Mon Sep 17 00:00:00 2001 From: Vinayak <34209962+Vinayak1337@users.noreply.github.com> Date: Sun, 24 May 2026 23:44:21 +0530 Subject: [PATCH] feat: add package.json Express v5 codemod --- codemods/v5-migration-recipe/README.md | 1 + codemods/v5-migration-recipe/codemod.yaml | 3 +- codemods/v5-migration-recipe/package.json | 3 + .../v5-migration-recipe/src/package-json.ts | 103 ++++++++++++++++++ .../tests/expected/dependencies.json | 12 ++ .../tests/expected/dev-dependency-only.json | 6 + .../tests/expected/no-express.json | 9 ++ .../tests/expected/peer-and-optional.json | 11 ++ .../tests/expected/sub-dependencies.json | 43 ++++++++ .../tests/input/dependencies.json | 12 ++ .../tests/input/dev-dependency-only.json | 6 + .../tests/input/no-express.json | 9 ++ .../tests/input/peer-and-optional.json | 11 ++ .../tests/input/sub-dependencies.json | 43 ++++++++ codemods/v5-migration-recipe/workflow.yaml | 12 +- 15 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 codemods/v5-migration-recipe/src/package-json.ts create mode 100644 codemods/v5-migration-recipe/tests/expected/dependencies.json create mode 100644 codemods/v5-migration-recipe/tests/expected/dev-dependency-only.json create mode 100644 codemods/v5-migration-recipe/tests/expected/no-express.json create mode 100644 codemods/v5-migration-recipe/tests/expected/peer-and-optional.json create mode 100644 codemods/v5-migration-recipe/tests/expected/sub-dependencies.json create mode 100644 codemods/v5-migration-recipe/tests/input/dependencies.json create mode 100644 codemods/v5-migration-recipe/tests/input/dev-dependency-only.json create mode 100644 codemods/v5-migration-recipe/tests/input/no-express.json create mode 100644 codemods/v5-migration-recipe/tests/input/peer-and-optional.json create mode 100644 codemods/v5-migration-recipe/tests/input/sub-dependencies.json diff --git a/codemods/v5-migration-recipe/README.md b/codemods/v5-migration-recipe/README.md index 3b1abc1..88bd509 100644 --- a/codemods/v5-migration-recipe/README.md +++ b/codemods/v5-migration-recipe/README.md @@ -4,6 +4,7 @@ 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). diff --git a/codemods/v5-migration-recipe/codemod.yaml b/codemods/v5-migration-recipe/codemod.yaml index 5fe3fe7..f2cf797 100644 --- a/codemods/v5-migration-recipe/codemod.yaml +++ b/codemods/v5-migration-recipe/codemod.yaml @@ -10,6 +10,7 @@ category: migration targets: languages: + - json - javascript - typescript @@ -23,4 +24,4 @@ keywords: registry: access: public - visibility: public \ No newline at end of file + visibility: public diff --git a/codemods/v5-migration-recipe/package.json b/codemods/v5-migration-recipe/package.json index c57d61a..4ac638a 100644 --- a/codemods/v5-migration-recipe/package.json +++ b/codemods/v5-migration-recipe/package.json @@ -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", diff --git a/codemods/v5-migration-recipe/src/package-json.ts b/codemods/v5-migration-recipe/src/package-json.ts new file mode 100644 index 0000000..a173a8f --- /dev/null +++ b/codemods/v5-migration-recipe/src/package-json.ts @@ -0,0 +1,103 @@ +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 { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function updateDependency(dependencies: unknown, packageName: string, version: string): boolean { + if (!isRecord(dependencies)) { + return false + } + + if (Object.hasOwn(dependencies, packageName) && dependencies[packageName] !== version) { + dependencies[packageName] = version + return true + } + + return false +} + +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): Promise { + 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 diff --git a/codemods/v5-migration-recipe/tests/expected/dependencies.json b/codemods/v5-migration-recipe/tests/expected/dependencies.json new file mode 100644 index 0000000..5fcd449 --- /dev/null +++ b/codemods/v5-migration-recipe/tests/expected/dependencies.json @@ -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" + } +} diff --git a/codemods/v5-migration-recipe/tests/expected/dev-dependency-only.json b/codemods/v5-migration-recipe/tests/expected/dev-dependency-only.json new file mode 100644 index 0000000..b88048a --- /dev/null +++ b/codemods/v5-migration-recipe/tests/expected/dev-dependency-only.json @@ -0,0 +1,6 @@ +{ + "name": "dev-only", + "devDependencies": { + "express": "^5.0.0" + } +} diff --git a/codemods/v5-migration-recipe/tests/expected/no-express.json b/codemods/v5-migration-recipe/tests/expected/no-express.json new file mode 100644 index 0000000..aa2bfec --- /dev/null +++ b/codemods/v5-migration-recipe/tests/expected/no-express.json @@ -0,0 +1,9 @@ +{ + "name": "no-express", + "dependencies": { + "koa": "^2.15.3" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/codemods/v5-migration-recipe/tests/expected/peer-and-optional.json b/codemods/v5-migration-recipe/tests/expected/peer-and-optional.json new file mode 100644 index 0000000..1398651 --- /dev/null +++ b/codemods/v5-migration-recipe/tests/expected/peer-and-optional.json @@ -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" + } +} diff --git a/codemods/v5-migration-recipe/tests/expected/sub-dependencies.json b/codemods/v5-migration-recipe/tests/expected/sub-dependencies.json new file mode 100644 index 0000000..ba3bc20 --- /dev/null +++ b/codemods/v5-migration-recipe/tests/expected/sub-dependencies.json @@ -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.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", + "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" + } +} diff --git a/codemods/v5-migration-recipe/tests/input/dependencies.json b/codemods/v5-migration-recipe/tests/input/dependencies.json new file mode 100644 index 0000000..faa22cb --- /dev/null +++ b/codemods/v5-migration-recipe/tests/input/dependencies.json @@ -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" + } +} diff --git a/codemods/v5-migration-recipe/tests/input/dev-dependency-only.json b/codemods/v5-migration-recipe/tests/input/dev-dependency-only.json new file mode 100644 index 0000000..c6c644a --- /dev/null +++ b/codemods/v5-migration-recipe/tests/input/dev-dependency-only.json @@ -0,0 +1,6 @@ +{ + "name": "dev-only", + "devDependencies": { + "express": "~4.21.0" + } +} diff --git a/codemods/v5-migration-recipe/tests/input/no-express.json b/codemods/v5-migration-recipe/tests/input/no-express.json new file mode 100644 index 0000000..aa2bfec --- /dev/null +++ b/codemods/v5-migration-recipe/tests/input/no-express.json @@ -0,0 +1,9 @@ +{ + "name": "no-express", + "dependencies": { + "koa": "^2.15.3" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/codemods/v5-migration-recipe/tests/input/peer-and-optional.json b/codemods/v5-migration-recipe/tests/input/peer-and-optional.json new file mode 100644 index 0000000..cad7c2b --- /dev/null +++ b/codemods/v5-migration-recipe/tests/input/peer-and-optional.json @@ -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" + } +} diff --git a/codemods/v5-migration-recipe/tests/input/sub-dependencies.json b/codemods/v5-migration-recipe/tests/input/sub-dependencies.json new file mode 100644 index 0000000..9819b2a --- /dev/null +++ b/codemods/v5-migration-recipe/tests/input/sub-dependencies.json @@ -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" + } +} diff --git a/codemods/v5-migration-recipe/workflow.yaml b/codemods/v5-migration-recipe/workflow.yaml index 77ae2fd..f60c176 100644 --- a/codemods/v5-migration-recipe/workflow.yaml +++ b/codemods/v5-migration-recipe/workflow.yaml @@ -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" @@ -29,4 +39,4 @@ nodes: source: "@expressjs/camelcase-sendfile" - name: Migrates usage of the legacy APIs `app.del()` to `app.delete()` codemod: - source: "@expressjs/route-del-to-delete" \ No newline at end of file + source: "@expressjs/route-del-to-delete"