diff --git a/recipes/cjs-to-esm/package.json b/recipes/cjs-to-esm/package.json index a08b90d8..d93fec60 100644 --- a/recipes/cjs-to-esm/package.json +++ b/recipes/cjs-to-esm/package.json @@ -4,7 +4,7 @@ "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": "node --run test:context-local-variable", "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", diff --git a/recipes/cjs-to-esm/src/context-local-variable-process.ts b/recipes/cjs-to-esm/src/context-local-variable-process.ts index 6b6706ae..3f6c396a 100644 --- a/recipes/cjs-to-esm/src/context-local-variable-process.ts +++ b/recipes/cjs-to-esm/src/context-local-variable-process.ts @@ -1,6 +1,33 @@ -import type { SgRoot, Edit } from '@codemod.com/jssg-types/main'; +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; import type JS from '@codemod.com/jssg-types/langs/javascript'; +/** + * Returns true when `__dirname` or `__filename` resolves to a local definition + * in the current file and should not be transformed. + * + * @example + * // true for: const __dirname = '/tmp'; + * isShadowedContextLocal(identifier, root) + * + * @example + * // true for: function fn(__filename) { return __filename; } + * isShadowedContextLocal(identifier, root) + * + * @example + * // false when using Node.js global `__dirname` with no local binding + * isShadowedContextLocal(identifier, root) + */ +const isShadowedContextLocal = ( + node: SgNode, + root: SgRoot, +): boolean => { + const definition = node.definition(); + + if (!definition || definition.kind !== 'local') return false; + + return definition.root.filename() === root.filename(); +}; + /** * @see https://github.com/nodejs/package-examples/blob/main/guide/05-cjs-esm-migration/migrating-context-local-variables/README.md */ @@ -8,7 +35,97 @@ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; - // do some stuff + const requireMainNodes = rootNode.findAll({ + rule: { + kind: 'member_expression', + has: { + field: 'object', + kind: 'identifier', + regex: '^require$', + }, + all: [ + { + has: { + field: 'property', + kind: 'property_identifier', + regex: '^main$', + }, + }, + ], + }, + }); + + for (const node of requireMainNodes) { + const result = node.find({ + rule: { + inside: { + kind: 'binary_expression', + has: { + kind: 'identifier', + regex: 'module', + }, + }, + }, + }); + + if (result) { + edits.push(result.replace('import.meta.main')); + } else { + edits.push(node.replace('import.meta.main')); + } + } + + const requireResolveNodes = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + all: [ + { + has: { + field: 'object', + kind: 'identifier', + regex: '^require$', + }, + }, + { + has: { + field: 'property', + kind: 'property_identifier', + regex: '^resolve$', + }, + }, + ], + }, + }, + }); + + for (const node of requireResolveNodes) { + const args = node.field('arguments'); + if (args) edits.push(node.replace(`import.meta.resolve${args.text()}`)); + } + + const contextLocalIdentifiers = rootNode.findAll({ + rule: { + kind: 'identifier', + regex: '^(__filename|__dirname)$', + }, + }); + + for (const identifier of contextLocalIdentifiers) { + if (isShadowedContextLocal(identifier, root)) continue; + const name = identifier.text(); + + switch (name) { + case '__dirname': + edits.push(identifier.replace('import.meta.dirname')); + break; + case '__filename': + edits.push(identifier.replace('import.meta.filename')); + break; + } + } if (!edits.length) return null; diff --git a/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/expected.js new file mode 100644 index 00000000..b495213e --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/expected.js @@ -0,0 +1,8 @@ +const __filename = '/tmp/file.js'; +const __dirname = '/tmp'; + +function print(__filename, __dirname) { + return `${__filename}::${__dirname}`; +} + +console.log(__filename, __dirname, print('a', 'b')); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/input.js b/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/input.js new file mode 100644 index 00000000..b495213e --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/all-shadowed-locals/input.js @@ -0,0 +1,8 @@ +const __filename = '/tmp/file.js'; +const __dirname = '/tmp'; + +function print(__filename, __dirname) { + return `${__filename}::${__dirname}`; +} + +console.log(__filename, __dirname, print('a', 'b')); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/already-esm/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/already-esm/expected.js new file mode 100644 index 00000000..0a02eebc --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/already-esm/expected.js @@ -0,0 +1,8 @@ +const info = { + filename: import.meta.filename, + dirname: import.meta.dirname, + isMain: import.meta.main, + resolved: import.meta.resolve('./foo.js'), +}; + +console.log(info); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/already-esm/input.js b/recipes/cjs-to-esm/tests/context-local-variable/already-esm/input.js new file mode 100644 index 00000000..0a02eebc --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/already-esm/input.js @@ -0,0 +1,8 @@ +const info = { + filename: import.meta.filename, + dirname: import.meta.dirname, + isMain: import.meta.main, + resolved: import.meta.resolve('./foo.js'), +}; + +console.log(info); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/basic/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/basic/expected.js new file mode 100644 index 00000000..69785d00 --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/basic/expected.js @@ -0,0 +1,8 @@ +const info = { + filename: import.meta.filename, + dirname: import.meta.dirname, + isMain: import.meta.main, + resolved: import.meta.resolve('./foo.js'), +}; + +console.log(info, import.meta.main); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/basic/input.js b/recipes/cjs-to-esm/tests/context-local-variable/basic/input.js new file mode 100644 index 00000000..1432433f --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/basic/input.js @@ -0,0 +1,8 @@ +const info = { + filename: __filename, + dirname: __dirname, + isMain: require.main === module, + resolved: require.resolve('./foo.js'), +}; + +console.log(info, require.main); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/expected.js new file mode 100644 index 00000000..37160fdb --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/expected.js @@ -0,0 +1,8 @@ +if (import.meta.main) { + boot(); +} + +const resolved = import.meta.resolve('./foo.js', { paths: [__dirname] }); +const filePath = `${import.meta.filename}`; + +console.log(import.meta.main, resolved, filePath); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/input.js b/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/input.js new file mode 100644 index 00000000..b3cbbdef --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/mixed-transformations/input.js @@ -0,0 +1,8 @@ +if (require.main === module) { + boot(); +} + +const resolved = require.resolve('./foo.js', { paths: [__dirname] }); +const filePath = `${__filename}`; + +console.log(require.main, resolved, filePath); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/expected.js new file mode 100644 index 00000000..31596592 --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/expected.js @@ -0,0 +1,2 @@ +const currentModule = module; +console.log(exports.value, currentModule.id); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/input.js b/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/input.js new file mode 100644 index 00000000..31596592 --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/module-exports-read/input.js @@ -0,0 +1,2 @@ +const currentModule = module; +console.log(exports.value, currentModule.id); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/expected.js new file mode 100644 index 00000000..e85b8df5 --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/expected.js @@ -0,0 +1,5 @@ +const shouldRun = Boolean(import.meta.main); + +if (shouldRun) { + run(); +} diff --git a/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/input.js b/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/input.js new file mode 100644 index 00000000..fd339cce --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/require-main-non-comparison/input.js @@ -0,0 +1,5 @@ +const shouldRun = Boolean(require.main); + +if (shouldRun) { + run(); +} diff --git a/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/expected.js new file mode 100644 index 00000000..c7fbf69d --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/expected.js @@ -0,0 +1,3 @@ +if (import.meta.main) { + start(); +} diff --git a/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/input.js b/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/input.js new file mode 100644 index 00000000..6b85c34f --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/reverse-comparison/input.js @@ -0,0 +1,3 @@ +if (module === require.main) { + start(); +} diff --git a/recipes/cjs-to-esm/tests/context-local-variable/shadowed/expected.js b/recipes/cjs-to-esm/tests/context-local-variable/shadowed/expected.js new file mode 100644 index 00000000..4afcfa69 --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/shadowed/expected.js @@ -0,0 +1,12 @@ +console.log(import.meta.dirname); + +function read(__filename) { + return __filename; +} + +{ + const __dirname = 'tmp'; + console.log(__dirname); +} + +console.log(import.meta.filename); diff --git a/recipes/cjs-to-esm/tests/context-local-variable/shadowed/input.js b/recipes/cjs-to-esm/tests/context-local-variable/shadowed/input.js new file mode 100644 index 00000000..ef1ae8fb --- /dev/null +++ b/recipes/cjs-to-esm/tests/context-local-variable/shadowed/input.js @@ -0,0 +1,12 @@ +console.log(__dirname); + +function read(__filename) { + return __filename; +} + +{ + const __dirname = 'tmp'; + console.log(__dirname); +} + +console.log(__filename);