From 981e743f95e3517acdd570f8ee9fe118837fe277 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:21:31 +0100 Subject: [PATCH 1/4] feat(`cjs-to-esm/package-json`): introduce --- package-lock.json | 77 ++++++- recipes/cjs-to-esm/package.json | 51 ++--- .../cjs-to-esm/src/package-json-process.ts | 194 +++++++++++++++++- .../01-type-missing-add-module/expected.json | 8 + .../01-type-missing-add-module/input.json | 7 + .../02-type-commonjs-replaced/expected.json | 7 + .../02-type-commonjs-replaced/input.json | 7 + .../expected.json | 8 + .../input.json | 8 + .../04-main-cjs-rewrite/expected.json | 8 + .../04-main-cjs-rewrite/input.json | 4 + .../05-types-d-cts-rewrite/expected.json | 8 + .../05-types-d-cts-rewrite/input.json | 4 + .../06-exports-nested-rewrites/expected.json | 22 ++ .../06-exports-nested-rewrites/input.json | 18 ++ .../07-imports-nested-rewrites/expected.json | 20 ++ .../07-imports-nested-rewrites/input.json | 16 ++ .../expected.json | 34 +++ .../input.json | 32 +++ .../tests/package-json/09-no-op/expected.json | 15 ++ .../tests/package-json/09-no-op/input.json | 15 ++ .../10-engines-merge-update/expected.json | 9 + .../10-engines-merge-update/input.json | 9 + 23 files changed, 554 insertions(+), 27 deletions(-) create mode 100644 recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/09-no-op/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/09-no-op/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/input.json diff --git a/package-lock.json b/package-lock.json index d01d5b70..9b23f6ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1933,6 +1933,80 @@ "win32" ] }, + "node_modules/@vltpkg/dep-id": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/dep-id/-/dep-id-1.0.0-rc.18.tgz", + "integrity": "sha512-vyK2MkF+MRnIjvalPHIGYL4fzEN9tM5e5FZitqV3smZdRf7pMK4rvsM0LAeGhnCbLtIACVmS5HR20TOZNtu51A==", + "license": "BSD-2-Clause-Patent", + "dependencies": { + "@vltpkg/error-cause": "1.0.0-rc.18", + "@vltpkg/spec": "1.0.0-rc.18", + "@vltpkg/types": "1.0.0-rc.18" + }, + "engines": { + "node": ">=22.9.0" + } + }, + "node_modules/@vltpkg/error-cause": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/error-cause/-/error-cause-1.0.0-rc.18.tgz", + "integrity": "sha512-Xil6WAHbqqUYtCLfvyh2BRPfj3Rkv2DZnUKK599KZWmDSbPbgYTO6X9naZY5x0LXiCqtJ0mfWGqvd5nAXHePjA==", + "license": "BSD-2-Clause-Patent", + "engines": { + "node": ">=22.9.0" + } + }, + "node_modules/@vltpkg/fast-split": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/fast-split/-/fast-split-1.0.0-rc.18.tgz", + "integrity": "sha512-3D5HTt8GpKuJVHpdgVBhuJToYFPmzTWqWaiNbcVqxE0FCGeg6YliRiXvHIpgezm5U3yHYIgzSn9zhPJBc17pBQ==", + "license": "BSD-2-Clause-Patent", + "engines": { + "node": ">=22.9.0" + } + }, + "node_modules/@vltpkg/semver": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/semver/-/semver-1.0.0-rc.18.tgz", + "integrity": "sha512-NsIhhef+6LOtjQXMp1ZGbEZlehELFjiO64Sd0/rn2UBrHr+Yxhgyit1JTTp/dhTytdgcIR2eK3RvRYzUgEkhRQ==", + "license": "BSD-2-Clause-Patent", + "dependencies": { + "@vltpkg/error-cause": "1.0.0-rc.18", + "@vltpkg/fast-split": "1.0.0-rc.18", + "@vltpkg/types": "1.0.0-rc.18" + }, + "engines": { + "node": ">=22.9.0" + } + }, + "node_modules/@vltpkg/spec": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/spec/-/spec-1.0.0-rc.18.tgz", + "integrity": "sha512-gdtOgaXAEzpV4hOcMnFjJZWW8pr2W8XISV+hokWtZmPUr4+DLOY3gOUwLVxiTMwAhzdSuVRTH8GPeh1QpzjvZg==", + "license": "BSD-2-Clause-Patent", + "dependencies": { + "@vltpkg/error-cause": "1.0.0-rc.18", + "@vltpkg/semver": "1.0.0-rc.18" + }, + "engines": { + "node": ">=22.9.0" + } + }, + "node_modules/@vltpkg/types": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@vltpkg/types/-/types-1.0.0-rc.18.tgz", + "integrity": "sha512-ntkJesdPYyVGZg1dUdFNDrsmVbqzd2LTxjHhXjIMmkGgA+qDDg3KeNSKJ1PU5Euuw81pO/oNFHQBEy+iPR2pzw==", + "license": "BSD-2-Clause-Patent", + "dependencies": { + "@vltpkg/dep-id": "1.0.0-rc.18", + "@vltpkg/error-cause": "1.0.0-rc.18", + "@vltpkg/semver": "1.0.0-rc.18", + "@vltpkg/spec": "1.0.0-rc.18" + }, + "engines": { + "node": ">=22.9.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4324,7 +4398,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@nodejs/codemod-utils": "*" + "@nodejs/codemod-utils": "*", + "@vltpkg/semver": "^1.0.0-rc.18" }, "devDependencies": { "@codemod.com/jssg-types": "^1.3.1" diff --git a/recipes/cjs-to-esm/package.json b/recipes/cjs-to-esm/package.json index a08b90d8..bc17c3e2 100644 --- a/recipes/cjs-to-esm/package.json +++ b/recipes/cjs-to-esm/package.json @@ -1,28 +1,29 @@ { "name": "@nodejs/cjs-to-esm", - "version": "1.0.0", - "description": "Codemod to assist CommonJS -> ESM migrations (imports, exports, package.json guidance)", - "type": "module", - "scripts": { - "test": "echo \"The test will be runned when implementation is done.\"", - "test:import": "npx codemod jssg test -l typescript ./src/import-process.ts ./tests/import", - "test:export": "npx codemod jssg test -l typescript ./src/export-process.ts ./tests/export", - "test:package-json": "npx codemod jssg test -l json ./src/package-json-process.ts ./tests/package-json", - "test:context-local-variable": "npx codemod jssg test -l typescript ./src/context-local-variable-process.ts ./tests/context-local-variable" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/nodejs/userland-migrations.git", - "directory": "recipes/cjs-to-esm", - "bugs": "https://github.com/nodejs/userland-migrations/issues" - }, - "author": "Augustin Mauroy", - "license": "MIT", - "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/cjs-to-esm/README.md", - "devDependencies": { - "@codemod.com/jssg-types": "^1.3.1" - }, - "dependencies": { - "@nodejs/codemod-utils": "*" - } + "version": "1.0.0", + "description": "Codemod to assist CommonJS -> ESM migrations (imports, exports, package.json guidance)", + "type": "module", + "scripts": { + "test": "echo \"The test will be runned when implementation is done.\"", + "test:import": "npx codemod jssg test -l typescript ./src/import-process.ts ./tests/import", + "test:export": "npx codemod jssg test -l typescript ./src/export-process.ts ./tests/export", + "test:package-json": "npx codemod jssg test -l json ./src/package-json-process.ts ./tests/package-json", + "test:context-local-variable": "npx codemod jssg test -l typescript ./src/context-local-variable-process.ts ./tests/context-local-variable" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/cjs-to-esm", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/cjs-to-esm/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + }, + "dependencies": { + "@nodejs/codemod-utils": "*", + "@vltpkg/semver": "^1.0.0-rc.18" + } } diff --git a/recipes/cjs-to-esm/src/package-json-process.ts b/recipes/cjs-to-esm/src/package-json-process.ts index 9d8288d8..e1ce05b7 100644 --- a/recipes/cjs-to-esm/src/package-json-process.ts +++ b/recipes/cjs-to-esm/src/package-json-process.ts @@ -1,5 +1,53 @@ import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; import type Json from '@codemod.com/jssg-types/langs/json'; +import { intersects, satisfies, validRange } from '@vltpkg/semver'; + +const REQUIRED_NODE_ENGINE = '^20.19.0 || >=22.12.0'; +const REQUIRED_NODE_ANCHORS = ['20.19.0', '20.20.0', '22.12.0', '24.0.0']; + +/** + * Given a current node engine range, returns the best next node engine range to use for an ESM package. + * If the current range is invalid, doesn't intersect with the required range, or doesn't cover all the required anchors, the required range will be returned. + * Otherwise, the current range will be returned. + * This allows to preserve custom node engine ranges as much as possible, while ensuring the resulting range is valid for ESM packages. + * @param current The current node engine range. + * @returns The best next node engine range to use for an ESM package. + */ +function chooseBestNodeEngine(current: unknown): string { + if (typeof current !== 'string' || !validRange(current)) { + return REQUIRED_NODE_ENGINE; + } + + if (current === REQUIRED_NODE_ENGINE) { + return current; + } + + if (!intersects(current, REQUIRED_NODE_ENGINE)) { + return REQUIRED_NODE_ENGINE; + } + + const coversRequiredAnchors = REQUIRED_NODE_ANCHORS.every((version) => + satisfies(version, current), + ); + + return coversRequiredAnchors ? REQUIRED_NODE_ENGINE : current; +} + +function appendPairToObject( + objectText: string, + pairSnippet: string, + pairIndent: string, + closingIndent: string, +): string { + if (objectText.trim() === '{}') { + return `{\n${pairIndent}${pairSnippet}\n${closingIndent}}`; + } + + return objectText.replace( + /\n([ \t]*)}$/, + `,\n${pairIndent}${pairSnippet}\n$1}`, + ); +} /** * @see https://github.com/nodejs/package-examples/tree/main/guide/05-cjs-esm-migration/migrating-package-json @@ -7,8 +55,152 @@ import type Json from '@codemod.com/jssg-types/langs/json'; export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; + const topLevelObject = rootNode.find({ + rule: { + kind: 'object', + }, + }); + + const typePariFeld = rootNode + .find({ + rule: { + kind: 'pair', + has: { + kind: 'string', + field: 'key', + has: { + kind: 'string_content', + regex: '^type$', + }, + }, + inside: { + kind: 'object', + }, + }, + }) + ?.field('value'); + const topLevelInsertions: string[] = []; + + if (typePariFeld && typePariFeld.text() !== '"module"') { + edits.push(typePariFeld.replace('"module"')); + } else if (!typePariFeld) { + topLevelInsertions.push('"type": "module"'); + } + + const enginesValueField = rootNode + .find({ + rule: { + kind: 'pair', + has: { + kind: 'string', + field: 'key', + has: { + kind: 'string_content', + regex: '^engines$', + }, + }, + inside: { + kind: 'object', + }, + }, + }) + ?.field('value'); + + if (!enginesValueField) { + topLevelInsertions.push( + `"engines": {\n "node": ${JSON.stringify(REQUIRED_NODE_ENGINE)}\n }`, + ); + } else if (enginesValueField.kind() !== 'object') { + edits.push( + enginesValueField.replace( + `{\n "node": ${JSON.stringify(REQUIRED_NODE_ENGINE)}\n }`, + ), + ); + } else { + const nodeEngineValueField = enginesValueField + .find({ + rule: { + kind: 'pair', + has: { + kind: 'string', + field: 'key', + has: { + kind: 'string_content', + regex: '^node$', + }, + }, + inside: { + kind: 'object', + }, + }, + }) + ?.field('value'); + + if (!nodeEngineValueField) { + edits.push( + enginesValueField.replace( + appendPairToObject( + enginesValueField.text(), + `"node": ${JSON.stringify(REQUIRED_NODE_ENGINE)}`, + ' ', + ' ', + ), + ), + ); + } else { + const currentLiteral = nodeEngineValueField + .find({ + rule: { + kind: 'string_content', + }, + }) + ?.text(); + const nextRange = chooseBestNodeEngine(currentLiteral); + + if (currentLiteral !== nextRange) { + edits.push(nodeEngineValueField.replace(JSON.stringify(nextRange))); + } + } + } + + if (topLevelObject && topLevelInsertions.length > 0) { + let nextTopLevelText = topLevelObject + .text() + .replace(/\.cjs(?=")/g, '.mjs') + .replace(/\.cts(?=")/g, '.mts'); + + for (const insertion of topLevelInsertions) { + nextTopLevelText = appendPairToObject( + nextTopLevelText, + insertion, + ' ', + '', + ); + } + edits.push(topLevelObject.replace(nextTopLevelText)); + } else { + const cjsStrings = rootNode.findAll({ + rule: { + kind: 'string_content', + regex: '\\.cjs$', + }, + }); + + for (const cjsString of cjsStrings) { + edits.push(cjsString.replace(cjsString.text().replace(/\.cjs$/, '.mjs'))); + } + + const ctsStrings = rootNode.findAll({ + rule: { + kind: 'string_content', + regex: '\\.cts$', + }, + }); - // do some stuff + for (const ctsString of ctsStrings) { + edits.push(ctsString.replace(ctsString.text().replace(/\.cts$/, '.mts'))); + } + } if (!edits.length) return null; diff --git a/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/expected.json b/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/expected.json new file mode 100644 index 00000000..825fc303 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-1", + "version": "1.0.0", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "type": "module" +} diff --git a/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/input.json b/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/input.json new file mode 100644 index 00000000..625f113b --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/01-type-missing-add-module/input.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-1", + "version": "1.0.0", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/expected.json b/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/expected.json new file mode 100644 index 00000000..81f951a1 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/expected.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-2", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/input.json b/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/input.json new file mode 100644 index 00000000..fd925498 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/02-type-commonjs-replaced/input.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-2", + "type": "commonjs", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/expected.json b/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/expected.json new file mode 100644 index 00000000..dce0e28f --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-3", + "type": "module", + "main": "./index.mjs", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/input.json b/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/input.json new file mode 100644 index 00000000..5ec50b33 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/03-type-already-module-other-migrations/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-3", + "type": "module", + "main": "./index.cjs", + "engines": { + "node": ">=18" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/expected.json b/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/expected.json new file mode 100644 index 00000000..79f0d55d --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-4", + "main": "./cli.mjs", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/input.json b/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/input.json new file mode 100644 index 00000000..99927fa4 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/04-main-cjs-rewrite/input.json @@ -0,0 +1,4 @@ +{ + "name": "fixture-4", + "main": "./cli.cjs" +} diff --git a/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/expected.json b/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/expected.json new file mode 100644 index 00000000..bcecfa4c --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-5", + "types": "./index.d.mts", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/input.json b/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/input.json new file mode 100644 index 00000000..8381a2b6 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/05-types-d-cts-rewrite/input.json @@ -0,0 +1,4 @@ +{ + "name": "fixture-5", + "types": "./index.d.cts" +} diff --git a/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/expected.json b/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/expected.json new file mode 100644 index 00000000..c886701c --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/expected.json @@ -0,0 +1,22 @@ +{ + "name": "fixture-6", + "exports": { + ".": { + "require": "./index.mjs", + "types": "./index.d.mts", + "nested": { + "array": [ + "./feature.mts", + { + "deep": "./deep.mjs" + }, + 1 + ] + } + } + }, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/input.json b/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/input.json new file mode 100644 index 00000000..b9d9d624 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/06-exports-nested-rewrites/input.json @@ -0,0 +1,18 @@ +{ + "name": "fixture-6", + "exports": { + ".": { + "require": "./index.cjs", + "types": "./index.d.cts", + "nested": { + "array": [ + "./feature.cts", + { + "deep": "./deep.cjs" + }, + 1 + ] + } + } + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/expected.json b/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/expected.json new file mode 100644 index 00000000..642effd9 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/expected.json @@ -0,0 +1,20 @@ +{ + "name": "fixture-7", + "imports": { + "#internal": { + "default": "./lib/internal.mjs", + "types": "./types/internal.d.mts", + "nested": [ + { + "dev": "./dev.mts" + }, + true, + "./already.mjs" + ] + } + }, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/input.json b/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/input.json new file mode 100644 index 00000000..9dc2507b --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/07-imports-nested-rewrites/input.json @@ -0,0 +1,16 @@ +{ + "name": "fixture-7", + "imports": { + "#internal": { + "default": "./lib/internal.cjs", + "types": "./types/internal.d.cts", + "nested": [ + { + "dev": "./dev.cts" + }, + true, + "./already.mjs" + ] + } + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/expected.json b/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/expected.json new file mode 100644 index 00000000..44222ed1 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/expected.json @@ -0,0 +1,34 @@ +{ + "name": "fixture-8", + "type": "module", + "main": "./entry.mts", + "types": "./index.d.mts", + "exports": { + ".": "./index.mjs", + "./feature": [ + "./feature.mjs", + { + "default": "./feature.mjs", + "metadata": { + "note": "do-not-change.cts.map" + } + }, + 42, + null + ] + }, + "imports": { + "#tooling": { + "node": "./tool.mjs", + "meta": [ + "./keep.js", + { + "flag": false + } + ] + } + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/input.json b/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/input.json new file mode 100644 index 00000000..99752996 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/08-mixed-values-partial-rewrites/input.json @@ -0,0 +1,32 @@ +{ + "name": "fixture-8", + "type": "commonjs", + "main": "./entry.cts", + "types": "./index.d.mts", + "exports": { + ".": "./index.mjs", + "./feature": [ + "./feature.cjs", + { + "default": "./feature.mjs", + "metadata": { + "note": "do-not-change.cts.map" + } + }, + 42, + null + ] + }, + "imports": { + "#tooling": { + "node": "./tool.cjs", + "meta": [ + "./keep.js", + { + "flag": false + } + ] + } + }, + "engines": "legacy" +} diff --git a/recipes/cjs-to-esm/tests/package-json/09-no-op/expected.json b/recipes/cjs-to-esm/tests/package-json/09-no-op/expected.json new file mode 100644 index 00000000..edd7bb90 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/09-no-op/expected.json @@ -0,0 +1,15 @@ +{ + "name": "fixture-9", + "type": "module", + "main": "./index.mjs", + "types": "./index.d.mts", + "exports": { + ".": "./index.mjs" + }, + "imports": { + "#x": "./x.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/09-no-op/input.json b/recipes/cjs-to-esm/tests/package-json/09-no-op/input.json new file mode 100644 index 00000000..edd7bb90 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/09-no-op/input.json @@ -0,0 +1,15 @@ +{ + "name": "fixture-9", + "type": "module", + "main": "./index.mjs", + "types": "./index.d.mts", + "exports": { + ".": "./index.mjs" + }, + "imports": { + "#x": "./x.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/expected.json b/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/expected.json new file mode 100644 index 00000000..6ff29f13 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/expected.json @@ -0,0 +1,9 @@ +{ + "name": "fixture-10", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10", + "pnpm": ">=9" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/input.json b/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/input.json new file mode 100644 index 00000000..3ebfdf0d --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/10-engines-merge-update/input.json @@ -0,0 +1,9 @@ +{ + "name": "fixture-10", + "type": "module", + "engines": { + "node": ">=18", + "npm": ">=10", + "pnpm": ">=9" + } +} From a2977cadfbc0ce77f34592c599403a760830060b Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:26:08 +0100 Subject: [PATCH 2/4] Update package.json --- recipes/cjs-to-esm/package.json | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/recipes/cjs-to-esm/package.json b/recipes/cjs-to-esm/package.json index bc17c3e2..42cd6524 100644 --- a/recipes/cjs-to-esm/package.json +++ b/recipes/cjs-to-esm/package.json @@ -1,29 +1,29 @@ { - "name": "@nodejs/cjs-to-esm", - "version": "1.0.0", - "description": "Codemod to assist CommonJS -> ESM migrations (imports, exports, package.json guidance)", - "type": "module", - "scripts": { - "test": "echo \"The test will be runned when implementation is done.\"", - "test:import": "npx codemod jssg test -l typescript ./src/import-process.ts ./tests/import", - "test:export": "npx codemod jssg test -l typescript ./src/export-process.ts ./tests/export", - "test:package-json": "npx codemod jssg test -l json ./src/package-json-process.ts ./tests/package-json", - "test:context-local-variable": "npx codemod jssg test -l typescript ./src/context-local-variable-process.ts ./tests/context-local-variable" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/nodejs/userland-migrations.git", - "directory": "recipes/cjs-to-esm", - "bugs": "https://github.com/nodejs/userland-migrations/issues" - }, - "author": "Augustin Mauroy", - "license": "MIT", - "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/cjs-to-esm/README.md", - "devDependencies": { - "@codemod.com/jssg-types": "^1.3.1" - }, - "dependencies": { - "@nodejs/codemod-utils": "*", - "@vltpkg/semver": "^1.0.0-rc.18" - } + "name": "@nodejs/cjs-to-esm", + "version": "1.0.0", + "description": "Codemod to assist CommonJS -> ESM migrations (imports, exports, package.json guidance)", + "type": "module", + "scripts": { + "test": "node --run test:package-json", + "test:import": "npx codemod jssg test -l typescript ./src/import-process.ts ./tests/import", + "test:export": "npx codemod jssg test -l typescript ./src/export-process.ts ./tests/export", + "test:package-json": "npx codemod jssg test -l json ./src/package-json-process.ts ./tests/package-json", + "test:context-local-variable": "npx codemod jssg test -l typescript ./src/context-local-variable-process.ts ./tests/context-local-variable" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/cjs-to-esm", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/cjs-to-esm/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + }, + "dependencies": { + "@nodejs/codemod-utils": "*", + "@vltpkg/semver": "^1.0.0-rc.18" + } } From 079f957394747042e5644192ee07c2f9d6b03789 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:31:24 +0100 Subject: [PATCH 3/4] add more tests cases --- .../package-json/11-engines-invalid-range/expected.json | 8 ++++++++ .../package-json/11-engines-invalid-range/input.json | 8 ++++++++ .../package-json/12-engines-node-non-string/expected.json | 8 ++++++++ .../package-json/12-engines-node-non-string/input.json | 8 ++++++++ .../package-json/13-engines-no-intersection/expected.json | 8 ++++++++ .../package-json/13-engines-no-intersection/input.json | 8 ++++++++ .../expected.json | 8 ++++++++ .../14-engines-intersecting-custom-preserved/input.json | 8 ++++++++ .../15-engines-broad-range-normalized/expected.json | 8 ++++++++ .../15-engines-broad-range-normalized/input.json | 8 ++++++++ 10 files changed, 80 insertions(+) create mode 100644 recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/input.json create mode 100644 recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/input.json diff --git a/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/expected.json b/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/expected.json new file mode 100644 index 00000000..d71252fc --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-11", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/input.json b/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/input.json new file mode 100644 index 00000000..8a86725f --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/11-engines-invalid-range/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-11", + "type": "module", + "engines": { + "node": "banana", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/expected.json b/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/expected.json new file mode 100644 index 00000000..08b5c4cb --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-12", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/input.json b/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/input.json new file mode 100644 index 00000000..5a417228 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/12-engines-node-non-string/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-12", + "type": "module", + "engines": { + "node": true, + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/expected.json b/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/expected.json new file mode 100644 index 00000000..d8c17123 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-13", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/input.json b/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/input.json new file mode 100644 index 00000000..fc460017 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/13-engines-no-intersection/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-13", + "type": "module", + "engines": { + "node": "<20.19.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/expected.json b/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/expected.json new file mode 100644 index 00000000..65c2527d --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-14", + "type": "module", + "engines": { + "node": ">=20.19.0 <24.0.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/input.json b/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/input.json new file mode 100644 index 00000000..65c2527d --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/14-engines-intersecting-custom-preserved/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-14", + "type": "module", + "engines": { + "node": ">=20.19.0 <24.0.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/expected.json b/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/expected.json new file mode 100644 index 00000000..cfc400b5 --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-15", + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/input.json b/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/input.json new file mode 100644 index 00000000..060f462f --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/15-engines-broad-range-normalized/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-15", + "type": "module", + "engines": { + "node": ">=20.19.0", + "npm": ">=10" + } +} From 756c8f68f6b66cd41b3f0908565090328fb465af Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:32:58 +0100 Subject: [PATCH 4/4] already higher --- .../package-json/16-engines-already-higher/expected.json | 8 ++++++++ .../package-json/16-engines-already-higher/input.json | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/expected.json create mode 100644 recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/input.json diff --git a/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/expected.json b/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/expected.json new file mode 100644 index 00000000..13f5fedf --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/expected.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-16", + "type": "module", + "engines": { + "node": ">=25", + "npm": ">=10" + } +} diff --git a/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/input.json b/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/input.json new file mode 100644 index 00000000..13f5fedf --- /dev/null +++ b/recipes/cjs-to-esm/tests/package-json/16-engines-already-higher/input.json @@ -0,0 +1,8 @@ +{ + "name": "fixture-16", + "type": "module", + "engines": { + "node": ">=25", + "npm": ">=10" + } +}