diff --git a/codemods/status-send-order/README.md b/codemods/status-send-order/README.md new file mode 100644 index 0000000..a0fbea2 --- /dev/null +++ b/codemods/status-send-order/README.md @@ -0,0 +1,62 @@ +# Migrate legacy `res.send(obj, status)`, `res.send(status)`, `res.json(obj, status)` and `res.jsonp(obj, status)` + +Migrates usage of the legacy APIs `res.send(obj, status)`, `res.json(obj, status)`, and `res.jsonp(obj, status)` to use the recommended approach of specifying the status code +using the `res.status(status).send(obj)`, `res.status(status).json(obj)`, and +`res.status(status).jsonp(obj)` methods respectively. The older APIs that allowed +specifying the status code as a second argument have been deprecated. + +## Example + +### Migrating `res.send(obj, status)` + +The migration involves replacing instances of `res.send(obj, status)` with `res.status(status).send(obj)`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.send(obj, status); ++ res.status(status).send(obj); +}); +``` + +### Migrating `res.json(obj, status)` + +The migration involves replacing instances of `res.json(obj, status)` with `res.status(status).json(obj)`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.json(obj, status); ++ res.status(status).json(obj); +}); +``` +### Migrating `res.jsonp(obj, status)` + +The migration involves replacing instances of `res.jsonp(obj, status)` with `res.status(status).jsonp(obj)`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.jsonp(obj, status); ++ res.status(status).jsonp(obj); +}); +``` + +### Migrating `res.send(status)` + +The migration involves replacing instances of `res.send(status)` with `res.sendStatus(status)`. + +```diff +app.get('/some-route', (req, res) => { + // Some logic here +- res.send(status); ++ res.sendStatus(status); +}); +``` + +## References + +- [Migration of res.send(status)](https://expressjs.com/en/guide/migrating-5.html#res.send.status) +- [Migration of res.send(obj, status)](https://expressjs.com/en/guide/migrating-5.html#res.send.body) +- [Migration of res.json(obj, status)](https://expressjs.com/en/guide/migrating-5.html#res.json) +- [Migration of res.jsonp(obj, status)](https://expressjs.com/en/guide/migrating-5.html#res.jsonp) diff --git a/codemods/status-send-order/codemod.yaml b/codemods/status-send-order/codemod.yaml new file mode 100644 index 0000000..3d15b5d --- /dev/null +++ b/codemods/status-send-order/codemod.yaml @@ -0,0 +1,27 @@ +schema_version: "1.0" +name: "@expressjs/status-send-order" +version: "1.0.0" +description: Migrates usage of the legacy APIs `res.send(status)`, `res.send(obj, status)`, `res.json(obj, status)` and `res.jsonp(obj, status)` to the current recommended approaches +author: bjohansebas (Sebastian Beltran) +license: MIT +workflow: workflow.yaml +repository: "https://github.com/expressjs/codemod/tree/HEAD/codemods/status-send-order" +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - express + - send + - json + - jsonp + - status + +registry: + access: public + visibility: public \ No newline at end of file diff --git a/codemods/status-send-order/package.json b/codemods/status-send-order/package.json new file mode 100644 index 0000000..b249239 --- /dev/null +++ b/codemods/status-send-order/package.json @@ -0,0 +1,22 @@ +{ + "name": "@expressjs/status-send-order", + "private": true, + "version": "1.0.0", + "description": "Migrates usage of the legacy APIs `res.send(status)`, `res.send(obj, status)`, `res.json(obj, status)` and `res.jsonp(obj, status)` to the current recommended approaches", + "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/status-send-order", + "bugs": "https://github.com/expressjs/codemod/issues" + }, + "author": "bjohansebas (Sebastian Beltran)", + "license": "MIT", + "homepage": "https://github.com/expressjs/codemod/blob/main/codemods/status-send-order/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } +} diff --git a/codemods/status-send-order/src/workflow.ts b/codemods/status-send-order/src/workflow.ts new file mode 100644 index 0000000..50ce5cb --- /dev/null +++ b/codemods/status-send-order/src/workflow.ts @@ -0,0 +1,78 @@ +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: '^(send|json|jsonp)$' }, + }, + }) + + if (!nodes.length) return null + + const edits: Edit[] = [] + + for (const call of nodes) { + const obj = call.getMatch('OBJ') + const args = call.getMultipleMatches('ARG') + + if (args.length === 0 || !obj) continue + + const objDef = obj.definition({ resolveExternal: false }) + if (!objDef) continue + + const method = call.getMatch('METHOD')?.text() + if (!method) continue + + // Single-argument forms: res.send(status) -> res.sendStatus(status) + if (args.length === 1) { + const a0 = args[0] + if (method === 'send' && a0.is('number')) { + edits.push(call.replace(`${obj.text()}.sendStatus(${a0.text()})`)) + } + continue + } + + // Two-argument forms: res.send(obj, status) -> res.status(status).send(obj) + if (args.length >= 2) { + const first = args[0] + const second = args[2] + + if (!second) continue + + // support both orders: (obj, status) and (status, obj) + if (first.is('number') && !second.is('number')) { + const status = first + const body = second + if (method === 'send') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).send(${body.text()})`)) + } else if (method === 'json') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).json(${body.text()})`)) + } else if (method === 'jsonp') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).jsonp(${body.text()})`)) + } + } else if (second.is('number') && !first.is('number')) { + const status = second + const body = first + if (method === 'send') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).send(${body.text()})`)) + } else if (method === 'json') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).json(${body.text()})`)) + } else if (method === 'jsonp') { + edits.push(call.replace(`${obj.text()}.status(${status.text()}).jsonp(${body.text()})`)) + } + } + } + } + + if (!edits.length) return null + + return rootNode.commitEdits(edits) +} + +export default transform diff --git a/codemods/status-send-order/tests/expected/json.ts b/codemods/status-send-order/tests/expected/json.ts new file mode 100644 index 0000000..c25583c --- /dev/null +++ b/codemods/status-send-order/tests/expected/json.ts @@ -0,0 +1,71 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (...arg) { + const [, res] = arg + res.json(); +}); + +app.get("/json", function (...arg) { + const [, res] = arg + res.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", function (req, res) { + res.json(); +}); + +app.get("/json", function (req, res) { + res.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", function (req, response) { + response.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", (req, res) => { + res.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", (req, response) => { + response.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", function (req, res) { + res.status(200).json({}); +}); + +app.get("/json", function (req, response) { + response.status(200).json({}); +}); + +app.get("/json", (req, res) => { + res.status(200).json({}); +}); + +app.get("/json", (req, response) => { + response.status(200).json({}); +}); + +// Still valid syntax -- START +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/tests/expected/jsonp.ts b/codemods/status-send-order/tests/expected/jsonp.ts new file mode 100644 index 0000000..4aafe06 --- /dev/null +++ b/codemods/status-send-order/tests/expected/jsonp.ts @@ -0,0 +1,61 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (req, res) { + res.json(); +}); + +app.get("/jsonp", function (req, res) { + res.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", function (req, response) { + response.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", (req, res) => { + res.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", (req, response) => { + response.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", function (req, res) { + res.status(200).jsonp({}); +}); + +app.get("/jsonp", function (req, response) { + response.status(200).jsonp({}); +}); + +app.get("/jsonp", (req, res) => { + res.status(200).jsonp({}) +}); + +app.get("/jsonp", (req, response) => { + response.status(200).jsonp({}) +}); + +// Still valid syntax -- START +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/tests/expected/send.ts b/codemods/status-send-order/tests/expected/send.ts new file mode 100644 index 0000000..0824a61 --- /dev/null +++ b/codemods/status-send-order/tests/expected/send.ts @@ -0,0 +1,75 @@ +import express from "express"; + +const app = express(); + +app.get("/send", function (...arg) { + const [, res] = arg + res.send(); +}); + +app.get("/send", function (...arg) { + const [, res] = arg + res.status(200).send(true); +}); + +app.get("/send", function (req, res) { + res.send(); +}); + +app.get("/send", function (req, res) { + res.status(200).send({ hello: "world" }); +}); + +app.get("/send", function (req, response) { + response.status(200).send("Hello World"); +}); + +app.get("/send", function (req, res) { + res.sendStatus(200); +}); + +app.get("/send", function (req, res) { + res.status(200).send(true); +}); + +app.get("/send", (req, res) => { + res.status(200).send({ hello: "world" }); +}); + +app.get("/send", (req, res) => { + res.sendStatus(200); +}); + +app.get("/send", (req, response) => { + response.sendStatus(200); +}); + +app.get("/send", (req, response) => { + response.status(200).send(true); +}); + +// Still valid syntax -- START +app.get("/send", function (req, res) { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); + +app.get("/send", function (req, response) { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, response) => { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, res) => { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/tests/input/json.ts b/codemods/status-send-order/tests/input/json.ts new file mode 100644 index 0000000..2f23e2a --- /dev/null +++ b/codemods/status-send-order/tests/input/json.ts @@ -0,0 +1,71 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (...arg) { + const [, res] = arg + res.json(); +}); + +app.get("/json", function (...arg) { + const [, res] = arg + res.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", function (req, res) { + res.json(); +}); + +app.get("/json", function (req, res) { + res.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", function (req, response) { + response.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", (req, res) => { + res.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", (req, response) => { + response.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", function (req, res) { + res.json({}, 200); +}); + +app.get("/json", function (req, response) { + response.json({}, 200); +}); + +app.get("/json", (req, res) => { + res.json({}, 200); +}); + +app.get("/json", (req, response) => { + response.json({}, 200); +}); + +// Still valid syntax -- START +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/tests/input/jsonp.ts b/codemods/status-send-order/tests/input/jsonp.ts new file mode 100644 index 0000000..88a7e27 --- /dev/null +++ b/codemods/status-send-order/tests/input/jsonp.ts @@ -0,0 +1,61 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (req, res) { + res.json(); +}); + +app.get("/jsonp", function (req, res) { + res.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", function (req, response) { + response.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", (req, res) => { + res.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", (req, response) => { + response.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", function (req, res) { + res.jsonp({}, 200); +}); + +app.get("/jsonp", function (req, response) { + response.jsonp({}, 200); +}); + +app.get("/jsonp", (req, res) => { + res.jsonp({}, 200) +}); + +app.get("/jsonp", (req, response) => { + response.jsonp({}, 200) +}); + +// Still valid syntax -- START +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/tests/input/send.ts b/codemods/status-send-order/tests/input/send.ts new file mode 100644 index 0000000..f436e85 --- /dev/null +++ b/codemods/status-send-order/tests/input/send.ts @@ -0,0 +1,75 @@ +import express from "express"; + +const app = express(); + +app.get("/send", function (...arg) { + const [, res] = arg + res.send(); +}); + +app.get("/send", function (...arg) { + const [, res] = arg + res.send(200, true); +}); + +app.get("/send", function (req, res) { + res.send(); +}); + +app.get("/send", function (req, res) { + res.send(200, { hello: "world" }); +}); + +app.get("/send", function (req, response) { + response.send(200, "Hello World"); +}); + +app.get("/send", function (req, res) { + res.send(200); +}); + +app.get("/send", function (req, res) { + res.send(200, true); +}); + +app.get("/send", (req, res) => { + res.send(200, { hello: "world" }); +}); + +app.get("/send", (req, res) => { + res.send(200); +}); + +app.get("/send", (req, response) => { + response.send(200); +}); + +app.get("/send", (req, response) => { + response.send(200, true); +}); + +// Still valid syntax -- START +app.get("/send", function (req, res) { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); + +app.get("/send", function (req, response) { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, response) => { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, res) => { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); +// Still valid syntax -- END \ No newline at end of file diff --git a/codemods/status-send-order/workflow.yaml b/codemods/status-send-order/workflow.yaml new file mode 100644 index 0000000..5e2eda3 --- /dev/null +++ b/codemods/status-send-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.send(status)`, `res.send(obj, status)`, `res.json(obj, status)` and `res.jsonp(obj, status)` 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 f65e18b..93ec2b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,14 @@ "@codemod.com/jssg-types": "^1.3.1" } }, + "codemods/camelcase-sendfile": { + "name": "@expressjs/camelcase-sendfile", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } + }, "codemods/magic-redirect": { "name": "@expressjs/back-redirect-deprecated", "version": "1.0.0", @@ -56,8 +64,8 @@ "@codemod.com/jssg-types": "^1.3.1" } }, - "codemods/sendfile-to-sendFile": { - "name": "@expressjs/sendfile-to-sendfile", + "codemods/status-send-order": { + "name": "@expressjs/status-send-order", "version": "1.0.0", "license": "MIT", "devDependencies": { @@ -1025,8 +1033,12 @@ "resolved": "codemods/back-redirect-deprecated", "link": true }, - "node_modules/@expressjs/sendfile-to-sendfile": { - "resolved": "codemods/sendfile-to-sendFile", + "node_modules/@expressjs/camelcase-sendfile": { + "resolved": "codemods/camelcase-sendfile", + "link": true + }, + "node_modules/@expressjs/status-send-order": { + "resolved": "codemods/status-send-order", "link": true }, "node_modules/@istanbuljs/load-nyc-config": {