diff --git a/codemods/redirect-arg-order/README.md b/codemods/redirect-arg-order/README.md new file mode 100644 index 0000000..05e90b3 --- /dev/null +++ b/codemods/redirect-arg-order/README.md @@ -0,0 +1,22 @@ +# Migrate legacy `res.redirect(url, status)` + +Migrates usage of the legacy APIs `res.redirect(url, status)` to the new signature +`res.redirect(status, url)`. This usage was deprecated in Express 4, in Express 5 you must use the new signature `res.redirect(status, url)`. + +## Example + +### Migrating `res.redirect(url, status)` + +The migration involves replacing instances of `res.redirect(url, status)` with `res.redirect(status, url)`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.redirect(url, status); ++ res.redirect(status, url); +}); +``` + +## References + +- [Migration of res.redirect(url, status)](https://expressjs.com/en/guide/migrating-5.html#res.redirect) diff --git a/codemods/redirect-arg-order/codemod.yaml b/codemods/redirect-arg-order/codemod.yaml new file mode 100644 index 0000000..2af0c34 --- /dev/null +++ b/codemods/redirect-arg-order/codemod.yaml @@ -0,0 +1,25 @@ +schema_version: "1.0" +name: "@expressjs/redirect-arg-order" +version: "1.0.0" +description: Migrates usage of the legacy APIs `res.redirect(url, status)` to use the recommended argument order `res.redirect(status, url)`. +author: bjohansebas (Sebastian Beltran) +license: MIT +workflow: workflow.yaml +repository: "https://github.com/expressjs/codemod/tree/HEAD/codemods/redirect-arg-order" +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - express + - redirect + - location + +registry: + access: public + visibility: public \ No newline at end of file diff --git a/codemods/redirect-arg-order/package.json b/codemods/redirect-arg-order/package.json new file mode 100644 index 0000000..b76d0b5 --- /dev/null +++ b/codemods/redirect-arg-order/package.json @@ -0,0 +1,22 @@ +{ + "name": "@expressjs/redirect-arg-order", + "private": true, + "version": "1.0.0", + "description": "Migrates usage of the legacy APIs `res.redirect(url, status)` to use the recommended argument order `res.redirect(status, url)`.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/expressjs/codemod.git", + "directory": "codemods/redirect-arg-order", + "bugs": "https://github.com/expressjs/codemod/issues" + }, + "author": "bjohansebas (Sebastian Beltran)", + "license": "MIT", + "homepage": "https://github.com/expressjs/codemod/blob/main/codemods/redirect-arg-order/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } +} diff --git a/codemods/redirect-arg-order/src/workflow.ts b/codemods/redirect-arg-order/src/workflow.ts new file mode 100644 index 0000000..58135f8 --- /dev/null +++ b/codemods/redirect-arg-order/src/workflow.ts @@ -0,0 +1,44 @@ +import type Js from '@codemod.com/jssg-types/src/langs/javascript' +import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main' + +async function transform(root: SgRoot): Promise { + const rootNode = root.root() + + const nodes = rootNode.findAll({ + rule: { + pattern: '$OBJ.$METHOD($$$ARG)', + }, + constraints: { + METHOD: { regex: '^(redirect)$' }, + }, + }) + + if (!nodes.length) return null + + const edits: Edit[] = [] + + for (const call of nodes) { + const obj = call.getMatch('OBJ') + const args = call.getMultipleMatches('ARG') + if (!obj || args.length < 3) continue + + const objDef = obj.definition({ resolveExternal: false }) + if (!objDef) continue + + // $$$ARG yields argument nodes interleaved with separators, so arg nodes are at 0,2,4... + const first = args[0] + const second = args[2] + if (!first || !second) continue + + // Only transform legacy form redirect(url, status) where second is number + if (second.is('number') && !first.is('number')) { + edits.push(call.replace(`${obj.text()}.redirect(${second.text()}, ${first.text()})`)) + } + } + + if (!edits.length) return null + + return rootNode.commitEdits(edits) +} + +export default transform diff --git a/codemods/redirect-arg-order/tests/expected/redirect.ts b/codemods/redirect-arg-order/tests/expected/redirect.ts new file mode 100644 index 0000000..3db2d52 --- /dev/null +++ b/codemods/redirect-arg-order/tests/expected/redirect.ts @@ -0,0 +1,38 @@ +import express from "express"; +import { redirect } from "somelibrary"; + +const app = express(); + +app.get("/", function (...arg) { + const [, res] = arg + res.redirect(); +}); + +app.get("/", function (...arg) { + const [, res] = arg + res.redirect(301, "/other-page"); +}); + +app.get("/", function (req, res) { + res.redirect(); +}); + +app.get("/", function (req, res) { + res.redirect(301, "/other-page"); +}); + +app.get("/", function (req, response) { + response.redirect(301, "/other-page"); +}); + +app.get("/", function (req, res) { + res.redirect(301, "/other-page"); +}); + +app.get("/", function (req, res) { + res.redirect("/other-page"); +}); + +app.get("/", function (req, res) { + redirect(301, "/other-page"); +}); \ No newline at end of file diff --git a/codemods/redirect-arg-order/tests/input/redirect.ts b/codemods/redirect-arg-order/tests/input/redirect.ts new file mode 100644 index 0000000..805d5a5 --- /dev/null +++ b/codemods/redirect-arg-order/tests/input/redirect.ts @@ -0,0 +1,38 @@ +import express from "express"; +import { redirect } from "somelibrary"; + +const app = express(); + +app.get("/", function (...arg) { + const [, res] = arg + res.redirect(); +}); + +app.get("/", function (...arg) { + const [, res] = arg + res.redirect("/other-page", 301); +}); + +app.get("/", function (req, res) { + res.redirect(); +}); + +app.get("/", function (req, res) { + res.redirect("/other-page", 301); +}); + +app.get("/", function (req, response) { + response.redirect("/other-page", 301); +}); + +app.get("/", function (req, res) { + res.redirect(301, "/other-page"); +}); + +app.get("/", function (req, res) { + res.redirect("/other-page"); +}); + +app.get("/", function (req, res) { + redirect(301, "/other-page"); +}); \ No newline at end of file diff --git a/codemods/redirect-arg-order/workflow.yaml b/codemods/redirect-arg-order/workflow.yaml new file mode 100644 index 0000000..08a3b89 --- /dev/null +++ b/codemods/redirect-arg-order/workflow.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: Migrates usage of the legacy APIs `res.redirect(url, status)` to use the recommended argument order `res.redirect(status, url)`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + semantic_analysis: file + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1bf6ff7..ddda229 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,14 @@ "@codemod.com/jssg-types": "^1.3.1" } }, + "codemods/redirect-arg-order": { + "name": "@expressjs/redirect-arg-order", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } + }, "codemods/route-del-to-delete": { "name": "@expressjs/route-del-to-delete", "version": "1.0.0", @@ -1057,6 +1065,10 @@ "resolved": "codemods/pluralize-method-names", "link": true }, + "node_modules/@expressjs/redirect-arg-order": { + "resolved": "codemods/redirect-arg-order", + "link": true + }, "node_modules/@expressjs/route-del-to-delete": { "resolved": "codemods/route-del-to-delete", "link": true