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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
npm run test-legacy:ci

- name: Run test
shell: bash
run: |
npm run test
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
5 changes: 4 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"useIgnoreFile": true
},
"files": {
"ignore": ["__testfixtures__"]
"ignore": ["__testfixtures__", "tests"]
},
"linter": {
"enabled": true,
Expand All @@ -15,6 +15,9 @@
"correctness": {
"noUnusedImports": "error",
"useExhaustiveDependencies": "off"
},
"suspicious": {
"noExplicitAny": "off"
}
}
},
Expand Down
File renamed without changes.
37 changes: 37 additions & 0 deletions codemods/back-redirect-deprecated/README.md
Original file line number Diff line number Diff line change
@@ -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)
24 changes: 24 additions & 0 deletions codemods/back-redirect-deprecated/codemod.yaml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions codemods/back-redirect-deprecated/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
66 changes: 66 additions & 0 deletions codemods/back-redirect-deprecated/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -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<Js>): 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<Js>): SgNode<Js, 'formal_parameters'> | null {
let parent = node.parent()
while (parent) {
if (parent.is('formal_parameters')) return parent
parent = parent.parent()
}
return null
}

async function transform(root: SgRoot<Js>): Promise<string | null> {
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
33 changes: 33 additions & 0 deletions codemods/back-redirect-deprecated/tests/expected/location.ts
Original file line number Diff line number Diff line change
@@ -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") || "/");
}
41 changes: 41 additions & 0 deletions codemods/back-redirect-deprecated/tests/expected/redirect.ts
Original file line number Diff line number Diff line change
@@ -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") || "/");
}
33 changes: 33 additions & 0 deletions codemods/back-redirect-deprecated/tests/input/location.ts
Original file line number Diff line number Diff line change
@@ -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');
}
41 changes: 41 additions & 0 deletions codemods/back-redirect-deprecated/tests/input/redirect.ts
Original file line number Diff line number Diff line change
@@ -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');
}
28 changes: 28 additions & 0 deletions codemods/back-redirect-deprecated/workflow.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading