Skip to content

Commit 693c5f1

Browse files
feat: Add codemod for DEP0185 - Instantiating node:repl classes without new (#242)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: AugustinMauroy <[email protected]>
1 parent 4e7c93a commit 693c5f1

File tree

15 files changed

+363
-0
lines changed

15 files changed

+363
-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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# `repl` classes DEP0185
2+
3+
This recipe provides a guide for migrating from the deprecated instantiation of `node:repl` classes without `new` to proper class instantiation in Node.js.
4+
5+
See [DEP0185](https://nodejs.org/api/deprecations.html#DEP0185).
6+
7+
## Example
8+
9+
**Before:**
10+
11+
```js
12+
const repl = require("node:repl");
13+
const { REPLServer, Recoverable } = require("node:repl");
14+
import { REPLServer } from "node:repl";
15+
const { REPLServer: REPL } = await import("node:repl");
16+
17+
// Missing 'new' keyword
18+
const server1 = repl.REPLServer();
19+
const server2 = REPLServer({ prompt: ">>> " });
20+
const server3 = repl.Recoverable();
21+
const error = Recoverable(new SyntaxError());
22+
const server4 = REPL({ prompt: ">>> " });
23+
```
24+
25+
**After:**
26+
27+
```js
28+
const repl = require("node:repl");
29+
const { REPLServer, Recoverable } = require("node:repl");
30+
import { REPLServer } from "node:repl";
31+
const { REPLServer: REPL } = await import("node:repl");
32+
33+
// With 'new' keyword
34+
const server1 = new repl.REPLServer();
35+
const server2 = new REPLServer({ prompt: ">>> " });
36+
const server3 = new repl.Recoverable();
37+
const error = new Recoverable(new SyntaxError());
38+
const server4 = new REPL({ prompt: ">>> " });
39+
```
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/repl-classes-with-new"
3+
version: 1.0.0
4+
description: "Handle DEP0185: Instantiating node:repl classes without new"
5+
author: GitHub Copilot
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/repl-classes-with-new",
3+
"version": "1.0.0",
4+
"description": "Handle DEP0185: Instantiating node:repl 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/repl-classes-with-new",
13+
"bugs": "https://github.com/nodejs/userland-migrations/issues"
14+
},
15+
"author": "GitHub Copilot",
16+
"license": "MIT",
17+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/repl-classes-with-new/README.md",
18+
"devDependencies": {
19+
"@codemod.com/jssg-types": "^1.0.9"
20+
},
21+
"dependencies": {
22+
"@nodejs/codemod-utils": "*"
23+
}
24+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { getNodeImportStatements, getNodeImportCalls } 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+
import type JS from "@codemod.com/jssg-types/langs/javascript";
6+
7+
/**
8+
* Classes of the repl module
9+
*/
10+
const CLASS_NAMES = [
11+
'REPLServer',
12+
'Recoverable',
13+
];
14+
15+
/**
16+
* Transform function that converts deprecated node:repl classes to use the `new` keyword
17+
*
18+
* Handles:
19+
* 1. `repl.REPLServer()` → `new repl.REPLServer()`
20+
* 2. `repl.Recoverable()` → `new repl.Recoverable()`
21+
* 3. Handles both CommonJS, ESM imports, and dynamic imports
22+
* 4. Preserves constructor arguments and assignments
23+
*/
24+
export default function transform(root: SgRoot<JS>): string | null {
25+
const rootNode = root.root();
26+
const edits: Edit[] = [];
27+
28+
const allStatementNodes = [
29+
...getNodeImportStatements(root, 'repl'),
30+
...getNodeRequireCalls(root, 'repl'),
31+
...getNodeImportCalls(root, 'repl'),
32+
];
33+
34+
// if no imports are present it means that we don't need to process the file
35+
if (!allStatementNodes.length) return null;
36+
37+
const classes = new Set<string>(getReplClassBasePaths(allStatementNodes));
38+
39+
for (const cls of classes) {
40+
const classesWithoutNew = rootNode.findAll({
41+
rule: {
42+
not: { follows: { pattern: 'new' } },
43+
pattern: `${cls}($$$ARGS)`,
44+
},
45+
});
46+
47+
for (const clsWithoutNew of classesWithoutNew) {
48+
edits.push(clsWithoutNew.replace(`new ${clsWithoutNew.text()}`));
49+
}
50+
}
51+
52+
if (!edits.length) return null;
53+
54+
return rootNode.commitEdits(edits);
55+
}
56+
57+
/**
58+
* Get the base path of the repl classes
59+
*
60+
* @param statements - The import & require statements to search for the repl classes
61+
* @returns The base path of the repl classes
62+
*/
63+
function* getReplClassBasePaths(statements: SgNode<JS>[]) {
64+
for (const cls of CLASS_NAMES) {
65+
for (const stmt of statements) {
66+
const resolvedPath = resolveBindingPath(stmt, `$.${cls}`);
67+
if (resolvedPath) {
68+
yield resolvedPath;
69+
}
70+
}
71+
}
72+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const repl = require("node:repl");
2+
3+
// Example 1: Basic REPL server instantiation
4+
const server = new repl.REPLServer();
5+
6+
// Example 2: REPL server with options
7+
const server2 = new repl.REPLServer({
8+
prompt: "custom> ",
9+
input: process.stdin,
10+
output: process.stdout
11+
});
12+
13+
// Example 3: Recoverable class
14+
const error = new repl.Recoverable(new SyntaxError());
15+
16+
// Example 4: Function parameter usage
17+
function createREPL(options) {
18+
return new repl.REPLServer(options);
19+
}
20+
21+
// Example 5: Variable assignment with configuration
22+
const customREPL = new repl.REPLServer({
23+
prompt: "node> ",
24+
useColors: true,
25+
useGlobal: false
26+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Destructured import
2+
const { REPLServer, Recoverable } = require("node:repl");
3+
const server = new REPLServer({ prompt: ">>> " });
4+
5+
// Recoverable without new
6+
const error = new Recoverable(new SyntaxError());
7+
8+
// Another destructured case
9+
const server2 = new REPLServer();
10+
11+
// With options
12+
const server3 = new REPLServer({
13+
prompt: "test> ",
14+
useColors: false
15+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { REPLServer } from "node:repl";
2+
3+
// ESM import with no arguments
4+
const server = new REPLServer();
5+
6+
// ESM import with options
7+
const server2 = new REPLServer({
8+
prompt: ">>> ",
9+
useColors: true
10+
});
11+
12+
// ESM import in function
13+
function createCustomREPL() {
14+
return new REPLServer({
15+
prompt: "custom> "
16+
});
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Dynamic import with await
2+
const { REPLServer, Recoverable } = await import("node:repl");
3+
4+
// REPLServer without new
5+
const server = new REPLServer();
6+
7+
// Recoverable without new
8+
const error = new Recoverable(new SyntaxError());
9+
10+
// With options
11+
const server2 = new REPLServer({
12+
prompt: ">>> ",
13+
useColors: true
14+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const repl = require("node:repl");
2+
3+
// Example 1: Basic REPL server instantiation
4+
const server = repl.REPLServer();
5+
6+
// Example 2: REPL server with options
7+
const server2 = repl.REPLServer({
8+
prompt: "custom> ",
9+
input: process.stdin,
10+
output: process.stdout
11+
});
12+
13+
// Example 3: Recoverable class
14+
const error = repl.Recoverable(new SyntaxError());
15+
16+
// Example 4: Function parameter usage
17+
function createREPL(options) {
18+
return repl.REPLServer(options);
19+
}
20+
21+
// Example 5: Variable assignment with configuration
22+
const customREPL = repl.REPLServer({
23+
prompt: "node> ",
24+
useColors: true,
25+
useGlobal: false
26+
});

0 commit comments

Comments
 (0)