diff --git a/recipes/http-outgoingmessage-headers/codemod.yaml b/recipes/http-outgoingmessage-headers/codemod.yaml new file mode 100644 index 00000000..cb71cba0 --- /dev/null +++ b/recipes/http-outgoingmessage-headers/codemod.yaml @@ -0,0 +1,22 @@ +schema_version: "1.0" +name: "@nodejs/http-outgoingmessage-headers" +version: 1.0.0 +description: Migrate deprecated usages of OutgoingMessage.prototype._headers and ._headerNames to public HTTP header APIs +author: Node.js Team +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - http + +registry: + access: public + visibility: public diff --git a/recipes/http-outgoingmessage-headers/package.json b/recipes/http-outgoingmessage-headers/package.json new file mode 100644 index 00000000..df516074 --- /dev/null +++ b/recipes/http-outgoingmessage-headers/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/http-outgoingmessage-headers", + "version": "1.0.0", + "description": "Migrate deprecated usages of OutgoingMessage.prototype._headers and ._headerNames to the official HTTP header APIs.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/http-outgoingmessage-headers", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "elvessilvavieira (Elves Vieira)", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/http-outgoingmessage-headers/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/http-outgoingmessage-headers/src/workflow.ts b/recipes/http-outgoingmessage-headers/src/workflow.ts new file mode 100644 index 00000000..f9573acf --- /dev/null +++ b/recipes/http-outgoingmessage-headers/src/workflow.ts @@ -0,0 +1,118 @@ +import type { SgRoot, Edit } from "@codemod.com/jssg-types/main"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + { + const matches = rootNode.findAll({ + rule: { pattern: "$OBJ._headers[$KEY]" }, + }); + for (const m of matches) { + const obj = m.getMatch("OBJ"); + const key = m.getMatch("KEY"); + if (!obj || !key) continue; + + const name = obj.text(); + if (!looksLikeOutgoingMessage(rootNode, name)) continue; + + edits.push(m.replace(`${obj.text()}.getHeader(${key.text()})`)); + } + } + + { + const matches = rootNode.findAll({ + rule: { pattern: "$KEY in $OBJ._headers" }, + }); + for (const m of matches) { + const obj = m.getMatch("OBJ"); + const key = m.getMatch("KEY"); + if (!obj || !key) continue; + + const name = obj.text(); + if (!looksLikeOutgoingMessage(rootNode, name)) continue; + + edits.push(m.replace(`${obj.text()}.hasHeader(${key.text()})`)); + } + } + + { + const matches = rootNode.findAll({ + rule: { pattern: "Object.keys($OBJ._headers)" }, + }); + for (const m of matches) { + const obj = m.getMatch("OBJ"); + if (!obj) continue; + + const name = obj.text(); + if (!looksLikeOutgoingMessage(rootNode, name)) continue; + + edits.push(m.replace(`${obj.text()}.getHeaderNames()`)); + } + } + + { + const matches = rootNode.findAll({ + rule: { pattern: "$OBJ._headerNames" }, + }); + for (const m of matches) { + const obj = m.getMatch("OBJ"); + if (!obj) continue; + + const name = obj.text(); + if (!looksLikeOutgoingMessage(rootNode, name)) continue; + + edits.push(m.replace(`${obj.text()}.getHeaderNames()`)); + } + } + + { + const matches = rootNode.findAll({ + rule: { pattern: "$OBJ._headers" }, + }); + for (const m of matches) { + const obj = m.getMatch("OBJ"); + if (!obj) continue; + + const name = obj.text(); + if (!looksLikeOutgoingMessage(rootNode, name)) continue; + + const text = m.text(); + if (text.includes("[")) continue; + + const parent = m.parent(); + const parentText = parent?.text() ?? ""; + + if (parentText.startsWith("Object.keys(")) continue; + + if (parentText.includes("=") && parentText.trim().startsWith(obj.text())) { + continue; + } + + edits.push(m.replace(`${obj.text()}.getHeaders()`)); + } + } + + if (!edits.length) return null; + return rootNode.commitEdits(edits); +} + +function looksLikeOutgoingMessage( + file: ReturnType["root"]>, + name: string +): boolean { + const methods = [ + "setHeader", + "getHeader", + "getHeaders", + "hasHeader", + "removeHeader", + "writeHead", + ]; + for (const m of methods) { + const hit = file.find({ rule: { pattern: `${name}.${m}($$)` } }); + if (hit) return true; + } + return false; +} diff --git a/recipes/http-outgoingmessage-headers/tests/expected/basic.js b/recipes/http-outgoingmessage-headers/tests/expected/basic.js new file mode 100644 index 00000000..82d2cf4b --- /dev/null +++ b/recipes/http-outgoingmessage-headers/tests/expected/basic.js @@ -0,0 +1,13 @@ +function example(response, request) { + const all = response.getHeaders(); + const ct = response.getHeader("content-type"); + if (response.hasHeader("content-length")) console.log("has length"); + console.log(response.getHeaderNames()); + console.log(response.getHeaderNames()); + + const allReq = request.getHeaders(); + const ua = request.getHeader('user-agent'); + if (request.hasHeader('accept')) console.log('has accept'); + console.log(request.getHeaderNames()); + console.log(request.getHeaderNames()); +} diff --git a/recipes/http-outgoingmessage-headers/tests/input/basic.js b/recipes/http-outgoingmessage-headers/tests/input/basic.js new file mode 100644 index 00000000..1ae1483b --- /dev/null +++ b/recipes/http-outgoingmessage-headers/tests/input/basic.js @@ -0,0 +1,13 @@ +function example(response, request) { + const all = response._headers; + const ct = response._headers["content-type"]; + if ("content-length" in response._headers) console.log("has length"); + console.log(Object.keys(response._headers)); + console.log(response._headerNames); + + const allReq = request._headers; + const ua = request._headers['user-agent']; + if ('accept' in request._headers) console.log('has accept'); + console.log(Object.keys(request._headers)); + console.log(request._headerNames); +} diff --git a/recipes/http-outgoingmessage-headers/workflow.yaml b/recipes/http-outgoingmessage-headers/workflow.yaml new file mode 100644 index 00000000..3ef80082 --- /dev/null +++ b/recipes/http-outgoingmessage-headers/workflow.yaml @@ -0,0 +1,25 @@ +# 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 + steps: + - name: Replace deprecated OutgoingMessage private header fields. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript