From bedccd87e029094557f99b2c04da6c4b6abba39b Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:03:53 +0200 Subject: [PATCH 01/14] scaffold codemod --- package-lock.json | 16 +++++++++ recipes/repl-builtin-modules/README.md | 33 +++++++++++++++++++ recipes/repl-builtin-modules/codemod.yaml | 21 ++++++++++++ recipes/repl-builtin-modules/package.json | 26 +++++++++++++++ recipes/repl-builtin-modules/src/workflow.ts | 22 +++++++++++++ .../tests/expected/file-1.js | 3 ++ .../tests/expected/file-2.js | 3 ++ .../tests/expected/file-3.js | 6 ++++ .../tests/expected/file-4.mjs | 6 ++++ .../tests/input/file-1.js | 3 ++ .../tests/input/file-2.js | 3 ++ .../tests/input/file-3.js | 5 +++ .../tests/input/file-4.mjs | 5 +++ recipes/repl-builtin-modules/tsconfig.json | 23 +++++++++++++ recipes/repl-builtin-modules/workflow.yaml | 25 ++++++++++++++ 15 files changed, 200 insertions(+) create mode 100644 recipes/repl-builtin-modules/README.md create mode 100644 recipes/repl-builtin-modules/codemod.yaml create mode 100644 recipes/repl-builtin-modules/package.json create mode 100644 recipes/repl-builtin-modules/src/workflow.ts create mode 100644 recipes/repl-builtin-modules/tests/expected/file-1.js create mode 100644 recipes/repl-builtin-modules/tests/expected/file-2.js create mode 100644 recipes/repl-builtin-modules/tests/expected/file-3.js create mode 100644 recipes/repl-builtin-modules/tests/expected/file-4.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-1.js create mode 100644 recipes/repl-builtin-modules/tests/input/file-2.js create mode 100644 recipes/repl-builtin-modules/tests/input/file-3.js create mode 100644 recipes/repl-builtin-modules/tests/input/file-4.mjs create mode 100644 recipes/repl-builtin-modules/tsconfig.json create mode 100644 recipes/repl-builtin-modules/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 7ece2478..88177688 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1418,6 +1418,10 @@ "resolved": "recipes/process-main-module", "link": true }, + "node_modules/@nodejs/repl-builtin-modules": { + "resolved": "recipes/repl-builtin-modules", + "link": true + }, "node_modules/@nodejs/rmdir": { "resolved": "recipes/rmdir", "link": true @@ -4084,6 +4088,18 @@ "@types/node": "^24.2.0" } }, + "recipes/repl-builtin-modules": { + "name": "@nodejs/repl-builtin-modules", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3", + "@types/node": "^24.0.3" + } + }, "recipes/rmdir": { "name": "@nodejs/rmdir", "version": "1.0.0", diff --git a/recipes/repl-builtin-modules/README.md b/recipes/repl-builtin-modules/README.md new file mode 100644 index 00000000..bc6df7f6 --- /dev/null +++ b/recipes/repl-builtin-modules/README.md @@ -0,0 +1,33 @@ +# `fs.rmdir` DEP0147 + +This recipe provides a guide for migrating from the deprecated `fs.rmdir` and its synchronous and promise-based counterparts to the new `fs.rm` method in Node.js. + +See [DEP0147](https://nodejs.org/api/deprecations.html#DEP0147). + +## Examples + +**Before:** + +```js +// Using fs.rmdir with the recursive option +fs.rmdir(path, { recursive: true }, callback); + +// Using fs.rmdirSync with the recursive option +fs.rmdirSync(path, { recursive: true }); + +// Using fs.promises.rmdir with the recursive option +fs.promises.rmdir(path, { recursive: true }); +``` + +**After:** + +```js +// Using fs.rm with recursive and force options +fs.rm(path, { recursive: true, force: true }, callback); + +// Using fs.rmSync with recursive and force options +fs.rmSync(path, { recursive: true, force: true }); + +// Using fs.promises.rm with recursive and force options +fs.promises.rm(path, { recursive: true, force: true }); +``` diff --git a/recipes/repl-builtin-modules/codemod.yaml b/recipes/repl-builtin-modules/codemod.yaml new file mode 100644 index 00000000..a647a5fa --- /dev/null +++ b/recipes/repl-builtin-modules/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/repl-builtin-modules" +version: 1.0.0 +description: Handle DEP0191 via transforming `repl.builtinModules` usage to `module.builtinModules`. +author: Augustin Mauroy +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/repl-builtin-modules/package.json b/recipes/repl-builtin-modules/package.json new file mode 100644 index 00000000..9619190c --- /dev/null +++ b/recipes/repl-builtin-modules/package.json @@ -0,0 +1,26 @@ +{ + "name": "@nodejs/repl-builtin-modules", + "version": "1.0.0", + "description": "Handle DEP0191 via transforming `repl.builtinModules` usage to `module.builtinModules`.", + "type": "module", + "scripts": { + "test": "npx codemod@next jssg test -l typescript ./src/workflow.ts ./", + "test:update-snapshots": "npx codemod@next jssg test --update-snapshots -l typescript ./src/workflow.ts ./ " + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/rmdirs", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/rmdirs/README.md", + "devDependencies": { + "@types/node": "^24.0.3", + "@codemod.com/jssg-types": "^1.0.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts new file mode 100644 index 00000000..0f95f696 --- /dev/null +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -0,0 +1,22 @@ +import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; +import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; +import type { SgRoot, Edit } from "@codemod.com/jssg-types/main"; + +/** + * Transform function that converts deprecated fs.rmdir calls + * with recursive: true option to the new fs.rm API. + * + * Handles: + * 1. ... + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + let hasChanges = false; + const edits: Edit[] = []; + + // do things + + if (!hasChanges) return null; + + return rootNode.commitEdits(edits); +} diff --git a/recipes/repl-builtin-modules/tests/expected/file-1.js b/recipes/repl-builtin-modules/tests/expected/file-1.js new file mode 100644 index 00000000..cbc6687f --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-1.js @@ -0,0 +1,3 @@ +const repl = require('node:repl'); + +console.log(repl.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-2.js b/recipes/repl-builtin-modules/tests/expected/file-2.js new file mode 100644 index 00000000..a4a24266 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-2.js @@ -0,0 +1,3 @@ +const { builtinModules } = require('node:module'); + +console.log(builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-3.js b/recipes/repl-builtin-modules/tests/expected/file-3.js new file mode 100644 index 00000000..2955c302 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-3.js @@ -0,0 +1,6 @@ +const { foo } = require('node:repl'); +const { builtinModules } = require('node:module'); + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/expected/file-4.mjs b/recipes/repl-builtin-modules/tests/expected/file-4.mjs new file mode 100644 index 00000000..6a5e1764 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-4.mjs @@ -0,0 +1,6 @@ +import { foo } from 'node:repl'; +import { builtinModules } from 'node:module'; + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-1.js b/recipes/repl-builtin-modules/tests/input/file-1.js new file mode 100644 index 00000000..a86f7cd3 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-1.js @@ -0,0 +1,3 @@ +const module = require('node:module'); + +console.log(module.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-2.js b/recipes/repl-builtin-modules/tests/input/file-2.js new file mode 100644 index 00000000..3aab054f --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-2.js @@ -0,0 +1,3 @@ +const { builtinModules } = require('node:repl'); + +console.log(builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-3.js b/recipes/repl-builtin-modules/tests/input/file-3.js new file mode 100644 index 00000000..3ab65558 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-3.js @@ -0,0 +1,5 @@ +const { builtinModules, foo } = require('node:repl'); + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-4.mjs b/recipes/repl-builtin-modules/tests/input/file-4.mjs new file mode 100644 index 00000000..b0c03340 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-4.mjs @@ -0,0 +1,5 @@ +import { builtinModules, foo } from 'node:repl'; + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tsconfig.json b/recipes/repl-builtin-modules/tsconfig.json new file mode 100644 index 00000000..8ec57821 --- /dev/null +++ b/recipes/repl-builtin-modules/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowJs": true, + "alwaysStrict": true, + "baseUrl": "", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "lib": ["ESNext", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noImplicitThis": true, + "removeComments": true, + "strict": true, + "stripInternal": true, + "target": "esnext" + }, + "include": [""], + "exclude": [ + "tests/**" + ] +} diff --git a/recipes/repl-builtin-modules/workflow.yaml b/recipes/repl-builtin-modules/workflow.yaml new file mode 100644 index 00000000..87d56100 --- /dev/null +++ b/recipes/repl-builtin-modules/workflow.yaml @@ -0,0 +1,25 @@ +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: Handle DEP0191 via transforming `repl.builtinModules` usage to `module.builtinModules`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From acc6164403f26b02d5a4544fe2b5de8c946cffd7 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:29:55 +0200 Subject: [PATCH 02/14] feat(`repl-builtin-modules`): first draft --- recipes/repl-builtin-modules/src/workflow.ts | 205 +++++++++++++++++- .../tests/expected/file-1.js | 4 +- .../tests/input/file-1.js | 4 +- 3 files changed, 199 insertions(+), 14 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 0f95f696..94966ac3 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -1,22 +1,207 @@ import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; import type { SgRoot, Edit } from "@codemod.com/jssg-types/main"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; /** - * Transform function that converts deprecated fs.rmdir calls - * with recursive: true option to the new fs.rm API. + * Transform function that converts deprecated repl.builtinModules + * to module.builtinModules API. * * Handles: - * 1. ... + * 1. const repl = require('node:repl'); repl.builtinModules -> const module = require('node:module'); module.builtinModules + * 2. const { builtinModules } = require('node:repl'); -> const { builtinModules } = require('node:module'); + * 3. const { builtinModules, foo } = require('node:repl'); -> const { foo } = require('node:repl'); const { builtinModules } = require('node:module'); + * 4. import { builtinModules } from 'node:repl'; -> import { builtinModules } from 'node:module'; + * 5. import { builtinModules, foo } from 'node:repl'; -> import { foo } from 'node:repl'; import { builtinModules } from 'node:module'; */ -export default function transform(root: SgRoot): string | null { - const rootNode = root.root(); - let hasChanges = false; - const edits: Edit[] = []; +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + let hasChanges = false; + const edits: Edit[] = []; - // do things + // Step 1: Handle require statements + // @ts-ignore - ast-grep types are not fully compatible with JSSG types + const replRequireStatements = getNodeRequireCalls(root, "repl"); - if (!hasChanges) return null; + for (const statement of replRequireStatements) { + // Check if this is destructuring assignment with builtinModules + const objectPattern = statement.find({ + rule: { + kind: "object_pattern" + } + }); - return rootNode.commitEdits(edits); + if (objectPattern) { + const originalText = objectPattern.text(); + + if (originalText.includes("builtinModules")) { + // Check if builtinModules is the only destructured property + const properties = objectPattern.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern" + } + }); + + const hasOnlyBuiltinModules = properties.length === 1 && + properties[0].text() === "builtinModules"; + + if (hasOnlyBuiltinModules) { + // Case 2: Replace entire require statement + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + edits.push(moduleSpecifier.replace(newModule)); + hasChanges = true; + } + } else { + // Case 3: Split into two statements + const newText = originalText.replace(/,?\s*builtinModules\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); + + if (newText !== "{ }") { + edits.push(objectPattern.replace(newText)); + } + + // Add new module require statement + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? "node:module" : "module"; + const newStatement = `const { builtinModules } = require(${newModule.includes("node:") ? '"node:module"' : '"module"'});`; + + // Insert after current statement + const statementEnd = statement.range().end; + edits.push({ + startPos: statementEnd.index, + endPos: statementEnd.index, + insertedText: `\n${newStatement}` + }); + hasChanges = true; + } + } + } + } else { + // Case 1: Handle namespace import like const repl = require('node:repl') + // Find usages of repl.builtinModules and replace with module.builtinModules + const variableDeclarator = statement.find({ + rule: { + kind: "variable_declarator" + } + }); + + if (variableDeclarator) { + const identifier = variableDeclarator.find({ + rule: { + kind: "identifier" + } + }); + + if (identifier) { + const varName = identifier.text(); + + // Find all member expressions using this variable with builtinModules + const memberExpressions = rootNode.findAll({ + rule: { + pattern: `${varName}.builtinModules` + } + }); + + if (memberExpressions.length > 0) { + // Replace require statement to use module instead + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + edits.push(moduleSpecifier.replace(newModule)); + hasChanges = true; + } + } + } + } + } + } + + // Step 2: Handle import statements + // @ts-ignore - ast-grep types are not fully compatible with JSSG types + const replImportStatements = getNodeImportStatements(root, "repl"); + + for (const statement of replImportStatements) { + const namedImports = statement.find({ + rule: { + kind: "named_imports" + } + }); + + if (namedImports) { + const originalText = namedImports.text(); + + if (originalText.includes("builtinModules")) { + // Check if builtinModules is the only import + const importSpecifiers = namedImports.findAll({ + rule: { + kind: "import_specifier" + } + }); + + const hasOnlyBuiltinModules = importSpecifiers.length === 1 && + importSpecifiers[0].text().includes("builtinModules"); + + if (hasOnlyBuiltinModules) { + // Case 4: Replace entire import statement + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + edits.push(moduleSpecifier.replace(newModule)); + hasChanges = true; + } + } else { + // Case 5: Split into two statements + const newText = originalText.replace(/,?\s*builtinModules\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); + edits.push(namedImports.replace(newText)); + + // Add new module import statement + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? "node:module" : "module"; + const newStatement = `import { builtinModules } from ${newModule.includes("node:") ? '"node:module"' : '"module"'};`; + + // Insert after current statement + const statementEnd = statement.range().end; + edits.push({ + startPos: statementEnd.index, + endPos: statementEnd.index, + insertedText: `\n${newStatement}` + }); + hasChanges = true; + } + } + } + } + } + + if (!hasChanges) return null; + + return rootNode.commitEdits(edits); } diff --git a/recipes/repl-builtin-modules/tests/expected/file-1.js b/recipes/repl-builtin-modules/tests/expected/file-1.js index cbc6687f..a86f7cd3 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-1.js +++ b/recipes/repl-builtin-modules/tests/expected/file-1.js @@ -1,3 +1,3 @@ -const repl = require('node:repl'); +const module = require('node:module'); -console.log(repl.builtinModules); +console.log(module.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-1.js b/recipes/repl-builtin-modules/tests/input/file-1.js index a86f7cd3..cbc6687f 100644 --- a/recipes/repl-builtin-modules/tests/input/file-1.js +++ b/recipes/repl-builtin-modules/tests/input/file-1.js @@ -1,3 +1,3 @@ -const module = require('node:module'); +const repl = require('node:repl'); -console.log(module.builtinModules); +console.log(repl.builtinModules); From 1927e990ebbbd4965a08474dc72bc7b7b1d4ac50 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:34:57 +0200 Subject: [PATCH 03/14] test(`repl-builtin-modules`): add more case --- recipes/repl-builtin-modules/tests/expected/file-3.js | 7 ++----- recipes/repl-builtin-modules/tests/expected/file-4.js | 6 ++++++ .../tests/expected/{file-4.mjs => file-5.mjs} | 0 recipes/repl-builtin-modules/tests/expected/file-6.mjs | 5 +++++ recipes/repl-builtin-modules/tests/expected/file-7.mjs | 3 +++ recipes/repl-builtin-modules/tests/input/file-3.js | 6 ++---- recipes/repl-builtin-modules/tests/input/file-4.js | 5 +++++ .../tests/input/{file-4.mjs => file-5.mjs} | 0 recipes/repl-builtin-modules/tests/input/file-6.mjs | 5 +++++ recipes/repl-builtin-modules/tests/input/file-7.mjs | 3 +++ 10 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 recipes/repl-builtin-modules/tests/expected/file-4.js rename recipes/repl-builtin-modules/tests/expected/{file-4.mjs => file-5.mjs} (100%) create mode 100644 recipes/repl-builtin-modules/tests/expected/file-6.mjs create mode 100644 recipes/repl-builtin-modules/tests/expected/file-7.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-4.js rename recipes/repl-builtin-modules/tests/input/{file-4.mjs => file-5.mjs} (100%) create mode 100644 recipes/repl-builtin-modules/tests/input/file-6.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-7.mjs diff --git a/recipes/repl-builtin-modules/tests/expected/file-3.js b/recipes/repl-builtin-modules/tests/expected/file-3.js index 2955c302..0e74922b 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-3.js +++ b/recipes/repl-builtin-modules/tests/expected/file-3.js @@ -1,6 +1,3 @@ -const { foo } = require('node:repl'); -const { builtinModules } = require('node:module'); +const { builtinModules: nodeBuiltinModules } = require('node:module'); -console.log(builtinModules); - -foo(); // does something else +console.log(nodeBuiltinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-4.js b/recipes/repl-builtin-modules/tests/expected/file-4.js new file mode 100644 index 00000000..2955c302 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-4.js @@ -0,0 +1,6 @@ +const { foo } = require('node:repl'); +const { builtinModules } = require('node:module'); + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/expected/file-4.mjs b/recipes/repl-builtin-modules/tests/expected/file-5.mjs similarity index 100% rename from recipes/repl-builtin-modules/tests/expected/file-4.mjs rename to recipes/repl-builtin-modules/tests/expected/file-5.mjs diff --git a/recipes/repl-builtin-modules/tests/expected/file-6.mjs b/recipes/repl-builtin-modules/tests/expected/file-6.mjs new file mode 100644 index 00000000..5782c64d --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-6.mjs @@ -0,0 +1,5 @@ +import repl from 'node:module'; +import * as nodeRepl from 'node:module'; + +console.log(repl.builtinModules); +console.log(nodeRepl.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-7.mjs b/recipes/repl-builtin-modules/tests/expected/file-7.mjs new file mode 100644 index 00000000..fd67a170 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-7.mjs @@ -0,0 +1,3 @@ +import { builtinModules as nodeBuiltinModules } from 'node:module'; + +console.log(nodeBuiltinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-3.js b/recipes/repl-builtin-modules/tests/input/file-3.js index 3ab65558..07dc1558 100644 --- a/recipes/repl-builtin-modules/tests/input/file-3.js +++ b/recipes/repl-builtin-modules/tests/input/file-3.js @@ -1,5 +1,3 @@ -const { builtinModules, foo } = require('node:repl'); +const { builtinModules: nodeBuiltinModules } = require('node:repl'); -console.log(builtinModules); - -foo(); // does something else +console.log(nodeBuiltinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-4.js b/recipes/repl-builtin-modules/tests/input/file-4.js new file mode 100644 index 00000000..3ab65558 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-4.js @@ -0,0 +1,5 @@ +const { builtinModules, foo } = require('node:repl'); + +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-4.mjs b/recipes/repl-builtin-modules/tests/input/file-5.mjs similarity index 100% rename from recipes/repl-builtin-modules/tests/input/file-4.mjs rename to recipes/repl-builtin-modules/tests/input/file-5.mjs diff --git a/recipes/repl-builtin-modules/tests/input/file-6.mjs b/recipes/repl-builtin-modules/tests/input/file-6.mjs new file mode 100644 index 00000000..29086500 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-6.mjs @@ -0,0 +1,5 @@ +import repl from 'node:repl'; +import * as nodeRepl from 'node:repl'; + +console.log(repl.builtinModules); +console.log(nodeRepl.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-7.mjs b/recipes/repl-builtin-modules/tests/input/file-7.mjs new file mode 100644 index 00000000..d1cb4a7f --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-7.mjs @@ -0,0 +1,3 @@ +import { builtinModules as nodeBuiltinModules } from 'node:repl'; + +console.log(nodeBuiltinModules); From 72d0b97087bc034b0e44ef27ee0be5f55bf5cbeb Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 01:08:02 +0200 Subject: [PATCH 04/14] feat(`repl-builtin-modules`): working draft --- recipes/repl-builtin-modules/src/workflow.ts | 157 +++++++++++++++---- 1 file changed, 129 insertions(+), 28 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 94966ac3..1df56d1d 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -42,8 +42,14 @@ export default function transform(root: SgRoot): string | null { } }); - const hasOnlyBuiltinModules = properties.length === 1 && - properties[0].text() === "builtinModules"; + const pairProperties = objectPattern.findAll({ + rule: { + kind: "pair_pattern" + } + }); + + const hasOnlyBuiltinModules = (properties.length === 1 && properties[0].text() === "builtinModules") || + (properties.length === 0 && pairProperties.length === 1 && pairProperties[0].text().includes("builtinModules")); if (hasOnlyBuiltinModules) { // Case 2: Replace entire require statement @@ -54,37 +60,50 @@ export default function transform(root: SgRoot): string | null { }); if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; edits.push(moduleSpecifier.replace(newModule)); hasChanges = true; } } else { // Case 3: Split into two statements - const newText = originalText.replace(/,?\s*builtinModules\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); + const newText = originalText.replace(/,?\s*builtinModules\s*(:\s*\w+)?\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); if (newText !== "{ }") { - edits.push(objectPattern.replace(newText)); - } + // Get the parent variable declaration to replace the entire statement + const variableDeclaration = statement.parent(); - // Add new module require statement - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "node:module" : "module"; - const newStatement = `const { builtinModules } = require(${newModule.includes("node:") ? '"node:module"' : '"module"'});`; + if (variableDeclaration && (variableDeclaration.kind() === "variable_declaration" || variableDeclaration.kind() === "lexical_declaration")) { + // Extract the alias if present + const aliasMatch = originalText.match(/builtinModules\s*:\s*(\w+)/); + const aliasText = aliasMatch ? `: ${aliasMatch[1]}` : ""; - // Insert after current statement - const statementEnd = statement.range().end; - edits.push({ - startPos: statementEnd.index, - endPos: statementEnd.index, - insertedText: `\n${newStatement}` - }); - hasChanges = true; + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? "node:module" : "module"; + + // Create the new statements + const firstStatement = `const ${newText} = require(${currentModule});`; + const secondStatement = `const { builtinModules${aliasText} } = require(${newModule.includes('node:') ? "'node:module'" : "'module'"});`; + + // Replace the entire variable declaration with both statements + const replacementText = `${firstStatement}\n${secondStatement}`; + + edits.push(variableDeclaration.replace(replacementText)); + hasChanges = true; + } + } else { + // Fallback to just replacing the object pattern + edits.push(objectPattern.replace(newText)); + } + } else { + // If we're removing the entire destructuring, we need to remove the whole statement + edits.push(statement.replace("")); } } } @@ -115,6 +134,14 @@ export default function transform(root: SgRoot): string | null { }); if (memberExpressions.length > 0) { + // Replace variable name from repl to module + edits.push(identifier.replace("module")); + + // Replace all member expressions + for (const memberExpr of memberExpressions) { + edits.push(memberExpr.replace("module.builtinModules")); + } + // Replace require statement to use module instead const moduleSpecifier = statement.find({ rule: { @@ -123,7 +150,7 @@ export default function transform(root: SgRoot): string | null { }); if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; edits.push(moduleSpecifier.replace(newModule)); hasChanges = true; } @@ -138,6 +165,7 @@ export default function transform(root: SgRoot): string | null { const replImportStatements = getNodeImportStatements(root, "repl"); for (const statement of replImportStatements) { + // Handle named imports like: import { builtinModules } from 'node:repl' const namedImports = statement.find({ rule: { kind: "named_imports" @@ -167,13 +195,13 @@ export default function transform(root: SgRoot): string | null { }); if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? '"node:module"' : '"module"'; + const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; edits.push(moduleSpecifier.replace(newModule)); hasChanges = true; } } else { // Case 5: Split into two statements - const newText = originalText.replace(/,?\s*builtinModules\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); + const newText = originalText.replace(/,?\s*builtinModules\s*(:\s*\w+)?\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); edits.push(namedImports.replace(newText)); // Add new module import statement @@ -185,7 +213,12 @@ export default function transform(root: SgRoot): string | null { if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); const newModule = currentModule.includes("node:") ? "node:module" : "module"; - const newStatement = `import { builtinModules } from ${newModule.includes("node:") ? '"node:module"' : '"module"'};`; + + // Extract the alias if present + const aliasMatch = originalText.match(/builtinModules\s*(as\s+\w+)/); + const aliasText = aliasMatch ? ` ${aliasMatch[1]}` : ""; + + const newStatement = `import { builtinModules${aliasText} } from ${newModule.includes("node:") ? "'node:module'" : "'module'"};`; // Insert after current statement const statementEnd = statement.range().end; @@ -199,6 +232,74 @@ export default function transform(root: SgRoot): string | null { } } } + + // Handle default imports like: import repl from 'node:repl' + // Handle namespace imports like: import * as repl from 'node:repl' + if (!namedImports) { + // Look for default or namespace imports that use repl.builtinModules + const importClause = statement.find({ + rule: { + kind: "import_clause" + } + }); + + if (importClause) { + let importIdentifier = null; + + // Check for default import + const defaultImport = importClause.find({ + rule: { + kind: "identifier" + } + }); + + // Check for namespace import + const namespaceImport = importClause.find({ + rule: { + kind: "namespace_import" + } + }); + + if (defaultImport) { + importIdentifier = defaultImport; + } else if (namespaceImport) { + const namespaceIdentifier = namespaceImport.find({ + rule: { + kind: "identifier" + } + }); + if (namespaceIdentifier) { + importIdentifier = namespaceIdentifier; + } + } + + if (importIdentifier) { + const varName = importIdentifier.text(); + + // Find all member expressions using this variable with builtinModules + const memberExpressions = rootNode.findAll({ + rule: { + pattern: `${varName}.builtinModules` + } + }); + + if (memberExpressions.length > 0) { + // Replace the import to use module instead + const moduleSpecifier = statement.find({ + rule: { + kind: "string" + } + }); + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; + edits.push(moduleSpecifier.replace(newModule)); + hasChanges = true; + } + } + } + } + } } if (!hasChanges) return null; From f48cc04d6bd1850d9702c1974c96c8d15ad2a183 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 01:10:34 +0200 Subject: [PATCH 05/14] chore: docs use UTF-8 char --- recipes/repl-builtin-modules/src/workflow.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 1df56d1d..2774031e 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -8,11 +8,11 @@ import type Js from "@codemod.com/jssg-types/langs/javascript"; * to module.builtinModules API. * * Handles: - * 1. const repl = require('node:repl'); repl.builtinModules -> const module = require('node:module'); module.builtinModules - * 2. const { builtinModules } = require('node:repl'); -> const { builtinModules } = require('node:module'); - * 3. const { builtinModules, foo } = require('node:repl'); -> const { foo } = require('node:repl'); const { builtinModules } = require('node:module'); - * 4. import { builtinModules } from 'node:repl'; -> import { builtinModules } from 'node:module'; - * 5. import { builtinModules, foo } from 'node:repl'; -> import { foo } from 'node:repl'; import { builtinModules } from 'node:module'; + * 1. const repl = require('node:repl'); repl.builtinModules → const module = require('node:module'); module.builtinModules + * 2. const { builtinModules } = require('node:repl'); → const { builtinModules } = require('node:module'); + * 3. const { builtinModules, foo } = require('node:repl'); → const { foo } = require('node:repl'); const { builtinModules } = require('node:module'); + * 4. import { builtinModules } from 'node:repl'; → import { builtinModules } from 'node:module'; + * 5. import { builtinModules, foo } from 'node:repl'; → import { foo } from 'node:repl'; import { builtinModules } from 'node:module'; */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); From b33e658f3c019f1e369f59965d402ddc7bb8d574 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 01:19:31 +0200 Subject: [PATCH 06/14] better doc --- recipes/repl-builtin-modules/README.md | 64 ++++++++++++++------ recipes/repl-builtin-modules/src/workflow.ts | 2 + 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/recipes/repl-builtin-modules/README.md b/recipes/repl-builtin-modules/README.md index bc6df7f6..bea7cfcf 100644 --- a/recipes/repl-builtin-modules/README.md +++ b/recipes/repl-builtin-modules/README.md @@ -1,33 +1,63 @@ -# `fs.rmdir` DEP0147 +# `repl.builtinModules` DEP0191 -This recipe provides a guide for migrating from the deprecated `fs.rmdir` and its synchronous and promise-based counterparts to the new `fs.rm` method in Node.js. +This recipe provides a guide for migrating from the deprecated `repl.builtinModules` to the new `module.builtinModules` property in Node.js. -See [DEP0147](https://nodejs.org/api/deprecations.html#DEP0147). +See [DEP0191](https://nodejs.org/api/deprecations.html#DEP0191). ## Examples **Before:** - ```js -// Using fs.rmdir with the recursive option -fs.rmdir(path, { recursive: true }, callback); +// Using require with namespace import +const repl = require('node:repl'); +console.log(repl.builtinModules); + +// Using require with destructuring +const { builtinModules } = require('node:repl'); + +// Using require with mixed destructuring +const { builtinModules, foo } = require('node:repl'); + +// Using ES6 import with named import +import { builtinModules } from 'node:repl'; + +// Using ES6 import with mixed named imports +import { builtinModules, foo } from 'node:repl'; -// Using fs.rmdirSync with the recursive option -fs.rmdirSync(path, { recursive: true }); +// Using ES6 import with default import +import repl from 'node:repl'; +console.log(repl.builtinModules); -// Using fs.promises.rmdir with the recursive option -fs.promises.rmdir(path, { recursive: true }); +// Using ES6 import with namespace import +import * as repl from 'node:repl'; +console.log(repl.builtinModules); ``` **After:** - ```js -// Using fs.rm with recursive and force options -fs.rm(path, { recursive: true, force: true }, callback); +// Using require with namespace import +const module = require('node:module'); +console.log(module.builtinModules); + +// Using require with destructuring +const { builtinModules } = require('node:module'); + +// Using require with mixed destructuring +const { foo } = require('node:repl'); +const { builtinModules } = require('node:module'); + +// Using ES6 import with named import +import { builtinModules } from 'node:module'; + +// Using ES6 import with mixed named imports +import { foo } from 'node:repl'; +import { builtinModules } from 'node:module'; -// Using fs.rmSync with recursive and force options -fs.rmSync(path, { recursive: true, force: true }); +// Using ES6 import with default import +import module from 'node:module'; +console.log(module.builtinModules); -// Using fs.promises.rm with recursive and force options -fs.promises.rm(path, { recursive: true, force: true }); +// Using ES6 import with namespace import +import * as module from 'node:module'; +console.log(module.builtinModules); ``` diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 2774031e..3ce22cf6 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -7,6 +7,8 @@ import type Js from "@codemod.com/jssg-types/langs/javascript"; * Transform function that converts deprecated repl.builtinModules * to module.builtinModules API. * + * See https://nodejs.org/api/deprecations.html#DEP0191 + * * Handles: * 1. const repl = require('node:repl'); repl.builtinModules → const module = require('node:module'); module.builtinModules * 2. const { builtinModules } = require('node:repl'); → const { builtinModules } = require('node:module'); From c50491f98c03b016c9d81b4a2c2a323a0bc41ea4 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 01:21:52 +0200 Subject: [PATCH 07/14] Update package-lock.json --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88177688..8dd01152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1890,9 +1890,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -2208,9 +2208,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", - "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3221,9 +3221,9 @@ } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.120", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz", - "integrity": "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==", + "version": "18.19.121", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz", + "integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" From e3497b4a389dd686bab6c0844832c895adc5dab0 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:04:14 +0200 Subject: [PATCH 08/14] feat(`repl-builtin-modules`): handle DEP0142 --- package-lock.json | 3 +- recipes/repl-builtin-modules/README.md | 2 +- recipes/repl-builtin-modules/codemod.yaml | 1 + recipes/repl-builtin-modules/package.json | 5 +- recipes/repl-builtin-modules/src/workflow.ts | 410 +++++++++--------- .../tests/expected/file-1.js | 1 + .../tests/expected/file-2.js | 2 + .../tests/expected/file-4.js | 1 + .../tests/expected/file-5.mjs | 1 + .../tests/input/file-1.js | 1 + .../tests/input/file-2.js | 2 + .../tests/input/file-4.js | 3 +- .../tests/input/file-5.mjs | 3 +- 13 files changed, 211 insertions(+), 224 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8dd01152..e3591e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4096,8 +4096,7 @@ "@nodejs/codemod-utils": "*" }, "devDependencies": { - "@codemod.com/jssg-types": "^1.0.3", - "@types/node": "^24.0.3" + "@codemod.com/jssg-types": "^1.0.3" } }, "recipes/rmdir": { diff --git a/recipes/repl-builtin-modules/README.md b/recipes/repl-builtin-modules/README.md index bea7cfcf..c921f949 100644 --- a/recipes/repl-builtin-modules/README.md +++ b/recipes/repl-builtin-modules/README.md @@ -2,7 +2,7 @@ This recipe provides a guide for migrating from the deprecated `repl.builtinModules` to the new `module.builtinModules` property in Node.js. -See [DEP0191](https://nodejs.org/api/deprecations.html#DEP0191). +See [DEP0191](https://nodejs.org/api/deprecations.html#DEP0191) and [DEP0142](https://nodejs.org/api/deprecations.html#DEP0142) ## Examples diff --git a/recipes/repl-builtin-modules/codemod.yaml b/recipes/repl-builtin-modules/codemod.yaml index a647a5fa..d4f0e97d 100644 --- a/recipes/repl-builtin-modules/codemod.yaml +++ b/recipes/repl-builtin-modules/codemod.yaml @@ -15,6 +15,7 @@ targets: keywords: - transformation - migration + - deprecation registry: access: public diff --git a/recipes/repl-builtin-modules/package.json b/recipes/repl-builtin-modules/package.json index 9619190c..33b01b2c 100644 --- a/recipes/repl-builtin-modules/package.json +++ b/recipes/repl-builtin-modules/package.json @@ -10,14 +10,13 @@ "repository": { "type": "git", "url": "git+https://github.com/nodejs/userland-migrations.git", - "directory": "recipes/rmdirs", + "directory": "recipes/repl-builtin-modules", "bugs": "https://github.com/nodejs/userland-migrations/issues" }, "author": "Augustin Mauroy", "license": "MIT", - "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/rmdirs/README.md", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/repl-builtin-modules/README.md", "devDependencies": { - "@types/node": "^24.0.3", "@codemod.com/jssg-types": "^1.0.3" }, "dependencies": { diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 3ce22cf6..3e0288b8 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -1,10 +1,12 @@ +import { EOL } from "node:os"; import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; -import type { SgRoot, Edit } from "@codemod.com/jssg-types/main"; +import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; +import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; import type Js from "@codemod.com/jssg-types/langs/javascript"; /** - * Transform function that converts deprecated repl.builtinModules + * Transform function that converts deprecated repl.builtinModules and repl._builtinLibs * to module.builtinModules API. * * See https://nodejs.org/api/deprecations.html#DEP0191 @@ -15,149 +17,167 @@ import type Js from "@codemod.com/jssg-types/langs/javascript"; * 3. const { builtinModules, foo } = require('node:repl'); → const { foo } = require('node:repl'); const { builtinModules } = require('node:module'); * 4. import { builtinModules } from 'node:repl'; → import { builtinModules } from 'node:module'; * 5. import { builtinModules, foo } from 'node:repl'; → import { foo } from 'node:repl'; import { builtinModules } from 'node:module'; + * 6. const repl = require('node:repl'); repl._builtinLibs → const module = require('node:module'); module.builtinModules + * 7. const { _builtinLibs } = require('node:repl'); → const { builtinModules } = require('node:module'); + * 8. import { _builtinLibs } from 'node:repl'; → import { builtinModules } from 'node:module'; */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); let hasChanges = false; const edits: Edit[] = []; + // Helper functions + const getNewModuleSpecifier = (currentModule: string): string => + currentModule.includes("node:") ? "'node:module'" : "'module'"; + + const containsBuiltinProperties = (text: string): boolean => + text.includes("builtinModules") || text.includes("_builtinLibs"); + + const replaceStandaloneBuiltinLibsReferences = (): void => { + const standaloneReferences = rootNode.findAll({ + rule: { pattern: "_builtinLibs" } + }); + + for (const ref of standaloneReferences) { + const parent = ref.parent(); + + if ( + parent + && parent.kind() !== "object_pattern" + && parent.kind() !== "member_expression" + && parent.kind() !== "named_imports" + ) { + edits.push(ref.replace("builtinModules")); + } + } + }; + + const updateModuleSpecifier = (statement: SgNode): void => { + const moduleSpecifier = statement.find({ rule: { kind: "string" } }); + + if (moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = getNewModuleSpecifier(currentModule); + edits.push(moduleSpecifier.replace(newModule)); + } + }; + // Step 1: Handle require statements // @ts-ignore - ast-grep types are not fully compatible with JSSG types const replRequireStatements = getNodeRequireCalls(root, "repl"); for (const statement of replRequireStatements) { - // Check if this is destructuring assignment with builtinModules - const objectPattern = statement.find({ - rule: { - kind: "object_pattern" - } - }); + const objectPattern = statement.find({ rule: { kind: "object_pattern" } }); if (objectPattern) { const originalText = objectPattern.text(); - if (originalText.includes("builtinModules")) { - // Check if builtinModules is the only destructured property + if (containsBuiltinProperties(originalText)) { const properties = objectPattern.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern" - } + rule: { kind: "shorthand_property_identifier_pattern" } }); - const pairProperties = objectPattern.findAll({ - rule: { - kind: "pair_pattern" - } + rule: { kind: "pair_pattern" } }); - const hasOnlyBuiltinModules = (properties.length === 1 && properties[0].text() === "builtinModules") || - (properties.length === 0 && pairProperties.length === 1 && pairProperties[0].text().includes("builtinModules")); + // Check if only builtin properties are destructured + const isOnlyBuiltin = ( + properties.length === 1 && + ["builtinModules", "_builtinLibs"].includes(properties[0].text()) + ) || ( + properties.length === 0 && + pairProperties.length === 1 && + containsBuiltinProperties(pairProperties[0].text()) + ); + + if (isOnlyBuiltin) { + // Replace entire require statement + updateModuleSpecifier(statement); + + if (originalText.includes("_builtinLibs")) { + //edits.push(objectPattern.replace(originalText.replace("_builtinLibs", "builtinModules"))); + const newText = originalText.replace("_builtinLibs", "builtinModules"); + edits.push(objectPattern.replace(newText)); + replaceStandaloneBuiltinLibsReferences(); + } + hasChanges = true; + } else { + // Split into two statements + const propertiesToKeep = []; - if (hasOnlyBuiltinModules) { - // Case 2: Replace entire require statement - const moduleSpecifier = statement.find({ - rule: { - kind: "string" + for (const prop of properties) { + const propText = prop.text(); + if (propText !== "builtinModules" && propText !== "_builtinLibs") { + propertiesToKeep.push(propText); } - }); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; - edits.push(moduleSpecifier.replace(newModule)); - hasChanges = true; } - } else { - // Case 3: Split into two statements - const newText = originalText.replace(/,?\s*builtinModules\s*(:\s*\w+)?\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); - if (newText !== "{ }") { - // Get the parent variable declaration to replace the entire statement - const variableDeclaration = statement.parent(); + for (const prop of pairProperties) { + const propText = prop.text(); + if (!containsBuiltinProperties(propText)) { + propertiesToKeep.push(propText); + } + } - if (variableDeclaration && (variableDeclaration.kind() === "variable_declaration" || variableDeclaration.kind() === "lexical_declaration")) { - // Extract the alias if present - const aliasMatch = originalText.match(/builtinModules\s*:\s*(\w+)/); - const aliasText = aliasMatch ? `: ${aliasMatch[1]}` : ""; + if (propertiesToKeep.length > 0) { + const variableDeclaration = statement.parent(); + const moduleSpecifier = statement.find({ rule: { kind: "string" } }); - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); + if (variableDeclaration && moduleSpecifier) { + const currentModule = moduleSpecifier.text(); + const newModule = getNewModuleSpecifier(currentModule); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "node:module" : "module"; + const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*:\s*(\w+)/); + const aliasText = aliasMatch ? `: ${aliasMatch[2]}` : ""; - // Create the new statements - const firstStatement = `const ${newText} = require(${currentModule});`; - const secondStatement = `const { builtinModules${aliasText} } = require(${newModule.includes('node:') ? "'node:module'" : "'module'"});`; + const reconstructedText = `{ ${propertiesToKeep.join(", ")} }`; + const firstStatement = `const ${reconstructedText} = require(${currentModule});`; + const secondStatement = `const { builtinModules${aliasText} } = require(${newModule});`; + const replacementText = `${firstStatement}${EOL}${secondStatement}`; - // Replace the entire variable declaration with both statements - const replacementText = `${firstStatement}\n${secondStatement}`; + edits.push(variableDeclaration.replace(replacementText)); - edits.push(variableDeclaration.replace(replacementText)); - hasChanges = true; + if (originalText.includes("_builtinLibs") && !aliasText) { + replaceStandaloneBuiltinLibsReferences(); } - } else { - // Fallback to just replacing the object pattern - edits.push(objectPattern.replace(newText)); + hasChanges = true; } - } else { - // If we're removing the entire destructuring, we need to remove the whole statement - edits.push(statement.replace("")); } } } } else { - // Case 1: Handle namespace import like const repl = require('node:repl') - // Find usages of repl.builtinModules and replace with module.builtinModules - const variableDeclarator = statement.find({ - rule: { - kind: "variable_declarator" - } - }); + // Handle namespace require (const repl = require('node:repl')) + const variableDeclarator = statement.find({ rule: { kind: "variable_declarator" } }); if (variableDeclarator) { - const identifier = variableDeclarator.find({ - rule: { - kind: "identifier" - } - }); - - if (identifier) { - const varName = identifier.text(); - - // Find all member expressions using this variable with builtinModules - const memberExpressions = rootNode.findAll({ - rule: { - pattern: `${varName}.builtinModules` - } - }); - - if (memberExpressions.length > 0) { - // Replace variable name from repl to module - edits.push(identifier.replace("module")); - - // Replace all member expressions - for (const memberExpr of memberExpressions) { - edits.push(memberExpr.replace("module.builtinModules")); - } - - // Replace require statement to use module instead - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; - edits.push(moduleSpecifier.replace(newModule)); - hasChanges = true; - } - } - } + // Use resolveBindingPath to determine how builtinModules should be accessed + const builtinModulesPath = resolveBindingPath(variableDeclarator, "$.builtinModules"); + const builtinLibsPath = resolveBindingPath(variableDeclarator, "$._builtinLibs"); + + const usages = rootNode.findAll({ + rule: { + any: [ + { pattern: builtinModulesPath }, + { pattern: builtinLibsPath } + ] + } + }); + + if (usages.length > 0) { + const identifier = variableDeclarator.find({ + rule: { kind: "identifier" } + }); + + if (identifier) { + edits.push(identifier.replace("module")); + updateModuleSpecifier(statement); + + for (const memberExpr of usages) { + edits.push(memberExpr.replace("module.builtinModules")); + } + hasChanges = true; + } + } } } } @@ -167,144 +187,102 @@ export default function transform(root: SgRoot): string | null { const replImportStatements = getNodeImportStatements(root, "repl"); for (const statement of replImportStatements) { - // Handle named imports like: import { builtinModules } from 'node:repl' - const namedImports = statement.find({ - rule: { - kind: "named_imports" - } - }); + const namedImports = statement.find({ rule: { kind: "named_imports" } }); if (namedImports) { const originalText = namedImports.text(); - if (originalText.includes("builtinModules")) { - // Check if builtinModules is the only import + if (containsBuiltinProperties(originalText)) { const importSpecifiers = namedImports.findAll({ - rule: { - kind: "import_specifier" - } + rule: { kind: "import_specifier" } }); - const hasOnlyBuiltinModules = importSpecifiers.length === 1 && - importSpecifiers[0].text().includes("builtinModules"); + const isOnlyBuiltin = importSpecifiers.length === 1 && + containsBuiltinProperties(importSpecifiers[0].text()); - if (hasOnlyBuiltinModules) { - // Case 4: Replace entire import statement - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; - edits.push(moduleSpecifier.replace(newModule)); - hasChanges = true; + if (isOnlyBuiltin) { + // Replace entire import statement + updateModuleSpecifier(statement); + + if (originalText.includes("_builtinLibs")) { + const newText = originalText.replace("_builtinLibs", "builtinModules"); + edits.push(namedImports.replace(newText)); + replaceStandaloneBuiltinLibsReferences(); } + hasChanges = true; } else { - // Case 5: Split into two statements - const newText = originalText.replace(/,?\s*builtinModules\s*(:\s*\w+)?\s*,?/g, "").replace(/,\s*$/, "").replace(/^\s*,/, ""); + // Split into two statements + const newText = originalText + .replace(/,?\s*(builtinModules|_builtinLibs)\s*(as\s+\w+)?\s*,?/g, "") + .replace(/,\s*$/, "").replace(/^\s*,/, ""); edits.push(namedImports.replace(newText)); - // Add new module import statement - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); + const moduleSpecifier = statement.find({ rule: { kind: "string" } }); if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "node:module" : "module"; - - // Extract the alias if present - const aliasMatch = originalText.match(/builtinModules\s*(as\s+\w+)/); - const aliasText = aliasMatch ? ` ${aliasMatch[1]}` : ""; + const newModule = getNewModuleSpecifier(currentModule); - const newStatement = `import { builtinModules${aliasText} } from ${newModule.includes("node:") ? "'node:module'" : "'module'"};`; + const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*(as\s+\w+)/); + const aliasText = aliasMatch ? ` ${aliasMatch[2]}` : ""; - // Insert after current statement + const newStatement = `import { builtinModules${aliasText} } from ${newModule};`; const statementEnd = statement.range().end; edits.push({ startPos: statementEnd.index, endPos: statementEnd.index, - insertedText: `\n${newStatement}` + insertedText: `${EOL}${newStatement}` }); - hasChanges = true; - } - } - } - } - - // Handle default imports like: import repl from 'node:repl' - // Handle namespace imports like: import * as repl from 'node:repl' - if (!namedImports) { - // Look for default or namespace imports that use repl.builtinModules - const importClause = statement.find({ - rule: { - kind: "import_clause" - } - }); - if (importClause) { - let importIdentifier = null; - - // Check for default import - const defaultImport = importClause.find({ - rule: { - kind: "identifier" - } - }); - - // Check for namespace import - const namespaceImport = importClause.find({ - rule: { - kind: "namespace_import" - } - }); - - if (defaultImport) { - importIdentifier = defaultImport; - } else if (namespaceImport) { - const namespaceIdentifier = namespaceImport.find({ - rule: { - kind: "identifier" - } - }); - if (namespaceIdentifier) { - importIdentifier = namespaceIdentifier; - } - } - - if (importIdentifier) { - const varName = importIdentifier.text(); - - // Find all member expressions using this variable with builtinModules - const memberExpressions = rootNode.findAll({ - rule: { - pattern: `${varName}.builtinModules` - } - }); - - if (memberExpressions.length > 0) { - // Replace the import to use module instead - const moduleSpecifier = statement.find({ - rule: { - kind: "string" - } - }); - if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = currentModule.includes("node:") ? "'node:module'" : "'module'"; - edits.push(moduleSpecifier.replace(newModule)); - hasChanges = true; + if (originalText.includes("_builtinLibs") && !aliasText) { + replaceStandaloneBuiltinLibsReferences(); } + hasChanges = true; } } } + } else { + // Handle default/namespace imports + const importClause = statement.find({ rule: { kind: "import_clause" } }); + if (!importClause) continue; + + // Use resolveBindingPath to determine how builtinModules should be accessed + const builtinModulesPath = resolveBindingPath(importClause, "$.builtinModules"); + const builtinLibsPath = resolveBindingPath(importClause, "$._builtinLibs"); + + const expressions = rootNode.findAll({ + rule: { + any: [ + { pattern: builtinModulesPath }, + { pattern: builtinLibsPath } + ] + } + }); + + if (expressions.length > 0) { + updateModuleSpecifier(statement); + + // Get the namespace identifier to maintain consistency + let importIdentifier = importClause.find({ rule: { kind: "identifier" } }); + if (!importIdentifier) { + const namespaceImport = importClause.find({ + rule: { kind: "namespace_import" } + }); + + if (namespaceImport) { + importIdentifier = namespaceImport.find({ rule: { kind: "identifier" } }); + } + } + + const varName = importIdentifier?.text() || "module"; + for (const memberExpr of expressions) { + edits.push(memberExpr.replace(`${varName}.builtinModules`)); + } + hasChanges = true; + } } } - if (!hasChanges) return null; + if (!hasChanges) return null; return rootNode.commitEdits(edits); } diff --git a/recipes/repl-builtin-modules/tests/expected/file-1.js b/recipes/repl-builtin-modules/tests/expected/file-1.js index a86f7cd3..ed06d798 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-1.js +++ b/recipes/repl-builtin-modules/tests/expected/file-1.js @@ -1,3 +1,4 @@ const module = require('node:module'); console.log(module.builtinModules); +console.log(module.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-2.js b/recipes/repl-builtin-modules/tests/expected/file-2.js index a4a24266..9e801c95 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-2.js +++ b/recipes/repl-builtin-modules/tests/expected/file-2.js @@ -1,3 +1,5 @@ const { builtinModules } = require('node:module'); +const { builtinModules } = require('node:module'); console.log(builtinModules); +console.log(builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-4.js b/recipes/repl-builtin-modules/tests/expected/file-4.js index 2955c302..bac625ee 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-4.js +++ b/recipes/repl-builtin-modules/tests/expected/file-4.js @@ -1,6 +1,7 @@ const { foo } = require('node:repl'); const { builtinModules } = require('node:module'); +console.log(builtinModules); console.log(builtinModules); foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/expected/file-5.mjs b/recipes/repl-builtin-modules/tests/expected/file-5.mjs index 6a5e1764..970e5bc7 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-5.mjs +++ b/recipes/repl-builtin-modules/tests/expected/file-5.mjs @@ -1,6 +1,7 @@ import { foo } from 'node:repl'; import { builtinModules } from 'node:module'; +console.log(builtinModules); console.log(builtinModules); foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-1.js b/recipes/repl-builtin-modules/tests/input/file-1.js index cbc6687f..1234f9c7 100644 --- a/recipes/repl-builtin-modules/tests/input/file-1.js +++ b/recipes/repl-builtin-modules/tests/input/file-1.js @@ -1,3 +1,4 @@ const repl = require('node:repl'); console.log(repl.builtinModules); +console.log(repl._builtinLibs); diff --git a/recipes/repl-builtin-modules/tests/input/file-2.js b/recipes/repl-builtin-modules/tests/input/file-2.js index 3aab054f..a1994a4f 100644 --- a/recipes/repl-builtin-modules/tests/input/file-2.js +++ b/recipes/repl-builtin-modules/tests/input/file-2.js @@ -1,3 +1,5 @@ const { builtinModules } = require('node:repl'); +const { _builtinLibs } = require('node:repl'); console.log(builtinModules); +console.log(_builtinLibs); diff --git a/recipes/repl-builtin-modules/tests/input/file-4.js b/recipes/repl-builtin-modules/tests/input/file-4.js index 3ab65558..66742ecc 100644 --- a/recipes/repl-builtin-modules/tests/input/file-4.js +++ b/recipes/repl-builtin-modules/tests/input/file-4.js @@ -1,5 +1,6 @@ -const { builtinModules, foo } = require('node:repl'); +const { builtinModules, foo, _builtinLibs } = require('node:repl'); console.log(builtinModules); +console.log(_builtinLibs); foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-5.mjs b/recipes/repl-builtin-modules/tests/input/file-5.mjs index b0c03340..c7948e42 100644 --- a/recipes/repl-builtin-modules/tests/input/file-5.mjs +++ b/recipes/repl-builtin-modules/tests/input/file-5.mjs @@ -1,5 +1,6 @@ -import { builtinModules, foo } from 'node:repl'; +import { builtinModules, _builtinLibs, foo } from 'node:repl'; console.log(builtinModules); +console.log(_builtinLibs); foo(); // does something else From eba1b314e4ce346c36a8bc2838cc4b75c4974ffb Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:05:28 +0200 Subject: [PATCH 09/14] feat(`repl-builtin-modules`): improve --- recipes/repl-builtin-modules/README.md | 46 +--- recipes/repl-builtin-modules/src/workflow.ts | 212 ++++++++++-------- .../tests/expected/{file-5.mjs => file-5.js} | 4 +- .../tests/expected/file-6.mjs | 10 +- .../tests/expected/file-7.mjs | 6 +- .../tests/expected/file-8.mjs | 3 + .../tests/input/file-5.js | 6 + .../tests/input/file-5.mjs | 6 - .../tests/input/file-6.mjs | 9 +- .../tests/input/file-7.mjs | 6 +- .../tests/input/file-8.mjs | 3 + 11 files changed, 157 insertions(+), 154 deletions(-) rename recipes/repl-builtin-modules/tests/expected/{file-5.mjs => file-5.js} (50%) create mode 100644 recipes/repl-builtin-modules/tests/expected/file-8.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-5.js delete mode 100644 recipes/repl-builtin-modules/tests/input/file-5.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-8.mjs diff --git a/recipes/repl-builtin-modules/README.md b/recipes/repl-builtin-modules/README.md index c921f949..e33811b9 100644 --- a/recipes/repl-builtin-modules/README.md +++ b/recipes/repl-builtin-modules/README.md @@ -1,6 +1,6 @@ # `repl.builtinModules` DEP0191 -This recipe provides a guide for migrating from the deprecated `repl.builtinModules` to the new `module.builtinModules` property in Node.js. +This recipe provides a guide for migrating from the deprecated `repl.builtinModules` & `repl._builtinLibs` to the new `module.builtinModules` property in Node.js. See [DEP0191](https://nodejs.org/api/deprecations.html#DEP0191) and [DEP0142](https://nodejs.org/api/deprecations.html#DEP0142) @@ -8,56 +8,16 @@ See [DEP0191](https://nodejs.org/api/deprecations.html#DEP0191) and [DEP0142](ht **Before:** ```js -// Using require with namespace import -const repl = require('node:repl'); -console.log(repl.builtinModules); - -// Using require with destructuring -const { builtinModules } = require('node:repl'); - -// Using require with mixed destructuring -const { builtinModules, foo } = require('node:repl'); - -// Using ES6 import with named import -import { builtinModules } from 'node:repl'; - -// Using ES6 import with mixed named imports -import { builtinModules, foo } from 'node:repl'; - -// Using ES6 import with default import import repl from 'node:repl'; -console.log(repl.builtinModules); -// Using ES6 import with namespace import -import * as repl from 'node:repl'; console.log(repl.builtinModules); +console.log(repl._builtinLibs); ``` **After:** ```js -// Using require with namespace import -const module = require('node:module'); -console.log(module.builtinModules); - -// Using require with destructuring -const { builtinModules } = require('node:module'); - -// Using require with mixed destructuring -const { foo } = require('node:repl'); -const { builtinModules } = require('node:module'); - -// Using ES6 import with named import -import { builtinModules } from 'node:module'; - -// Using ES6 import with mixed named imports -import { foo } from 'node:repl'; -import { builtinModules } from 'node:module'; - -// Using ES6 import with default import import module from 'node:module'; -console.log(module.builtinModules); -// Using ES6 import with namespace import -import * as module from 'node:module'; +console.log(module.builtinModules); console.log(module.builtinModules); ``` diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 3e0288b8..c296abde 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -42,11 +42,11 @@ export default function transform(root: SgRoot): string | null { const parent = ref.parent(); if ( - parent - && parent.kind() !== "object_pattern" - && parent.kind() !== "member_expression" - && parent.kind() !== "named_imports" - ) { + parent + && parent.kind() !== "object_pattern" + && parent.kind() !== "member_expression" + && parent.kind() !== "named_imports" + ) { edits.push(ref.replace("builtinModules")); } } @@ -81,29 +81,33 @@ export default function transform(root: SgRoot): string | null { }); // Check if only builtin properties are destructured - const isOnlyBuiltin = ( - properties.length === 1 && - ["builtinModules", "_builtinLibs"].includes(properties[0].text()) - ) || ( - properties.length === 0 && - pairProperties.length === 1 && - containsBuiltinProperties(pairProperties[0].text()) - ); + const builtinShorthandCount = properties.filter(p => + ["builtinModules", "_builtinLibs"].includes(p.text()) + ).length; + + const builtinPairCount = pairProperties.filter(p => + containsBuiltinProperties(p.text()) + ).length; + + const totalBuiltinCount = builtinShorthandCount + builtinPairCount; + const totalCount = properties.length + pairProperties.length; + + const isOnlyBuiltin = totalCount > 0 && totalBuiltinCount === totalCount; if (isOnlyBuiltin) { // Replace entire require statement updateModuleSpecifier(statement); if (originalText.includes("_builtinLibs")) { - //edits.push(objectPattern.replace(originalText.replace("_builtinLibs", "builtinModules"))); - const newText = originalText.replace("_builtinLibs", "builtinModules"); - edits.push(objectPattern.replace(newText)); - replaceStandaloneBuiltinLibsReferences(); - } + const newText = originalText.replace("_builtinLibs", "builtinModules"); + edits.push(objectPattern.replace(newText)); + replaceStandaloneBuiltinLibsReferences(); + } hasChanges = true; } else { // Split into two statements const propertiesToKeep = []; + const builtinAliases = []; // Change to array to handle multiple aliases for (const prop of properties) { const propText = prop.text(); @@ -114,7 +118,16 @@ export default function transform(root: SgRoot): string | null { for (const prop of pairProperties) { const propText = prop.text(); - if (!containsBuiltinProperties(propText)) { + if (containsBuiltinProperties(propText)) { + // Extract alias from pair pattern like "builtinModules: alias" or "_builtinLibs: alias" + const keyNode = prop.find({ rule: { kind: "property_identifier" } }); + const valueNode = prop.find({ rule: { kind: "identifier" } }); + + if (keyNode && valueNode && + (keyNode.text() === "builtinModules" || keyNode.text() === "_builtinLibs")) { + builtinAliases.push(valueNode.text()); + } + } else { propertiesToKeep.push(propText); } } @@ -127,17 +140,33 @@ export default function transform(root: SgRoot): string | null { const currentModule = moduleSpecifier.text(); const newModule = getNewModuleSpecifier(currentModule); - const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*:\s*(\w+)/); - const aliasText = aliasMatch ? `: ${aliasMatch[2]}` : ""; - const reconstructedText = `{ ${propertiesToKeep.join(", ")} }`; const firstStatement = `const ${reconstructedText} = require(${currentModule});`; - const secondStatement = `const { builtinModules${aliasText} } = require(${newModule});`; + + // Always use builtinModules without alias for the new module import + const secondStatement = `const { builtinModules } = require(${newModule});`; const replacementText = `${firstStatement}${EOL}${secondStatement}`; edits.push(variableDeclaration.replace(replacementText)); - if (originalText.includes("_builtinLibs") && !aliasText) { + // Replace all alias references to use builtinModules + for (const alias of builtinAliases) { + const aliasReferences = rootNode.findAll({ + rule: { pattern: alias } + }); + + for (const ref of aliasReferences) { + const parent = ref.parent(); + if (parent && + parent.kind() !== "object_pattern" && + parent.kind() !== "pair_pattern" && + parent.kind() !== "variable_declarator") { + edits.push(ref.replace("builtinModules")); + } + } + } + + if (originalText.includes("_builtinLibs")) { replaceStandaloneBuiltinLibsReferences(); } hasChanges = true; @@ -150,34 +179,34 @@ export default function transform(root: SgRoot): string | null { const variableDeclarator = statement.find({ rule: { kind: "variable_declarator" } }); if (variableDeclarator) { - // Use resolveBindingPath to determine how builtinModules should be accessed - const builtinModulesPath = resolveBindingPath(variableDeclarator, "$.builtinModules"); - const builtinLibsPath = resolveBindingPath(variableDeclarator, "$._builtinLibs"); - - const usages = rootNode.findAll({ - rule: { - any: [ - { pattern: builtinModulesPath }, - { pattern: builtinLibsPath } - ] - } - }); - - if (usages.length > 0) { - const identifier = variableDeclarator.find({ - rule: { kind: "identifier" } - }); - - if (identifier) { - edits.push(identifier.replace("module")); - updateModuleSpecifier(statement); - - for (const memberExpr of usages) { - edits.push(memberExpr.replace("module.builtinModules")); - } - hasChanges = true; - } - } + // Use resolveBindingPath to determine how builtinModules should be accessed + const builtinModulesPath = resolveBindingPath(variableDeclarator, "$.builtinModules"); + const builtinLibsPath = resolveBindingPath(variableDeclarator, "$._builtinLibs"); + + const usages = rootNode.findAll({ + rule: { + any: [ + { pattern: builtinModulesPath }, + { pattern: builtinLibsPath } + ] + } + }); + + if (usages.length > 0) { + const identifier = variableDeclarator.find({ + rule: { kind: "identifier" } + }); + + if (identifier) { + edits.push(identifier.replace("module")); + updateModuleSpecifier(statement); + + for (const memberExpr of usages) { + edits.push(memberExpr.replace("module.builtinModules")); + } + hasChanges = true; + } + } } } } @@ -205,16 +234,17 @@ export default function transform(root: SgRoot): string | null { updateModuleSpecifier(statement); if (originalText.includes("_builtinLibs")) { - const newText = originalText.replace("_builtinLibs", "builtinModules"); - edits.push(namedImports.replace(newText)); + const newText = originalText.replace("_builtinLibs", "builtinModules"); + edits.push(namedImports.replace(newText)); replaceStandaloneBuiltinLibsReferences(); } hasChanges = true; } else { // Split into two statements const newText = originalText - .replace(/,?\s*(builtinModules|_builtinLibs)\s*(as\s+\w+)?\s*,?/g, "") - .replace(/,\s*$/, "").replace(/^\s*,/, ""); + .replace(/,?\s*(builtinModules|_builtinLibs)\s*(as\s+\w+)?\s*,?/g, "") + .replace(/,\s*$/, "") + .replace(/^\s*,/, ""); edits.push(namedImports.replace(newText)); const moduleSpecifier = statement.find({ rule: { kind: "string" } }); @@ -245,44 +275,44 @@ export default function transform(root: SgRoot): string | null { const importClause = statement.find({ rule: { kind: "import_clause" } }); if (!importClause) continue; - // Use resolveBindingPath to determine how builtinModules should be accessed - const builtinModulesPath = resolveBindingPath(importClause, "$.builtinModules"); - const builtinLibsPath = resolveBindingPath(importClause, "$._builtinLibs"); - - const expressions = rootNode.findAll({ - rule: { - any: [ - { pattern: builtinModulesPath }, - { pattern: builtinLibsPath } - ] - } - }); - - if (expressions.length > 0) { - updateModuleSpecifier(statement); - - // Get the namespace identifier to maintain consistency - let importIdentifier = importClause.find({ rule: { kind: "identifier" } }); - if (!importIdentifier) { - const namespaceImport = importClause.find({ - rule: { kind: "namespace_import" } - }); - - if (namespaceImport) { - importIdentifier = namespaceImport.find({ rule: { kind: "identifier" } }); - } - } - - const varName = importIdentifier?.text() || "module"; - for (const memberExpr of expressions) { - edits.push(memberExpr.replace(`${varName}.builtinModules`)); - } - hasChanges = true; - } + // Use resolveBindingPath to determine how builtinModules should be accessed + const builtinModulesPath = resolveBindingPath(importClause, "$.builtinModules"); + const builtinLibsPath = resolveBindingPath(importClause, "$._builtinLibs"); + + const expressions = rootNode.findAll({ + rule: { + any: [ + { pattern: builtinModulesPath }, + { pattern: builtinLibsPath } + ] + } + }); + + if (expressions.length > 0) { + updateModuleSpecifier(statement); + + // Get the namespace identifier to maintain consistency + let importIdentifier = importClause.find({ rule: { kind: "identifier" } }); + if (!importIdentifier) { + const namespaceImport = importClause.find({ + rule: { kind: "namespace_import" } + }); + + if (namespaceImport) { + importIdentifier = namespaceImport.find({ rule: { kind: "identifier" } }); + } + } + + const varName = importIdentifier?.text() || "module"; + for (const memberExpr of expressions) { + edits.push(memberExpr.replace(`${varName}.builtinModules`)); + } + hasChanges = true; + } } } - if (!hasChanges) return null; + if (!hasChanges) return null; return rootNode.commitEdits(edits); } diff --git a/recipes/repl-builtin-modules/tests/expected/file-5.mjs b/recipes/repl-builtin-modules/tests/expected/file-5.js similarity index 50% rename from recipes/repl-builtin-modules/tests/expected/file-5.mjs rename to recipes/repl-builtin-modules/tests/expected/file-5.js index 970e5bc7..bac625ee 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-5.mjs +++ b/recipes/repl-builtin-modules/tests/expected/file-5.js @@ -1,5 +1,5 @@ -import { foo } from 'node:repl'; -import { builtinModules } from 'node:module'; +const { foo } = require('node:repl'); +const { builtinModules } = require('node:module'); console.log(builtinModules); console.log(builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-6.mjs b/recipes/repl-builtin-modules/tests/expected/file-6.mjs index 5782c64d..970e5bc7 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-6.mjs +++ b/recipes/repl-builtin-modules/tests/expected/file-6.mjs @@ -1,5 +1,7 @@ -import repl from 'node:module'; -import * as nodeRepl from 'node:module'; +import { foo } from 'node:repl'; +import { builtinModules } from 'node:module'; -console.log(repl.builtinModules); -console.log(nodeRepl.builtinModules); +console.log(builtinModules); +console.log(builtinModules); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/expected/file-7.mjs b/recipes/repl-builtin-modules/tests/expected/file-7.mjs index fd67a170..5782c64d 100644 --- a/recipes/repl-builtin-modules/tests/expected/file-7.mjs +++ b/recipes/repl-builtin-modules/tests/expected/file-7.mjs @@ -1,3 +1,5 @@ -import { builtinModules as nodeBuiltinModules } from 'node:module'; +import repl from 'node:module'; +import * as nodeRepl from 'node:module'; -console.log(nodeBuiltinModules); +console.log(repl.builtinModules); +console.log(nodeRepl.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/expected/file-8.mjs b/recipes/repl-builtin-modules/tests/expected/file-8.mjs new file mode 100644 index 00000000..fd67a170 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-8.mjs @@ -0,0 +1,3 @@ +import { builtinModules as nodeBuiltinModules } from 'node:module'; + +console.log(nodeBuiltinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-5.js b/recipes/repl-builtin-modules/tests/input/file-5.js new file mode 100644 index 00000000..71c08aef --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-5.js @@ -0,0 +1,6 @@ +const { builtinModules: quez, foo, _builtinLibs: quux } = require('node:repl'); + +console.log(quez); +console.log(quux); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-5.mjs b/recipes/repl-builtin-modules/tests/input/file-5.mjs deleted file mode 100644 index c7948e42..00000000 --- a/recipes/repl-builtin-modules/tests/input/file-5.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { builtinModules, _builtinLibs, foo } from 'node:repl'; - -console.log(builtinModules); -console.log(_builtinLibs); - -foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-6.mjs b/recipes/repl-builtin-modules/tests/input/file-6.mjs index 29086500..c7948e42 100644 --- a/recipes/repl-builtin-modules/tests/input/file-6.mjs +++ b/recipes/repl-builtin-modules/tests/input/file-6.mjs @@ -1,5 +1,6 @@ -import repl from 'node:repl'; -import * as nodeRepl from 'node:repl'; +import { builtinModules, _builtinLibs, foo } from 'node:repl'; -console.log(repl.builtinModules); -console.log(nodeRepl.builtinModules); +console.log(builtinModules); +console.log(_builtinLibs); + +foo(); // does something else diff --git a/recipes/repl-builtin-modules/tests/input/file-7.mjs b/recipes/repl-builtin-modules/tests/input/file-7.mjs index d1cb4a7f..29086500 100644 --- a/recipes/repl-builtin-modules/tests/input/file-7.mjs +++ b/recipes/repl-builtin-modules/tests/input/file-7.mjs @@ -1,3 +1,5 @@ -import { builtinModules as nodeBuiltinModules } from 'node:repl'; +import repl from 'node:repl'; +import * as nodeRepl from 'node:repl'; -console.log(nodeBuiltinModules); +console.log(repl.builtinModules); +console.log(nodeRepl.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-8.mjs b/recipes/repl-builtin-modules/tests/input/file-8.mjs new file mode 100644 index 00000000..d1cb4a7f --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-8.mjs @@ -0,0 +1,3 @@ +import { builtinModules as nodeBuiltinModules } from 'node:repl'; + +console.log(nodeBuiltinModules); From 86a0cc3d74012d0dd94a04a2e62a5139d5c92a36 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:42:24 +0200 Subject: [PATCH 10/14] feat(`repl-builtin-modules`): remove `hasChanges` --- recipes/repl-builtin-modules/src/workflow.ts | 35 +++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index c296abde..f5d5d864 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -23,7 +23,6 @@ import type Js from "@codemod.com/jssg-types/langs/javascript"; */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); - let hasChanges = false; const edits: Edit[] = []; // Helper functions @@ -58,6 +57,7 @@ export default function transform(root: SgRoot): string | null { if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); const newModule = getNewModuleSpecifier(currentModule); + edits.push(moduleSpecifier.replace(newModule)); } }; @@ -103,7 +103,6 @@ export default function transform(root: SgRoot): string | null { edits.push(objectPattern.replace(newText)); replaceStandaloneBuiltinLibsReferences(); } - hasChanges = true; } else { // Split into two statements const propertiesToKeep = []; @@ -118,13 +117,20 @@ export default function transform(root: SgRoot): string | null { for (const prop of pairProperties) { const propText = prop.text(); - if (containsBuiltinProperties(propText)) { + + if (containsBuiltinProperties(propText)) { // Extract alias from pair pattern like "builtinModules: alias" or "_builtinLibs: alias" const keyNode = prop.find({ rule: { kind: "property_identifier" } }); const valueNode = prop.find({ rule: { kind: "identifier" } }); - if (keyNode && valueNode && - (keyNode.text() === "builtinModules" || keyNode.text() === "_builtinLibs")) { + if ( + keyNode && + valueNode && + ( + keyNode.text() === "builtinModules" || + keyNode.text() === "_builtinLibs" + ) + ) { builtinAliases.push(valueNode.text()); } } else { @@ -157,10 +163,13 @@ export default function transform(root: SgRoot): string | null { for (const ref of aliasReferences) { const parent = ref.parent(); - if (parent && + + if ( + parent && parent.kind() !== "object_pattern" && parent.kind() !== "pair_pattern" && - parent.kind() !== "variable_declarator") { + parent.kind() !== "variable_declarator" + ) { edits.push(ref.replace("builtinModules")); } } @@ -169,7 +178,6 @@ export default function transform(root: SgRoot): string | null { if (originalText.includes("_builtinLibs")) { replaceStandaloneBuiltinLibsReferences(); } - hasChanges = true; } } } @@ -179,7 +187,6 @@ export default function transform(root: SgRoot): string | null { const variableDeclarator = statement.find({ rule: { kind: "variable_declarator" } }); if (variableDeclarator) { - // Use resolveBindingPath to determine how builtinModules should be accessed const builtinModulesPath = resolveBindingPath(variableDeclarator, "$.builtinModules"); const builtinLibsPath = resolveBindingPath(variableDeclarator, "$._builtinLibs"); @@ -204,7 +211,6 @@ export default function transform(root: SgRoot): string | null { for (const memberExpr of usages) { edits.push(memberExpr.replace("module.builtinModules")); } - hasChanges = true; } } } @@ -238,7 +244,6 @@ export default function transform(root: SgRoot): string | null { edits.push(namedImports.replace(newText)); replaceStandaloneBuiltinLibsReferences(); } - hasChanges = true; } else { // Split into two statements const newText = originalText @@ -248,15 +253,15 @@ export default function transform(root: SgRoot): string | null { edits.push(namedImports.replace(newText)); const moduleSpecifier = statement.find({ rule: { kind: "string" } }); + if (moduleSpecifier) { const currentModule = moduleSpecifier.text(); const newModule = getNewModuleSpecifier(currentModule); - const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*(as\s+\w+)/); const aliasText = aliasMatch ? ` ${aliasMatch[2]}` : ""; - const newStatement = `import { builtinModules${aliasText} } from ${newModule};`; const statementEnd = statement.range().end; + edits.push({ startPos: statementEnd.index, endPos: statementEnd.index, @@ -266,7 +271,6 @@ export default function transform(root: SgRoot): string | null { if (originalText.includes("_builtinLibs") && !aliasText) { replaceStandaloneBuiltinLibsReferences(); } - hasChanges = true; } } } @@ -307,12 +311,11 @@ export default function transform(root: SgRoot): string | null { for (const memberExpr of expressions) { edits.push(memberExpr.replace(`${varName}.builtinModules`)); } - hasChanges = true; } } } - if (!hasChanges) return null; + if (!edits.length) return null; return rootNode.commitEdits(edits); } From 3929ed8a9929ed3a8a2e351d04264806cd773d59 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:09:54 +0200 Subject: [PATCH 11/14] update from feedback Co-Authored-By: Aviv Keller --- recipes/repl-builtin-modules/src/workflow.ts | 54 ++++++++------------ 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index f5d5d864..04f1061e 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -3,7 +3,9 @@ import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-s import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; -import type Js from "@codemod.com/jssg-types/langs/javascript"; + +const containsBuiltinProperties = (text: string): boolean => + text.includes("builtinModules") || text.includes("_builtinLibs"); /** * Transform function that converts deprecated repl.builtinModules and repl._builtinLibs @@ -21,17 +23,10 @@ import type Js from "@codemod.com/jssg-types/langs/javascript"; * 7. const { _builtinLibs } = require('node:repl'); → const { builtinModules } = require('node:module'); * 8. import { _builtinLibs } from 'node:repl'; → import { builtinModules } from 'node:module'; */ -export default function transform(root: SgRoot): string | null { +export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; - // Helper functions - const getNewModuleSpecifier = (currentModule: string): string => - currentModule.includes("node:") ? "'node:module'" : "'module'"; - - const containsBuiltinProperties = (text: string): boolean => - text.includes("builtinModules") || text.includes("_builtinLibs"); - const replaceStandaloneBuiltinLibsReferences = (): void => { const standaloneReferences = rootNode.findAll({ rule: { pattern: "_builtinLibs" } @@ -55,15 +50,13 @@ export default function transform(root: SgRoot): string | null { const moduleSpecifier = statement.find({ rule: { kind: "string" } }); if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = getNewModuleSpecifier(currentModule); - + // Always use 'node:module' + const newModule = "'node:module'"; edits.push(moduleSpecifier.replace(newModule)); } }; // Step 1: Handle require statements - // @ts-ignore - ast-grep types are not fully compatible with JSSG types const replRequireStatements = getNodeRequireCalls(root, "repl"); for (const statement of replRequireStatements) { @@ -80,10 +73,10 @@ export default function transform(root: SgRoot): string | null { rule: { kind: "pair_pattern" } }); - // Check if only builtin properties are destructured - const builtinShorthandCount = properties.filter(p => - ["builtinModules", "_builtinLibs"].includes(p.text()) - ).length; + // Check if only builtin properties are destructured + const builtinShorthandCount = properties.filter(p => + containsBuiltinProperties(p.text()) + ).length; const builtinPairCount = pairProperties.filter(p => containsBuiltinProperties(p.text()) @@ -105,8 +98,8 @@ export default function transform(root: SgRoot): string | null { } } else { // Split into two statements - const propertiesToKeep = []; - const builtinAliases = []; // Change to array to handle multiple aliases + const propertiesToKeep: string[] = []; + const builtinAliases: string[] = []; // Change to array to handle multiple aliases for (const prop of properties) { const propText = prop.text(); @@ -118,21 +111,18 @@ export default function transform(root: SgRoot): string | null { for (const prop of pairProperties) { const propText = prop.text(); - if (containsBuiltinProperties(propText)) { + if (containsBuiltinProperties(propText)) { // Extract alias from pair pattern like "builtinModules: alias" or "_builtinLibs: alias" const keyNode = prop.find({ rule: { kind: "property_identifier" } }); const valueNode = prop.find({ rule: { kind: "identifier" } }); - if ( + if ( keyNode && valueNode && - ( - keyNode.text() === "builtinModules" || - keyNode.text() === "_builtinLibs" - ) + containsBuiltinProperties(keyNode.text()) ) { - builtinAliases.push(valueNode.text()); - } + builtinAliases.push(valueNode.text()); + } } else { propertiesToKeep.push(propText); } @@ -144,7 +134,7 @@ export default function transform(root: SgRoot): string | null { if (variableDeclaration && moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = getNewModuleSpecifier(currentModule); + const newModule = "'node:module'"; const reconstructedText = `{ ${propertiesToKeep.join(", ")} }`; const firstStatement = `const ${reconstructedText} = require(${currentModule});`; @@ -165,11 +155,11 @@ export default function transform(root: SgRoot): string | null { const parent = ref.parent(); if ( - parent && + parent && parent.kind() !== "object_pattern" && parent.kind() !== "pair_pattern" && parent.kind() !== "variable_declarator" - ) { + ) { edits.push(ref.replace("builtinModules")); } } @@ -218,7 +208,6 @@ export default function transform(root: SgRoot): string | null { } // Step 2: Handle import statements - // @ts-ignore - ast-grep types are not fully compatible with JSSG types const replImportStatements = getNodeImportStatements(root, "repl"); for (const statement of replImportStatements) { @@ -255,8 +244,7 @@ export default function transform(root: SgRoot): string | null { const moduleSpecifier = statement.find({ rule: { kind: "string" } }); if (moduleSpecifier) { - const currentModule = moduleSpecifier.text(); - const newModule = getNewModuleSpecifier(currentModule); + const newModule = "'node:module'"; const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*(as\s+\w+)/); const aliasText = aliasMatch ? ` ${aliasMatch[2]}` : ""; const newStatement = `import { builtinModules${aliasText} } from ${newModule};`; From b61e726c6088cb35921ec7c770c528bb274187c8 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:12:59 +0200 Subject: [PATCH 12/14] Update workflow.ts --- recipes/repl-builtin-modules/src/workflow.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 04f1061e..7cf532f2 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -26,6 +26,8 @@ const containsBuiltinProperties = (text: string): boolean => export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; + // Reusable quoted module specifier for generating new statements + const newModule = "'node:module'"; const replaceStandaloneBuiltinLibsReferences = (): void => { const standaloneReferences = rootNode.findAll({ @@ -47,12 +49,11 @@ export default function transform(root: SgRoot): string | null { }; const updateModuleSpecifier = (statement: SgNode): void => { - const moduleSpecifier = statement.find({ rule: { kind: "string" } }); + const moduleSpecifier = statement.find({ rule: { kind: "string_fragment" } }); if (moduleSpecifier) { // Always use 'node:module' - const newModule = "'node:module'"; - edits.push(moduleSpecifier.replace(newModule)); + edits.push(moduleSpecifier.replace('node:module')); } }; @@ -134,7 +135,6 @@ export default function transform(root: SgRoot): string | null { if (variableDeclaration && moduleSpecifier) { const currentModule = moduleSpecifier.text(); - const newModule = "'node:module'"; const reconstructedText = `{ ${propertiesToKeep.join(", ")} }`; const firstStatement = `const ${reconstructedText} = require(${currentModule});`; @@ -244,7 +244,6 @@ export default function transform(root: SgRoot): string | null { const moduleSpecifier = statement.find({ rule: { kind: "string" } }); if (moduleSpecifier) { - const newModule = "'node:module'"; const aliasMatch = originalText.match(/(builtinModules|_builtinLibs)\s*(as\s+\w+)/); const aliasText = aliasMatch ? ` ${aliasMatch[2]}` : ""; const newStatement = `import { builtinModules${aliasText} } from ${newModule};`; From f21d7589b9b8ded81d43d8db2c927c2b35f59413 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:33:31 +0200 Subject: [PATCH 13/14] add dynamic import --- recipes/repl-builtin-modules/src/workflow.ts | 95 +++++++++++++++++++ .../tests/expected/file-9.mjs | 6 ++ .../tests/input/file-9.mjs | 6 ++ 3 files changed, 107 insertions(+) create mode 100644 recipes/repl-builtin-modules/tests/expected/file-9.mjs create mode 100644 recipes/repl-builtin-modules/tests/input/file-9.mjs diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 7cf532f2..011b5e64 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -302,6 +302,101 @@ export default function transform(root: SgRoot): string | null { } } + // Step 3: Handle dynamic imports + const dynamicImportDeclarators = rootNode.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "await_expression", + has: { + kind: "call_expression", + all: [ + { + has: { + field: "function", + kind: "import" + } + }, + { + has: { + field: "arguments", + kind: "arguments", + has: { + kind: "string", + has: { + kind: "string_fragment", + regex: "(node:)?repl$" + } + } + } + } + ] + } + } + } + }); + + for (const variableDeclarator of dynamicImportDeclarators) { + // Find the string fragment to replace + const stringFragment = variableDeclarator.find({ + rule: { + kind: "string_fragment", + regex: "(node:)?repl$" + } + }); + + if (stringFragment) { + edits.push(stringFragment.replace("node:module")); + } + + // Handle the variable assignment for dynamic imports + const objectPattern = variableDeclarator.find({ + rule: { kind: "object_pattern" } + }); + + if (objectPattern) { + // For cases like: const { builtinModules } = await import('node:repl'); + const originalText = objectPattern.text(); + if (containsBuiltinProperties(originalText)) { + if (originalText.includes("_builtinLibs")) { + const newText = originalText.replace("_builtinLibs", "builtinModules"); + edits.push(objectPattern.replace(newText)); + replaceStandaloneBuiltinLibsReferences(); + } + } + } else { + // For cases like: const repl = await import('node:repl'); + const identifier = variableDeclarator.find({ + rule: { kind: "identifier" } + }); + + if (identifier) { + const builtinModulesPath = resolveBindingPath(variableDeclarator, "$.builtinModules"); + const builtinLibsPath = resolveBindingPath(variableDeclarator, "$._builtinLibs"); + + const usages = rootNode.findAll({ + rule: { + any: [ + { pattern: builtinModulesPath }, + { pattern: builtinLibsPath } + ] + } + }); + + if (usages.length > 0) { + // Replace variable name from 'repl' to 'module' + edits.push(identifier.replace("module")); + + // Replace all usages to use builtinModules + for (const usage of usages) { + edits.push(usage.replace("module.builtinModules")); + } + } + } + } + } + if (!edits.length) return null; return rootNode.commitEdits(edits); diff --git a/recipes/repl-builtin-modules/tests/expected/file-9.mjs b/recipes/repl-builtin-modules/tests/expected/file-9.mjs new file mode 100644 index 00000000..9ca0454d --- /dev/null +++ b/recipes/repl-builtin-modules/tests/expected/file-9.mjs @@ -0,0 +1,6 @@ +const { builtinModules } = await import('node:module'); +const module = await import('node:module'); + +console.log(builtinModules); +console.log(module.builtinModules); +console.log(module.builtinModules); diff --git a/recipes/repl-builtin-modules/tests/input/file-9.mjs b/recipes/repl-builtin-modules/tests/input/file-9.mjs new file mode 100644 index 00000000..e860ca44 --- /dev/null +++ b/recipes/repl-builtin-modules/tests/input/file-9.mjs @@ -0,0 +1,6 @@ +const { builtinModules } = await import('node:repl'); +const repl = await import('node:repl'); + +console.log(builtinModules); +console.log(repl.builtinModules); +console.log(repl._builtinLibs); From 42c1df6f4e773ffd9ac65140f378ea59ee3bfef4 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:41:00 +0200 Subject: [PATCH 14/14] Update workflow.ts --- recipes/repl-builtin-modules/src/workflow.ts | 83 ++++++++++---------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/recipes/repl-builtin-modules/src/workflow.ts b/recipes/repl-builtin-modules/src/workflow.ts index 011b5e64..25c4a899 100644 --- a/recipes/repl-builtin-modules/src/workflow.ts +++ b/recipes/repl-builtin-modules/src/workflow.ts @@ -3,9 +3,7 @@ import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-s import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; - -const containsBuiltinProperties = (text: string): boolean => - text.includes("builtinModules") || text.includes("_builtinLibs"); +import type JS from "@codemod.com/jssg-types/langs/javascript"; /** * Transform function that converts deprecated repl.builtinModules and repl._builtinLibs @@ -23,40 +21,12 @@ const containsBuiltinProperties = (text: string): boolean => * 7. const { _builtinLibs } = require('node:repl'); → const { builtinModules } = require('node:module'); * 8. import { _builtinLibs } from 'node:repl'; → import { builtinModules } from 'node:module'; */ -export default function transform(root: SgRoot): string | null { +export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; // Reusable quoted module specifier for generating new statements const newModule = "'node:module'"; - const replaceStandaloneBuiltinLibsReferences = (): void => { - const standaloneReferences = rootNode.findAll({ - rule: { pattern: "_builtinLibs" } - }); - - for (const ref of standaloneReferences) { - const parent = ref.parent(); - - if ( - parent - && parent.kind() !== "object_pattern" - && parent.kind() !== "member_expression" - && parent.kind() !== "named_imports" - ) { - edits.push(ref.replace("builtinModules")); - } - } - }; - - const updateModuleSpecifier = (statement: SgNode): void => { - const moduleSpecifier = statement.find({ rule: { kind: "string_fragment" } }); - - if (moduleSpecifier) { - // Always use 'node:module' - edits.push(moduleSpecifier.replace('node:module')); - } - }; - // Step 1: Handle require statements const replRequireStatements = getNodeRequireCalls(root, "repl"); @@ -90,12 +60,12 @@ export default function transform(root: SgRoot): string | null { if (isOnlyBuiltin) { // Replace entire require statement - updateModuleSpecifier(statement); + updateModuleSpecifier(statement, edits); if (originalText.includes("_builtinLibs")) { const newText = originalText.replace("_builtinLibs", "builtinModules"); edits.push(objectPattern.replace(newText)); - replaceStandaloneBuiltinLibsReferences(); + replaceStandaloneBuiltinLibsReferences(rootNode, edits); } } else { // Split into two statements @@ -166,7 +136,7 @@ export default function transform(root: SgRoot): string | null { } if (originalText.includes("_builtinLibs")) { - replaceStandaloneBuiltinLibsReferences(); + replaceStandaloneBuiltinLibsReferences(rootNode, edits); } } } @@ -196,7 +166,7 @@ export default function transform(root: SgRoot): string | null { if (identifier) { edits.push(identifier.replace("module")); - updateModuleSpecifier(statement); + updateModuleSpecifier(statement, edits); for (const memberExpr of usages) { edits.push(memberExpr.replace("module.builtinModules")); @@ -226,12 +196,12 @@ export default function transform(root: SgRoot): string | null { if (isOnlyBuiltin) { // Replace entire import statement - updateModuleSpecifier(statement); + updateModuleSpecifier(statement, edits); if (originalText.includes("_builtinLibs")) { const newText = originalText.replace("_builtinLibs", "builtinModules"); edits.push(namedImports.replace(newText)); - replaceStandaloneBuiltinLibsReferences(); + replaceStandaloneBuiltinLibsReferences(rootNode, edits); } } else { // Split into two statements @@ -256,7 +226,7 @@ export default function transform(root: SgRoot): string | null { }); if (originalText.includes("_builtinLibs") && !aliasText) { - replaceStandaloneBuiltinLibsReferences(); + replaceStandaloneBuiltinLibsReferences(rootNode, edits); } } } @@ -280,7 +250,7 @@ export default function transform(root: SgRoot): string | null { }); if (expressions.length > 0) { - updateModuleSpecifier(statement); + updateModuleSpecifier(statement, edits); // Get the namespace identifier to maintain consistency let importIdentifier = importClause.find({ rule: { kind: "identifier" } }); @@ -362,7 +332,7 @@ export default function transform(root: SgRoot): string | null { if (originalText.includes("_builtinLibs")) { const newText = originalText.replace("_builtinLibs", "builtinModules"); edits.push(objectPattern.replace(newText)); - replaceStandaloneBuiltinLibsReferences(); + replaceStandaloneBuiltinLibsReferences(rootNode, edits); } } } else { @@ -401,3 +371,34 @@ export default function transform(root: SgRoot): string | null { return rootNode.commitEdits(edits); } + +const replaceStandaloneBuiltinLibsReferences = (rootNode: SgNode, edits: Edit[]): void => { + const standaloneReferences = rootNode.findAll({ + rule: { pattern: "_builtinLibs" } + }); + + for (const ref of standaloneReferences) { + const parent = ref.parent(); + + if ( + parent + && parent.kind() !== "object_pattern" + && parent.kind() !== "member_expression" + && parent.kind() !== "named_imports" + ) { + edits.push(ref.replace("builtinModules")); + } + } +}; + +const containsBuiltinProperties = (text: string): boolean => + text.includes("builtinModules") || text.includes("_builtinLibs"); + +const updateModuleSpecifier = (statement: SgNode, edits: Edit[]): void => { + const moduleSpecifier = statement.find({ rule: { kind: "string_fragment" } }); + + if (moduleSpecifier) { + // Always use 'node:module' + edits.push(moduleSpecifier.replace('node:module')); + } + };