diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27537c1..872b0c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,12 @@ jobs: echo "Node.js version: $(node -v)" echo "NPM version: $(npm -v)" - - name: Run tests + - name: Run tests-legacy cli shell: bash run: | - npm run test:ci \ No newline at end of file + npm run test-legacy:ci + + - name: Run test + shell: bash + run: | + npm run test \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 88353f7..4d2e197 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -59,7 +59,7 @@ jobs: VERSION="${TAG%@*}" # Everything before @ VERSION="${VERSION#v}" # Remove v prefix CODEMOD_NAME="${TAG#*@}" # Everything after @ - CODEMOD_PATH="recipes/$CODEMOD_NAME" + CODEMOD_PATH="codemods/$CODEMOD_NAME" # Set outputs echo "version=$VERSION" >> $GITHUB_OUTPUT @@ -72,8 +72,8 @@ jobs: run: | if [[ ! -d "$CODEMOD_PATH" ]]; then echo "❌ Codemod directory not found: $CODEMOD_PATH" - echo "Available directories in recipes/:" - ls -lah recipes/ || echo "No recipes directory found" + echo "Available directories in codemods/:" + ls -lah codemods/ || echo "No codemods directory found" exit 1 fi diff --git a/biome.json b/biome.json index 5531f5c..9aee26d 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,7 @@ "useIgnoreFile": true }, "files": { - "ignore": ["__testfixtures__"] + "ignore": ["__testfixtures__", "tests"] }, "linter": { "enabled": true, @@ -15,6 +15,9 @@ "correctness": { "noUnusedImports": "error", "useExhaustiveDependencies": "off" + }, + "suspicious": { + "noExplicitAny": "off" } } }, diff --git a/recipes/.gitkeep b/codemods/.gitkeep similarity index 100% rename from recipes/.gitkeep rename to codemods/.gitkeep diff --git a/codemods/back-redirect-deprecated/README.md b/codemods/back-redirect-deprecated/README.md new file mode 100644 index 0000000..26892ba --- /dev/null +++ b/codemods/back-redirect-deprecated/README.md @@ -0,0 +1,37 @@ +# Migrate legacy `res.redirect('back')` and `res.location('back')` + +Migrates usage of the legacy APIs `res.redirect('back')` and `res.location('back')` +to use the recommended approach of accessing the `Referer` header directly from +the request object. Versions of Express before 5 allowed the use of the string +"back" as a shortcut to redirect to the referring page, but this has been +deprecated. + +## Example + +### Migrating `res.redirect('back')` + +The migration involves replacing instances of `res.redirect('back')` with `res.redirect(req.get('Referer') || '/')`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.redirect('back'); ++ res.redirect(req.get('Referer') || '/'); +}); +``` + +### Migrating `res.location('back')` + +The migration involves replacing instances of `res.location('back')` with `res.location(req.get('Referer') || '/')`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.location('back'); ++ res.location(req.get('Referer') || '/'); +}); +``` + +## References + +- [Migration of res.redirect('back') and res.location('back')](https://expressjs.com/en/guide/migrating-5.html#magic-redirect) diff --git a/codemods/back-redirect-deprecated/codemod.yaml b/codemods/back-redirect-deprecated/codemod.yaml new file mode 100644 index 0000000..5f606d3 --- /dev/null +++ b/codemods/back-redirect-deprecated/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: "@expressjs/back-redirect-deprecated" +version: "1.0.0" +description: Migrates usage of the legacy APIs `res.redirect('back')` and `res.location('back')` to the current recommended approaches +author: bjohansebas (Sebastian Beltran) +license: MIT +workflow: workflow.yaml +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/back-redirect-deprecated/package.json b/codemods/back-redirect-deprecated/package.json new file mode 100644 index 0000000..83d8feb --- /dev/null +++ b/codemods/back-redirect-deprecated/package.json @@ -0,0 +1,22 @@ +{ + "name": "@expressjs/back-redirect-deprecated", + "private": true, + "version": "1.0.0", + "description": "Migrates usage of the legacy APIs `res.redirect('back')` and `res.location('back')`.", + "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/back-redirect-deprecated", + "bugs": "https://github.com/expressjs/codemod/issues" + }, + "author": "bjohansebas (Sebastian Beltran)", + "license": "MIT", + "homepage": "https://github.com/expressjs/codemod/blob/main/codemods/back-redirect-deprecated/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } +} diff --git a/codemods/back-redirect-deprecated/src/workflow.ts b/codemods/back-redirect-deprecated/src/workflow.ts new file mode 100644 index 0000000..1364b5c --- /dev/null +++ b/codemods/back-redirect-deprecated/src/workflow.ts @@ -0,0 +1,66 @@ +import type Js from '@codemod.com/jssg-types/src/langs/javascript' +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/src/main' + +function getStringLiteralValue(node: SgNode): string | null { + if (!node.is('string')) return null + + const fragments = node.findAll({ rule: { kind: 'string_fragment' } }) + if (fragments.length !== 1) return null + return fragments[0]?.text() ?? null +} + +function findParentFunctionParameters(node: SgNode): SgNode | null { + let parent = node.parent() + while (parent) { + if (parent.is('formal_parameters')) return parent + parent = parent.parent() + } + return null +} + +async function transform(root: SgRoot): Promise { + const rootNode = root.root() + + const nodes = rootNode.findAll({ + rule: { + pattern: '$OBJ.$METHOD($ARG)', + }, + constraints: { + METHOD: { regex: '^(redirect|location)$' }, + ARG: { pattern: { context: "'back'", strictness: 'relaxed' } }, + }, + }) + + const edits: Edit[] = [] + + for (const call of nodes) { + const arg = call.getMatch('ARG') + const obj = call.getMatch('OBJ') + if (!arg || !obj) continue + + if (getStringLiteralValue(arg) !== 'back') continue + + const objDef = obj.definition({ resolveExternal: false }) + if (!objDef) continue + + const isParameter = objDef.node.matches({ + rule: { inside: { kind: 'formal_parameters', stopBy: 'end' } }, + }) + if (!isParameter) continue + + const parameters = findParentFunctionParameters(objDef.node) + if (!parameters) continue + + const firstParameter = parameters.find({ rule: { kind: 'identifier' } }) + if (!firstParameter) continue + + const requestName = firstParameter.text() + + edits.push(arg.replace(`${requestName}.get("Referrer") || "/"`)) + } + + if (edits.length === 0) return null + return rootNode.commitEdits(edits) +} + +export default transform diff --git a/codemods/back-redirect-deprecated/tests/expected/location.ts b/codemods/back-redirect-deprecated/tests/expected/location.ts new file mode 100644 index 0000000..569856e --- /dev/null +++ b/codemods/back-redirect-deprecated/tests/expected/location.ts @@ -0,0 +1,33 @@ +import express from "express"; +import { location } from "somelibrary"; + +const app = express(); + +app.get("/", function (req, res) { + res.location(req.get("Referrer") || "/"); +}); +app.get("/", (req, res) => { + res.location(req.get("Referrer") || "/"); +}); +app.get("/", (req, res) => { + res.location("testing"); +}); +app.get("/", (req, res) => { + res.location(); +}); +app.get("/articles", function (request, response) { + response.location(request.get("Referrer") || "/"); +}); +app.get("/articles", function (request, response) { + response.location("testing"); +}); +app.get("/articles", (request, response) => { + response.location(request.get("Referrer") || "/"); +}); +app.get("/articles", function (_req, _res) { + location("back"); +}); + +export function handleLocation(req, res) { + res.location(req.get("Referrer") || "/"); +} \ No newline at end of file diff --git a/codemods/back-redirect-deprecated/tests/expected/redirect.ts b/codemods/back-redirect-deprecated/tests/expected/redirect.ts new file mode 100644 index 0000000..18386c0 --- /dev/null +++ b/codemods/back-redirect-deprecated/tests/expected/redirect.ts @@ -0,0 +1,41 @@ +import express from "express"; +import { redirect } from "somelibrary"; + +const app = express(); + +app.get("/", function (req, res) { + res.redirect(req.get("Referrer") || "/"); +}); +app.get("/", (req, res) => { + res.redirect(req.get("Referrer") || "/"); +}); +app.get("/", (req, res) => { + res.redirect("testing"); +}); +app.get("/", (req, res) => { + res.redirect(); +}); +app.get("/articles", function (request, response) { + response.redirect(request.get("Referrer") || "/"); +}); +app.get("/articles", (request, response) => { + response.redirect(request.get("Referrer") || "/"); +}); +app.get("/articles", function (request, response) { + response.redirect("testing"); +}); +app.get("/articles", function (_req, _res) { + redirect("back"); +}); + +export function handler(requests, response) { + response.redirect(requests.get("Referrer") || "/"); +} + +export function handleRedirect(req: any) { + req.redirect(req.get("Referrer") || "/"); +} + +export function handlerWith(req: any, res: any) { + res.redirect(req.get("Referrer") || "/"); +} \ No newline at end of file diff --git a/codemods/back-redirect-deprecated/tests/input/location.ts b/codemods/back-redirect-deprecated/tests/input/location.ts new file mode 100644 index 0000000..0c69ddf --- /dev/null +++ b/codemods/back-redirect-deprecated/tests/input/location.ts @@ -0,0 +1,33 @@ +import express from "express"; +import { location } from "somelibrary"; + +const app = express(); + +app.get("/", function (req, res) { + res.location('back'); +}); +app.get("/", (req, res) => { + res.location("back"); +}); +app.get("/", (req, res) => { + res.location("testing"); +}); +app.get("/", (req, res) => { + res.location(); +}); +app.get("/articles", function (request, response) { + response.location("back"); +}); +app.get("/articles", function (request, response) { + response.location("testing"); +}); +app.get("/articles", (request, response) => { + response.location("back"); +}); +app.get("/articles", function (_req, _res) { + location("back"); +}); + +export function handleLocation(req, res) { + res.location('back'); +} \ No newline at end of file diff --git a/codemods/back-redirect-deprecated/tests/input/redirect.ts b/codemods/back-redirect-deprecated/tests/input/redirect.ts new file mode 100644 index 0000000..650ec7c --- /dev/null +++ b/codemods/back-redirect-deprecated/tests/input/redirect.ts @@ -0,0 +1,41 @@ +import express from "express"; +import { redirect } from "somelibrary"; + +const app = express(); + +app.get("/", function (req, res) { + res.redirect("back"); +}); +app.get("/", (req, res) => { + res.redirect("back"); +}); +app.get("/", (req, res) => { + res.redirect("testing"); +}); +app.get("/", (req, res) => { + res.redirect(); +}); +app.get("/articles", function (request, response) { + response.redirect("back"); +}); +app.get("/articles", (request, response) => { + response.redirect("back"); +}); +app.get("/articles", function (request, response) { + response.redirect("testing"); +}); +app.get("/articles", function (_req, _res) { + redirect("back"); +}); + +export function handler(requests, response) { + response.redirect('back'); +} + +export function handleRedirect(req: any) { + req.redirect('back'); +} + +export function handlerWith(req: any, res: any) { + res.redirect('back'); +} \ No newline at end of file diff --git a/codemods/back-redirect-deprecated/workflow.yaml b/codemods/back-redirect-deprecated/workflow.yaml new file mode 100644 index 0000000..15bb5c2 --- /dev/null +++ b/codemods/back-redirect-deprecated/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('back')` and `res.location('back')` to the current recommended approaches + 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 dc492f1..01fc31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@expressjs/codemod", "version": "0.0.5", "license": "MIT", + "workspaces": [ + "./codemods/*" + ], "dependencies": { "commander": "^12.1.0", "fast-glob": "^3.3.2", @@ -30,6 +33,18 @@ }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "codemods/magic-redirect": { + "name": "@expressjs/back-redirect-deprecated", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" } }, "node_modules/@ampproject/remapping": { @@ -73,6 +88,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -981,6 +997,17 @@ "node": ">=14.21.3" } }, + "node_modules/@codemod.com/jssg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@codemod.com/jssg-types/-/jssg-types-1.3.1.tgz", + "integrity": "sha512-poYNa8mfr8+4+kBPc3bAKBTaUtOQdg5z3voeGGAAr0tiTBvC4cmmoY/dyHXEWT8F+p8A1tWUnhmJZ4WQXV3HVA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@expressjs/back-redirect-deprecated": { + "resolved": "codemods/magic-redirect", + "link": true + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1810,6 +1837,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -2923,6 +2951,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4622,6 +4651,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4804,6 +4834,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "recipes/magic-redirect": { + "name": "@expressjs/magic-redirect", + "version": "1.0.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } } } } diff --git a/package.json b/package.json index 5c1c94a..6ea09fd 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "build": "tsc -d -p tsconfig.json", "lint": "biome check", "lint:fix": "biome check --fix", - "test": "jest", - "test:ci": "jest --ci", + "test": "npm run test --workspaces --if-present", + "test-legacy": "jest", + "test-legacy:ci": "jest --ci", "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { @@ -46,5 +47,6 @@ }, "engines": { "node": ">=18" - } + }, + "workspaces": ["./codemods/*"] } diff --git a/tsconfig.json b/tsconfig.json index 8e54036..fbdb66a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "strictNullChecks": true, "outDir": "build" }, - "include": ["**/*.ts"], + "include": ["**/*.ts", "./codemods/"], "exclude": ["node_modules", "build", "**/__testfixtures__", "**/__test__", "**/*.spec.ts"] }