Skip to content

Commit 8ab386f

Browse files
max-programmingAugustinMauroybrunocrohJakobJingleheimer
authored
feat(http-classes-with-new): add codemod for node:http classes instantiation with new keyword (#179)
Co-authored-by: Augustin Mauroy <[email protected]> Co-authored-by: Bruno Rodrigues <[email protected]> Co-authored-by: Jacob Smith <[email protected]>
1 parent 1ed2586 commit 8ab386f

File tree

11 files changed

+258
-0
lines changed

11 files changed

+258
-0
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# `http.request` DEP0195
2+
3+
This recipe provides a guide for migrating from the deprecated `http.request` and its synchronous and promise-based counterparts to the new `http.request` method in Node.js.
4+
5+
See [DEP0195](https://nodejs.org/api/deprecations.html#DEP0195).
6+
7+
## Examples
8+
9+
**Before:**
10+
11+
```js
12+
// import { IncomingMessage, ClientRequest } from "node:http";
13+
// const http = require("node:http");
14+
15+
const message = http.OutgoingMessage();
16+
const response = http.ServerResponse(socket);
17+
18+
const incoming = IncomingMessage(socket);
19+
const request = ClientRequest(options);
20+
```
21+
22+
**After:**
23+
24+
```js
25+
// import { IncomingMessage, ClientRequest } from "node:http";
26+
// const http = require("node:http");
27+
28+
const message = new http.OutgoingMessage();
29+
const response = new http.ServerResponse(socket);
30+
31+
const incoming = new IncomingMessage(socket);
32+
const request = new ClientRequest(options);
33+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
schema_version: "1.0"
2+
name: "@nodejs/http-classes-with-new"
3+
version: 1.0.0
4+
description: "Handle DEDEP0195: Instantiating node:http classes without new"
5+
author: Usman S.
6+
license: MIT
7+
workflow: workflow.yaml
8+
category: migration
9+
10+
targets:
11+
languages:
12+
- javascript
13+
- typescript
14+
15+
keywords:
16+
- transformation
17+
- migration
18+
19+
registry:
20+
access: public
21+
visibility: public
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@nodejs/http-classes-with-new",
3+
"version": "1.0.0",
4+
"description": "Handle DEP0195: Instantiating node:http classes without new.",
5+
"type": "module",
6+
"scripts": {
7+
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/nodejs/userland-migrations.git",
12+
"directory": "recipes/http-classes-with-new",
13+
"bugs": "https://github.com/nodejs/userland-migrations/issues"
14+
},
15+
"author": "Usman S.",
16+
"license": "MIT",
17+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/http-classes-with-new/README.md",
18+
"devDependencies": {
19+
"@codemod.com/jssg-types": "^1.0.3"
20+
},
21+
"dependencies": {
22+
"@nodejs/codemod-utils": "*"
23+
}
24+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement';
2+
import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call';
3+
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
4+
import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main';
5+
6+
/**
7+
* Classes of the http module
8+
*/
9+
const CLASS_NAMES = [
10+
'Agent',
11+
'ClientRequest',
12+
'IncomingMessage',
13+
'OutgoingMessage',
14+
'Server',
15+
'ServerResponse',
16+
];
17+
18+
/**
19+
* Transform function that converts deprecated node:http classes to use the `new` keyword
20+
*
21+
* Handles:
22+
* 1. `http.Agent()` → `new http.Agent()`
23+
* 2. `http.ClientRequest()` → `new http.ClientRequest()`
24+
* 3. `http.IncomingMessage()` → `new http.IncomingMessage()`
25+
* 4. `http.OutgoingMessage()` → `new http.OutgoingMessage()`
26+
* 5. `http.Server()` → `new http.Server()`
27+
* 6. `http.ServerResponse() → `new http.ServerResponse()`
28+
*/
29+
export default function transform(root: SgRoot): string | null {
30+
const rootNode = root.root();
31+
const edits: Edit[] = [];
32+
33+
const importNodes = getNodeImportStatements(root, 'http');
34+
const requireNodes = getNodeRequireCalls(root, 'http');
35+
const allStatementNodes = [...importNodes, ...requireNodes];
36+
const classes = new Set<string>(getHttpClassBasePaths(allStatementNodes));
37+
38+
for (const cls of classes) {
39+
const classesWithoutNew = rootNode.findAll({
40+
rule: {
41+
not: { follows: { pattern: 'new' } },
42+
pattern: `${cls}($$$ARGS)`,
43+
},
44+
});
45+
46+
for (const clsWithoutNew of classesWithoutNew) {
47+
edits.push(clsWithoutNew.replace(`new ${clsWithoutNew.text()}`));
48+
}
49+
}
50+
51+
if (edits.length === 0) return null;
52+
53+
return rootNode.commitEdits(edits);
54+
}
55+
56+
/**
57+
* Get the base path of the http classes
58+
*
59+
* @param statements - The import & require statements to search for the http classes
60+
* @returns The base path of the http classes
61+
*/
62+
function* getHttpClassBasePaths(statements: SgNode[]) {
63+
for (const cls of CLASS_NAMES) {
64+
for (const stmt of statements) {
65+
const resolvedPath = resolveBindingPath(stmt, `$.${cls}`);
66+
if (resolvedPath) {
67+
yield resolvedPath;
68+
}
69+
}
70+
}
71+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const http = require("node:http");
2+
3+
const agent = new http.Agent();
4+
const request = new http.ClientRequest(options);
5+
const incoming = new http.IncomingMessage(socket);
6+
const message = new http.OutgoingMessage();
7+
const server = new http.Server();
8+
const response = new http.ServerResponse(socket);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {
2+
Agent,
3+
ClientRequest,
4+
IncomingMessage,
5+
OutgoingMessage,
6+
Server,
7+
ServerResponse,
8+
} from "node:http";
9+
10+
const agent = new Agent();
11+
const request = new ClientRequest(options);
12+
const incoming = new IncomingMessage(socket);
13+
const message = new OutgoingMessage();
14+
const server = new Server();
15+
const response = new ServerResponse(socket);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const http = require("node:http");
2+
3+
const agent = http.Agent();
4+
const request = http.ClientRequest(options);
5+
const incoming = http.IncomingMessage(socket);
6+
const message = http.OutgoingMessage();
7+
const server = http.Server();
8+
const response = http.ServerResponse(socket);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {
2+
Agent,
3+
ClientRequest,
4+
IncomingMessage,
5+
OutgoingMessage,
6+
Server,
7+
ServerResponse,
8+
} from "node:http";
9+
10+
const agent = Agent();
11+
const request = ClientRequest(options);
12+
const incoming = IncomingMessage(socket);
13+
const message = OutgoingMessage();
14+
const server = Server();
15+
const response = ServerResponse(socket);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"allowImportingTsExtensions": true,
4+
"allowJs": true,
5+
"alwaysStrict": true,
6+
"baseUrl": "./",
7+
"declaration": true,
8+
"declarationMap": true,
9+
"emitDeclarationOnly": true,
10+
"lib": ["ESNext", "DOM"],
11+
"module": "NodeNext",
12+
"moduleResolution": "NodeNext",
13+
"noImplicitThis": true,
14+
"removeComments": true,
15+
"strict": true,
16+
"stripInternal": true,
17+
"target": "esnext"
18+
},
19+
"include": ["./"],
20+
"exclude": [
21+
"tests/**"
22+
]
23+
}

0 commit comments

Comments
 (0)