From 58b4af950d081664b4ca8de8c556aa8269b71a07 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Tue, 3 Mar 2026 18:26:48 +0000 Subject: [PATCH 01/15] feat: add mock-module-exports --- recipes/mock-module-exports/README.md | 4 + recipes/mock-module-exports/codemod.yaml | 21 ++++ recipes/mock-module-exports/package.json | 24 ++++ recipes/mock-module-exports/src/workflow.ts | 105 ++++++++++++++++++ .../tests/basic/expected.mjs | 8 ++ .../mock-module-exports/tests/basic/input.mjs | 8 ++ .../tests/without-default-export/expected.mjs | 7 ++ .../tests/without-default-export/input.mjs | 7 ++ .../tests/without-named-export/expected.mjs | 7 ++ .../tests/without-named-export/input.mjs | 5 + recipes/mock-module-exports/workflow.yaml | 25 +++++ 11 files changed, 221 insertions(+) create mode 100644 recipes/mock-module-exports/README.md create mode 100644 recipes/mock-module-exports/codemod.yaml create mode 100644 recipes/mock-module-exports/package.json create mode 100644 recipes/mock-module-exports/src/workflow.ts create mode 100644 recipes/mock-module-exports/tests/basic/expected.mjs create mode 100644 recipes/mock-module-exports/tests/basic/input.mjs create mode 100644 recipes/mock-module-exports/tests/without-default-export/expected.mjs create mode 100644 recipes/mock-module-exports/tests/without-default-export/input.mjs create mode 100644 recipes/mock-module-exports/tests/without-named-export/expected.mjs create mode 100644 recipes/mock-module-exports/tests/without-named-export/input.mjs create mode 100644 recipes/mock-module-exports/workflow.yaml diff --git a/recipes/mock-module-exports/README.md b/recipes/mock-module-exports/README.md new file mode 100644 index 00000000..69208808 --- /dev/null +++ b/recipes/mock-module-exports/README.md @@ -0,0 +1,4 @@ +# Mock Module Exports + +This codemod helps migrate code that uses mock.module. + diff --git a/recipes/mock-module-exports/codemod.yaml b/recipes/mock-module-exports/codemod.yaml new file mode 100644 index 00000000..e062f733 --- /dev/null +++ b/recipes/mock-module-exports/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/mock-module-exports" +version: 1.0.0 +description: "" +author: Bruno Rodrigues +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json new file mode 100644 index 00000000..15b78f43 --- /dev/null +++ b/recipes/mock-module-exports/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/mock-module-exports", + "version": "1.0.1", + "description": "", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/util-is", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Bruno Rodrigues", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/mock-module-exports/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + }, + "dependencies": { + "@nodejs/codemod-utils": "0.0.0" + } +} diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts new file mode 100644 index 00000000..0afca1d0 --- /dev/null +++ b/recipes/mock-module-exports/src/workflow.ts @@ -0,0 +1,105 @@ +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; +import type JS from '@codemod.com/jssg-types/langs/javascript'; +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { + detectIndentUnit, + getLineIndent, +} from '@nodejs/codemod-utils/ast-grep/indent'; +import { EOL } from 'node:os'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + const deps = getModuleDependencies(root, 'test'); + let moduleFnCalls: SgNode[] = []; + + for (const dep of deps) { + const moduleFn = resolveBindingPath(dep, '$.mock.module'); + + const fnCallNodes = rootNode.findAll<'call_expression'>({ + rule: { + kind: 'call_expression', + has: { + any: [ + { + kind: 'member_expression', + pattern: moduleFn, + }, + ], + // handle others + }, + }, + }); + + moduleFnCalls = moduleFnCalls.concat(fnCallNodes); + } + + for (const moduleFnCall of moduleFnCalls) { + const argumentsNode = moduleFnCall.field<'arguments'>('arguments'); + const args = argumentsNode.children().filter((node) => node.isNamed()); + + if (args.length < 2) continue; + const optionsArg = args[1]; + const indentUnit = detectIndentUnit(rootNode.text()); + const pairs = []; + + if (optionsArg.is('object')) { + const defaultExport = optionsArg.find<'pair'>({ + rule: { + kind: 'pair', + has: { + field: 'key', + kind: 'property_identifier', + regex: 'defaultExport', + }, + }, + }); + + if (defaultExport) { + pairs.push(`default: ${defaultExport?.field('value').text()}`); + } + + const namedExport = optionsArg.find<'pair'>({ + rule: { + kind: 'pair', + has: { + field: 'key', + kind: 'property_identifier', + regex: 'namedExport', + }, + }, + }); + + if (namedExport) { + for (const namedPair of namedExport.field('value').children()) { + if (!namedPair.is('pair')) continue; + pairs.push(namedPair.text()); + } + } + + const indentLevel = getLineIndent( + rootNode.text(), + optionsArg.range().start.index, + ); + + const exportsLevel = `${indentLevel}${indentUnit}`; + const innerExports = `${exportsLevel}${indentUnit}`; + + const newValue = + `{${EOL}` + + `${exportsLevel}exports: {${EOL}` + + `${innerExports}${pairs.join(`,${EOL}${innerExports}`)}` + + `,${EOL}` + + `${exportsLevel}},${EOL}` + + `${indentLevel}}`; + + edits.push(optionsArg.replace(newValue)); + } + } + + let sourceCode = rootNode.commitEdits(edits); + + return sourceCode; +} diff --git a/recipes/mock-module-exports/tests/basic/expected.mjs b/recipes/mock-module-exports/tests/basic/expected.mjs new file mode 100644 index 00000000..5eaa856f --- /dev/null +++ b/recipes/mock-module-exports/tests/basic/expected.mjs @@ -0,0 +1,8 @@ +import { mock } from 'node:test'; + +mock.module('example', { + exports: { + default: 'bar', + foo: 'foo', + }, +}); diff --git a/recipes/mock-module-exports/tests/basic/input.mjs b/recipes/mock-module-exports/tests/basic/input.mjs new file mode 100644 index 00000000..74a3d627 --- /dev/null +++ b/recipes/mock-module-exports/tests/basic/input.mjs @@ -0,0 +1,8 @@ +import { mock } from 'node:test'; + +mock.module('example', { + defaultExport: 'bar', + namedExports: { + foo: 'foo', + }, +}); diff --git a/recipes/mock-module-exports/tests/without-default-export/expected.mjs b/recipes/mock-module-exports/tests/without-default-export/expected.mjs new file mode 100644 index 00000000..59923414 --- /dev/null +++ b/recipes/mock-module-exports/tests/without-default-export/expected.mjs @@ -0,0 +1,7 @@ +import { mock } from 'node:test'; + +mock.module('example', { + exports: { + foo: 'foo', + }, +}); diff --git a/recipes/mock-module-exports/tests/without-default-export/input.mjs b/recipes/mock-module-exports/tests/without-default-export/input.mjs new file mode 100644 index 00000000..d82e8ced --- /dev/null +++ b/recipes/mock-module-exports/tests/without-default-export/input.mjs @@ -0,0 +1,7 @@ +import { mock } from 'node:test'; + +mock.module('example', { + namedExports: { + foo: 'foo', + }, +}); diff --git a/recipes/mock-module-exports/tests/without-named-export/expected.mjs b/recipes/mock-module-exports/tests/without-named-export/expected.mjs new file mode 100644 index 00000000..322cb8c8 --- /dev/null +++ b/recipes/mock-module-exports/tests/without-named-export/expected.mjs @@ -0,0 +1,7 @@ +import { mock } from 'node:test'; + +mock.module('example', { + exports: { + default: 'bar', + }, +}); diff --git a/recipes/mock-module-exports/tests/without-named-export/input.mjs b/recipes/mock-module-exports/tests/without-named-export/input.mjs new file mode 100644 index 00000000..10e46bdd --- /dev/null +++ b/recipes/mock-module-exports/tests/without-named-export/input.mjs @@ -0,0 +1,5 @@ +import { mock } from 'node:test'; + +mock.module('example', { + defaultExport: 'bar', +}); diff --git a/recipes/mock-module-exports/workflow.yaml b/recipes/mock-module-exports/workflow.yaml new file mode 100644 index 00000000..61c35e95 --- /dev/null +++ b/recipes/mock-module-exports/workflow.yaml @@ -0,0 +1,25 @@ +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: "" + 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 86f4fff7272ef88b7b1aad11fc9cb5ff04e7642c Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Tue, 3 Mar 2026 18:34:45 +0000 Subject: [PATCH 02/15] add extra test case --- recipes/mock-module-exports/src/workflow.ts | 12 +++++------- .../tests/esm-default-import/expected.mjs | 8 ++++++++ .../tests/esm-default-import/input.mjs | 8 ++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 recipes/mock-module-exports/tests/esm-default-import/expected.mjs create mode 100644 recipes/mock-module-exports/tests/esm-default-import/input.mjs diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 0afca1d0..4ddb40c3 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -28,7 +28,6 @@ export default function transform(root: SgRoot): string | null { pattern: moduleFn, }, ], - // handle others }, }, }); @@ -88,12 +87,11 @@ export default function transform(root: SgRoot): string | null { const innerExports = `${exportsLevel}${indentUnit}`; const newValue = - `{${EOL}` + - `${exportsLevel}exports: {${EOL}` + - `${innerExports}${pairs.join(`,${EOL}${innerExports}`)}` + - `,${EOL}` + - `${exportsLevel}},${EOL}` + - `${indentLevel}}`; + `{${EOL}` + + `${exportsLevel}exports: {${EOL}` + + `${innerExports}${pairs.join(`,${EOL}${innerExports}`)}` + `,${EOL}` + + `${exportsLevel}},${EOL}` + + `${indentLevel}}`; edits.push(optionsArg.replace(newValue)); } diff --git a/recipes/mock-module-exports/tests/esm-default-import/expected.mjs b/recipes/mock-module-exports/tests/esm-default-import/expected.mjs new file mode 100644 index 00000000..c46c9f9d --- /dev/null +++ b/recipes/mock-module-exports/tests/esm-default-import/expected.mjs @@ -0,0 +1,8 @@ +import test from 'node:test'; + +test.mock.module('example', { + exports: { + default: 'bar', + foo: 'foo', + }, +}); diff --git a/recipes/mock-module-exports/tests/esm-default-import/input.mjs b/recipes/mock-module-exports/tests/esm-default-import/input.mjs new file mode 100644 index 00000000..74149ec9 --- /dev/null +++ b/recipes/mock-module-exports/tests/esm-default-import/input.mjs @@ -0,0 +1,8 @@ +import test from 'node:test'; + +test.mock.module('example', { + defaultExport: 'bar', + namedExports: { + foo: 'foo', + }, +}); From 327691c0a4b303d9ff4367c07763d3f183e6c81c Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 13:54:49 +0000 Subject: [PATCH 03/15] feat: add support to replace variables --- recipes/mock-module-exports/package.json | 3 +- recipes/mock-module-exports/src/workflow.ts | 207 ++++++++++++++---- .../tests/option-as-variable/expected.mjs | 10 + .../tests/option-as-variable/input.mjs | 10 + 4 files changed, 181 insertions(+), 49 deletions(-) create mode 100644 recipes/mock-module-exports/tests/option-as-variable/expected.mjs create mode 100644 recipes/mock-module-exports/tests/option-as-variable/input.mjs diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index 15b78f43..bcbccc09 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -4,7 +4,8 @@ "description": "", "type": "module", "scripts": { - "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/" + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/", + "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"option-as-variable\"" }, "repository": { "type": "git", diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 4ddb40c3..825b89c8 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -1,4 +1,4 @@ -import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; +import type { SgRoot, Edit, SgNode, Kinds } from '@codemod.com/jssg-types/main'; import type JS from '@codemod.com/jssg-types/langs/javascript'; import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; @@ -8,6 +8,136 @@ import { } from '@nodejs/codemod-utils/ast-grep/indent'; import { EOL } from 'node:os'; +type QueueEvent = { + event: keyof typeof parsers; + handler: () => Edit[]; +}; + +const queue: QueueEvent[] = []; + +type Pair = { + before: SgNode; + after: string; +}; + +type ExportedValue = { + node: SgNode>; + default: Pair; + named: Pair[] | undefined; +}; +const exportedValues: Map = new Map(); + +type Parsers = { + parseOptions: (optionsNode: SgNode>) => undefined; + defaultExport: (node: SgNode>) => Edit[]; + resolveVariables: (node: SgNode>) => undefined; + namedExports: (optionsNode: SgNode>) => Edit[]; +}; + +const parsers: Parsers = { + parseOptions: (optionsNode: SgNode>): undefined => { + switch (optionsNode.kind()) { + case 'object': + queue.unshift({ + event: 'defaultExport', + handler: () => parsers.defaultExport(optionsNode), + }); + queue.unshift({ + event: 'namedExports', + handler: () => parsers.namedExports(optionsNode), + }); + case 'identifier': + queue.unshift({ + event: 'resolveVariables', + handler: () => parsers.resolveVariables(optionsNode), + }); + } + }, + resolveVariables: (node: SgNode>) => { + const definition = node.definition(); + if (!definition) return; + + switch (definition.node.parent().kind()) { + case 'variable_declarator': + const parent = definition.node.parent<'variable_declarator'>(); + queue.unshift({ + event: 'parseOptions', + handler: () => parsers.parseOptions(parent.field('value')), + }); + break; + default: + throw new Error('unhandled scenario'); + } + }, + defaultExport: (node: SgNode>): Edit[] => { + const edits: Edit[] = []; + const defaultExport = node.find<'pair'>({ + rule: { + kind: 'pair', + has: { + field: 'key', + kind: 'property_identifier', + regex: 'defaultExport', + }, + }, + }); + if (defaultExport) { + const change = { + before: defaultExport, + after: `default: ${defaultExport?.field('value').text()}`, + }; + + if (!exportedValues.has(node.id())) { + exportedValues.set(node.id(), { + node, + default: change, + named: [], + }); + return; + } + + const n = exportedValues.get(node.id()); + n.default = change; + } + return edits; + }, + namedExports: (node: SgNode>): Edit[] => { + const edits: Edit[] = []; + const namedExport = node.find<'pair'>({ + rule: { + kind: 'pair', + has: { + field: 'key', + kind: 'property_identifier', + regex: 'namedExport', + }, + }, + }); + + if (namedExport) { + if (!exportedValues.has(node.id())) { + exportedValues.set(node.id(), { + node, + default: undefined, + named: [], + }); + } + + const pairs = exportedValues.get(node.id()).named; + + for (const namedPair of namedExport.field('value').children()) { + if (!namedPair.is('pair')) continue; + + pairs.push({ + before: namedPair, + after: namedPair.text(), + }); + } + } + return edits; + }, +}; + export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; @@ -41,60 +171,41 @@ export default function transform(root: SgRoot): string | null { if (args.length < 2) continue; const optionsArg = args[1]; - const indentUnit = detectIndentUnit(rootNode.text()); - const pairs = []; - - if (optionsArg.is('object')) { - const defaultExport = optionsArg.find<'pair'>({ - rule: { - kind: 'pair', - has: { - field: 'key', - kind: 'property_identifier', - regex: 'defaultExport', - }, - }, - }); - - if (defaultExport) { - pairs.push(`default: ${defaultExport?.field('value').text()}`); - } + queue.unshift({ + event: 'parseOptions', + handler: () => parsers.parseOptions(optionsArg), + }); + } - const namedExport = optionsArg.find<'pair'>({ - rule: { - kind: 'pair', - has: { - field: 'key', - kind: 'property_identifier', - regex: 'namedExport', - }, - }, - }); + const indentUnit = detectIndentUnit(rootNode.text()); + while (queue.length) { + const event = queue.at(-1); + event.handler(); + queue.pop(); + } - if (namedExport) { - for (const namedPair of namedExport.field('value').children()) { - if (!namedPair.is('pair')) continue; - pairs.push(namedPair.text()); - } - } + for (const [_nodeId, change] of Array.from(exportedValues)) { + const indentLevel = getLineIndent( + rootNode.text(), + change.node.range().start.index, + ); - const indentLevel = getLineIndent( - rootNode.text(), - optionsArg.range().start.index, - ); + const exportsLevel = `${indentLevel}${indentUnit}`; + const innerExports = `${exportsLevel}${indentUnit}`; - const exportsLevel = `${indentLevel}${indentUnit}`; - const innerExports = `${exportsLevel}${indentUnit}`; + let newValue = `{${EOL}` + `${exportsLevel}exports: {${EOL}`; - const newValue = - `{${EOL}` - + `${exportsLevel}exports: {${EOL}` - + `${innerExports}${pairs.join(`,${EOL}${innerExports}`)}` + `,${EOL}` - + `${exportsLevel}},${EOL}` - + `${indentLevel}}`; + if (change.default?.after) { + newValue += `${innerExports}${change.default.after},${EOL}`; + } - edits.push(optionsArg.replace(newValue)); + if (change.named?.length) { + newValue += `${innerExports}${change.named.map((t) => t.after).join(`,${EOL}${innerExports}`)},${EOL}`; } + + newValue += `${exportsLevel}},${EOL}` + `${indentLevel}}`; + + edits.push(change.node.replace(newValue)); } let sourceCode = rootNode.commitEdits(edits); diff --git a/recipes/mock-module-exports/tests/option-as-variable/expected.mjs b/recipes/mock-module-exports/tests/option-as-variable/expected.mjs new file mode 100644 index 00000000..572a2e7b --- /dev/null +++ b/recipes/mock-module-exports/tests/option-as-variable/expected.mjs @@ -0,0 +1,10 @@ +import { mock } from 'node:test'; + +const mockValues = { + exports: { + default: 'bar', + foo: 'foo', + }, +} + +mock.module('example', mockValues); diff --git a/recipes/mock-module-exports/tests/option-as-variable/input.mjs b/recipes/mock-module-exports/tests/option-as-variable/input.mjs new file mode 100644 index 00000000..bb2f95ad --- /dev/null +++ b/recipes/mock-module-exports/tests/option-as-variable/input.mjs @@ -0,0 +1,10 @@ +import { mock } from 'node:test'; + +const mockValues = { + defaultExport: 'bar', + namedExports: { + foo: 'foo', + }, +} + +mock.module('example', mockValues); From 472a0d44e99b5e9d19d71627e72a644f8bd8f817 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 14:56:53 +0000 Subject: [PATCH 04/15] feat: add destructuring basic support --- recipes/mock-module-exports/package.json | 2 +- recipes/mock-module-exports/src/workflow.ts | 25 ++++++++++++++++++- .../tests/basic/expected.mjs | 1 + .../mock-module-exports/tests/basic/input.mjs | 1 + .../tests/destructuring-example/expected.mjs | 16 ++++++++++++ .../tests/destructuring-example/input.mjs | 16 ++++++++++++ .../tests/esm-default-import/expected.mjs | 7 ++++-- .../tests/esm-default-import/input.mjs | 7 ++++-- 8 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 recipes/mock-module-exports/tests/destructuring-example/expected.mjs create mode 100644 recipes/mock-module-exports/tests/destructuring-example/input.mjs diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index bcbccc09..0726da07 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/", - "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"option-as-variable\"" + "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"destru\"" }, "repository": { "type": "git", diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 825b89c8..fa7c0cb8 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -16,7 +16,7 @@ type QueueEvent = { const queue: QueueEvent[] = []; type Pair = { - before: SgNode; + before: SgNode | SgNode; after: string; }; @@ -32,6 +32,7 @@ type Parsers = { defaultExport: (node: SgNode>) => Edit[]; resolveVariables: (node: SgNode>) => undefined; namedExports: (optionsNode: SgNode>) => Edit[]; + spreadElements: (node: SgNode>) => undefined; }; const parsers: Parsers = { @@ -46,6 +47,10 @@ const parsers: Parsers = { event: 'namedExports', handler: () => parsers.namedExports(optionsNode), }); + queue.unshift({ + event: 'spreadElements', + handler: () => parsers.spreadElements(optionsNode), + }); case 'identifier': queue.unshift({ event: 'resolveVariables', @@ -136,6 +141,24 @@ const parsers: Parsers = { } return edits; }, + spreadElements: (node: SgNode>): undefined => { + const spreadElements = node.findAll<'spread_element'>({ + rule: { + kind: 'spread_element', + }, + }); + + if (spreadElements) { + const pairs = exportedValues.get(node.id()).named; + + for (const spread of spreadElements) { + pairs.push({ + before: spread, + after: spread.text(), + }); + } + } + }, }; export default function transform(root: SgRoot): string | null { diff --git a/recipes/mock-module-exports/tests/basic/expected.mjs b/recipes/mock-module-exports/tests/basic/expected.mjs index 5eaa856f..4dda4559 100644 --- a/recipes/mock-module-exports/tests/basic/expected.mjs +++ b/recipes/mock-module-exports/tests/basic/expected.mjs @@ -4,5 +4,6 @@ mock.module('example', { exports: { default: 'bar', foo: 'foo', + baz: 'baz', }, }); diff --git a/recipes/mock-module-exports/tests/basic/input.mjs b/recipes/mock-module-exports/tests/basic/input.mjs index 74a3d627..8e6b9df8 100644 --- a/recipes/mock-module-exports/tests/basic/input.mjs +++ b/recipes/mock-module-exports/tests/basic/input.mjs @@ -4,5 +4,6 @@ mock.module('example', { defaultExport: 'bar', namedExports: { foo: 'foo', + baz: 'baz', }, }); diff --git a/recipes/mock-module-exports/tests/destructuring-example/expected.mjs b/recipes/mock-module-exports/tests/destructuring-example/expected.mjs new file mode 100644 index 00000000..6ada2ee8 --- /dev/null +++ b/recipes/mock-module-exports/tests/destructuring-example/expected.mjs @@ -0,0 +1,16 @@ +import { mock } from 'node:test'; + +const foo = { + a: 'a', + b: 'b', + c: 'c' +} + +const mockValues = { + exports: { + default: 'bar', + ...foo, + }, +} + +mock.module('example', mockValues); diff --git a/recipes/mock-module-exports/tests/destructuring-example/input.mjs b/recipes/mock-module-exports/tests/destructuring-example/input.mjs new file mode 100644 index 00000000..63d1ccd1 --- /dev/null +++ b/recipes/mock-module-exports/tests/destructuring-example/input.mjs @@ -0,0 +1,16 @@ +import { mock } from 'node:test'; + +const foo = { + a: 'a', + b: 'b', + c: 'c' +} + +const mockValues = { + defaultExport: 'bar', + namedExports: { + ...foo + }, +} + +mock.module('example', mockValues); diff --git a/recipes/mock-module-exports/tests/esm-default-import/expected.mjs b/recipes/mock-module-exports/tests/esm-default-import/expected.mjs index c46c9f9d..0fb52d3b 100644 --- a/recipes/mock-module-exports/tests/esm-default-import/expected.mjs +++ b/recipes/mock-module-exports/tests/esm-default-import/expected.mjs @@ -1,8 +1,11 @@ import test from 'node:test'; +const bar = 'bar' +const foo = 'foo' + test.mock.module('example', { exports: { - default: 'bar', - foo: 'foo', + default: bar, + foo: foo, }, }); diff --git a/recipes/mock-module-exports/tests/esm-default-import/input.mjs b/recipes/mock-module-exports/tests/esm-default-import/input.mjs index 74149ec9..827272e2 100644 --- a/recipes/mock-module-exports/tests/esm-default-import/input.mjs +++ b/recipes/mock-module-exports/tests/esm-default-import/input.mjs @@ -1,8 +1,11 @@ import test from 'node:test'; +const bar = 'bar' +const foo = 'foo' + test.mock.module('example', { - defaultExport: 'bar', + defaultExport: bar, namedExports: { - foo: 'foo', + foo: foo, }, }); From 1c6c0eff6a5c699dcf0bc65f0701c9f5cd85d315 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 16:09:18 +0000 Subject: [PATCH 05/15] feat: support namedExports as a variable --- recipes/mock-module-exports/src/workflow.ts | 12 ++++++--- .../tests/basic/expected.mjs | 27 +++++++++++++++++++ .../mock-module-exports/tests/basic/input.mjs | 27 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index fa7c0cb8..5caf3234 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -107,7 +107,6 @@ const parsers: Parsers = { return edits; }, namedExports: (node: SgNode>): Edit[] => { - const edits: Edit[] = []; const namedExport = node.find<'pair'>({ rule: { kind: 'pair', @@ -130,7 +129,15 @@ const parsers: Parsers = { const pairs = exportedValues.get(node.id()).named; - for (const namedPair of namedExport.field('value').children()) { + const fieldValueNode = namedExport.field('value'); + + if (fieldValueNode.is('identifier')) { + pairs.push({ + before: namedExport, + after: `...(${fieldValueNode.text()} || {})`, + }); + } + for (const namedPair of fieldValueNode.children()) { if (!namedPair.is('pair')) continue; pairs.push({ @@ -139,7 +146,6 @@ const parsers: Parsers = { }); } } - return edits; }, spreadElements: (node: SgNode>): undefined => { const spreadElements = node.findAll<'spread_element'>({ diff --git a/recipes/mock-module-exports/tests/basic/expected.mjs b/recipes/mock-module-exports/tests/basic/expected.mjs index 4dda4559..b734377e 100644 --- a/recipes/mock-module-exports/tests/basic/expected.mjs +++ b/recipes/mock-module-exports/tests/basic/expected.mjs @@ -7,3 +7,30 @@ mock.module('example', { baz: 'baz', }, }); + +const namedExports = { + foo: 'foo', + baz: 'baz', +} + +mock.module('example2', { + exports: { + default: 'bar', + ...(namedExports || {}), + }, +}); + +function getDefault() { + return 'bar' +} + +function getBar() { + return 'bar' +} + +mock.module('example3', { + exports: { + default: getDefault(), + bar: getBar(), + }, +}); diff --git a/recipes/mock-module-exports/tests/basic/input.mjs b/recipes/mock-module-exports/tests/basic/input.mjs index 8e6b9df8..88f17d08 100644 --- a/recipes/mock-module-exports/tests/basic/input.mjs +++ b/recipes/mock-module-exports/tests/basic/input.mjs @@ -7,3 +7,30 @@ mock.module('example', { baz: 'baz', }, }); + +const namedExports = { + foo: 'foo', + baz: 'baz', +} + +mock.module('example2', { + defaultExport: 'bar', + namedExports: namedExports, +}); + +function getDefault() { + return 'bar' +} + +function getBar() { + return 'bar' +} + +mock.module('example3', { + exports: { + defaultExport: getDefault(), + namedExports: { + bar: getBar(), + } + }, +}); From d419a0a36fc88757c65d9225b4ee2cbe651752fa Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 16:22:10 +0000 Subject: [PATCH 06/15] feat: empty examples --- recipes/mock-module-exports/src/workflow.ts | 8 ++++++++ .../tests/basic/expected.mjs | 17 +++++++++++++++++ .../mock-module-exports/tests/basic/input.mjs | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 5caf3234..df22066c 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -155,6 +155,14 @@ const parsers: Parsers = { }); if (spreadElements) { + if (!exportedValues.has(node.id())) { + exportedValues.set(node.id(), { + node, + default: undefined, + named: [], + }); + } + const pairs = exportedValues.get(node.id()).named; for (const spread of spreadElements) { diff --git a/recipes/mock-module-exports/tests/basic/expected.mjs b/recipes/mock-module-exports/tests/basic/expected.mjs index b734377e..4361f0d5 100644 --- a/recipes/mock-module-exports/tests/basic/expected.mjs +++ b/recipes/mock-module-exports/tests/basic/expected.mjs @@ -34,3 +34,20 @@ mock.module('example3', { bar: getBar(), }, }); + +mock.module('example-no-named', { + exports: { + default: getDefault(), + }, +}); + +mock.module('example-no-fault', { + exports: { + bar: 'bar', + }, +}); + +mock.module('example-empty', { + exports: { + }, +}); diff --git a/recipes/mock-module-exports/tests/basic/input.mjs b/recipes/mock-module-exports/tests/basic/input.mjs index 88f17d08..2f4c1d5f 100644 --- a/recipes/mock-module-exports/tests/basic/input.mjs +++ b/recipes/mock-module-exports/tests/basic/input.mjs @@ -34,3 +34,22 @@ mock.module('example3', { } }, }); + +mock.module('example-no-named', { + exports: { + defaultExport: getDefault(), + }, +}); + +mock.module('example-no-fault', { + exports: { + namedExports: { + bar: 'bar', + } + }, +}); + +mock.module('example-empty', { + exports: { + }, +}); From 2ea22ebf82a8a9f00f152b2e9796ebc42452c7d3 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 22:35:00 +0000 Subject: [PATCH 07/15] feat: add support when a fn return values --- recipes/mock-module-exports/package.json | 2 +- recipes/mock-module-exports/src/workflow.ts | 45 +++++++++++++++---- .../tests/fn-return-example/expected.mjs | 18 ++++++++ .../tests/fn-return-example/input.mjs | 18 ++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 recipes/mock-module-exports/tests/fn-return-example/expected.mjs create mode 100644 recipes/mock-module-exports/tests/fn-return-example/input.mjs diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index 0726da07..fbdb0800 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/", - "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"destru\"" + "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"variable\"" }, "repository": { "type": "git", diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index df22066c..1887ab4c 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -10,7 +10,7 @@ import { EOL } from 'node:os'; type QueueEvent = { event: keyof typeof parsers; - handler: () => Edit[]; + handler: () => void; }; const queue: QueueEvent[] = []; @@ -28,15 +28,15 @@ type ExportedValue = { const exportedValues: Map = new Map(); type Parsers = { - parseOptions: (optionsNode: SgNode>) => undefined; - defaultExport: (node: SgNode>) => Edit[]; - resolveVariables: (node: SgNode>) => undefined; - namedExports: (optionsNode: SgNode>) => Edit[]; - spreadElements: (node: SgNode>) => undefined; + parseOptions: (optionsNode: SgNode>) => void; + defaultExport: (node: SgNode>) => void; + resolveVariables: (node: SgNode>) => void; + namedExports: (optionsNode: SgNode>) => void; + spreadElements: (node: SgNode>) => void; }; const parsers: Parsers = { - parseOptions: (optionsNode: SgNode>): undefined => { + parseOptions: (optionsNode: SgNode>) => { switch (optionsNode.kind()) { case 'object': queue.unshift({ @@ -51,11 +51,20 @@ const parsers: Parsers = { event: 'spreadElements', handler: () => parsers.spreadElements(optionsNode), }); + break; case 'identifier': queue.unshift({ event: 'resolveVariables', handler: () => parsers.resolveVariables(optionsNode), }); + break; + case 'call_expression': + queue.unshift({ + event: 'resolveVariables', + handler: () => + parsers.resolveVariables(optionsNode.field('function')), + }); + break; } }, resolveVariables: (node: SgNode>) => { @@ -70,6 +79,26 @@ const parsers: Parsers = { handler: () => parsers.parseOptions(parent.field('value')), }); break; + case 'function_declaration': + const fnDeclaration = definition.node.parent<'variable_declarator'>(); + + const returns = fnDeclaration + .findAll<'return_statement'>({ + rule: { + kind: 'return_statement', + }, + }) + .map((n) => n.child(1)); + + for (const ret of returns) { + if (!ret) continue; + + queue.unshift({ + event: 'parseOptions', + handler: () => parsers.parseOptions(ret), + }); + } + break; default: throw new Error('unhandled scenario'); } @@ -106,7 +135,7 @@ const parsers: Parsers = { } return edits; }, - namedExports: (node: SgNode>): Edit[] => { + namedExports: (node: SgNode>) => { const namedExport = node.find<'pair'>({ rule: { kind: 'pair', diff --git a/recipes/mock-module-exports/tests/fn-return-example/expected.mjs b/recipes/mock-module-exports/tests/fn-return-example/expected.mjs new file mode 100644 index 00000000..abf124a1 --- /dev/null +++ b/recipes/mock-module-exports/tests/fn-return-example/expected.mjs @@ -0,0 +1,18 @@ +import { mock } from 'node:test'; + +const foo = { + a: 'a', + b: 'b', + c: 'c' +} + +function mockValues() { + return { + exports: { + default: 'bar', + ...foo, + }, + } +} + +mock.module('example', mockValues()); diff --git a/recipes/mock-module-exports/tests/fn-return-example/input.mjs b/recipes/mock-module-exports/tests/fn-return-example/input.mjs new file mode 100644 index 00000000..0ad9e863 --- /dev/null +++ b/recipes/mock-module-exports/tests/fn-return-example/input.mjs @@ -0,0 +1,18 @@ +import { mock } from 'node:test'; + +const foo = { + a: 'a', + b: 'b', + c: 'c' +} + +function mockValues() { + return { + defaultExport: 'bar', + namedExports: { + ...foo, + }, + } +} + +mock.module('example', mockValues()); From 4298c5a8a74a948855cbaf8bacf87edde50e5387 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 22:46:26 +0000 Subject: [PATCH 08/15] docs --- recipes/mock-module-exports/README.md | 17 ++++++++++++++++- recipes/mock-module-exports/codemod.yaml | 2 +- recipes/mock-module-exports/package.json | 5 ++--- recipes/mock-module-exports/workflow.yaml | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/recipes/mock-module-exports/README.md b/recipes/mock-module-exports/README.md index 69208808..633e9e5f 100644 --- a/recipes/mock-module-exports/README.md +++ b/recipes/mock-module-exports/README.md @@ -1,4 +1,19 @@ # Mock Module Exports -This codemod helps migrate code that uses mock.module. +This migration trasforming use of deprecated `options.defaultExport` and `options.namedExports` on +`node:test.mock` +## Example + +```diff +mock.module('…', { +- defaultExport: …, +- namedExports: { +- foo: … +- }, ++ exports: { ++ default: …, ++ foo: …, ++ }, +}); +``` diff --git a/recipes/mock-module-exports/codemod.yaml b/recipes/mock-module-exports/codemod.yaml index e062f733..5643f3b6 100644 --- a/recipes/mock-module-exports/codemod.yaml +++ b/recipes/mock-module-exports/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "@nodejs/mock-module-exports" version: 1.0.0 -description: "" +description: "Handle mock.module exports deprecation" author: Bruno Rodrigues license: MIT workflow: workflow.yaml diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index fbdb0800..9c2424dd 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -1,11 +1,10 @@ { "name": "@nodejs/mock-module-exports", - "version": "1.0.1", - "description": "", + "version": "1.0.0", + "description": "Handle mock.module exports deprecation", "type": "module", "scripts": { "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/", - "testu": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/ --filter \"variable\"" }, "repository": { "type": "git", diff --git a/recipes/mock-module-exports/workflow.yaml b/recipes/mock-module-exports/workflow.yaml index 61c35e95..80809110 100644 --- a/recipes/mock-module-exports/workflow.yaml +++ b/recipes/mock-module-exports/workflow.yaml @@ -7,7 +7,7 @@ nodes: runtime: type: direct steps: - - name: "" + - name: Handle mock.module exports deprecation js-ast-grep: js_file: src/workflow.ts base_path: . From 5b15a7909f5f350d59683d04353f236541edcc9d Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 22:47:48 +0000 Subject: [PATCH 09/15] docs --- recipes/mock-module-exports/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index 9c2424dd..a75a220b 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -4,7 +4,7 @@ "description": "Handle mock.module exports deprecation", "type": "module", "scripts": { - "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/", + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/" }, "repository": { "type": "git", From 462774723d37cc34e5176597155191f71bfea22b Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 22:50:35 +0000 Subject: [PATCH 10/15] npm install --- package-lock.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index bce2a02a..b9372ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1527,6 +1527,10 @@ "resolved": "recipes/import-assertions-to-attributes", "link": true }, + "node_modules/@nodejs/mock-module-exports": { + "resolved": "recipes/mock-module-exports", + "link": true + }, "node_modules/@nodejs/node-url-to-whatwg-url": { "resolved": "recipes/node-url-to-whatwg-url", "link": true @@ -4438,6 +4442,17 @@ "@codemod.com/jssg-types": "^1.5.0" } }, + "recipes/mock-module-exports": { + "name": "@nodejs/mock-module-exports", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "0.0.0" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.3.1" + } + }, "recipes/node-url-to-whatwg-url": { "name": "@nodejs/node-url-to-whatwg-url", "version": "1.0.1", From 21f69b28f32890e21b71f7c91c749d0d872caac0 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 14 Mar 2026 22:52:29 +0000 Subject: [PATCH 11/15] lint --- recipes/mock-module-exports/src/workflow.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 1887ab4c..cfdef0ae 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -72,14 +72,15 @@ const parsers: Parsers = { if (!definition) return; switch (definition.node.parent().kind()) { - case 'variable_declarator': + case 'variable_declarator': { const parent = definition.node.parent<'variable_declarator'>(); queue.unshift({ event: 'parseOptions', handler: () => parsers.parseOptions(parent.field('value')), }); break; - case 'function_declaration': + } + case 'function_declaration': { const fnDeclaration = definition.node.parent<'variable_declarator'>(); const returns = fnDeclaration @@ -99,6 +100,7 @@ const parsers: Parsers = { }); } break; + } default: throw new Error('unhandled scenario'); } @@ -274,7 +276,7 @@ export default function transform(root: SgRoot): string | null { edits.push(change.node.replace(newValue)); } - let sourceCode = rootNode.commitEdits(edits); + const sourceCode = rootNode.commitEdits(edits); return sourceCode; } From 2267171224d91b3f0e242d530dbd783d126ce890 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 15 Mar 2026 21:33:54 +0000 Subject: [PATCH 12/15] Group unshift on queue, and make ifs more readable Co-authored-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- recipes/mock-module-exports/src/workflow.ts | 63 ++++++++++----------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index cfdef0ae..bb49a610 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -27,30 +27,24 @@ type ExportedValue = { }; const exportedValues: Map = new Map(); -type Parsers = { - parseOptions: (optionsNode: SgNode>) => void; - defaultExport: (node: SgNode>) => void; - resolveVariables: (node: SgNode>) => void; - namedExports: (optionsNode: SgNode>) => void; - spreadElements: (node: SgNode>) => void; -}; - -const parsers: Parsers = { +const parsers = { parseOptions: (optionsNode: SgNode>) => { switch (optionsNode.kind()) { case 'object': - queue.unshift({ - event: 'defaultExport', - handler: () => parsers.defaultExport(optionsNode), - }); - queue.unshift({ - event: 'namedExports', - handler: () => parsers.namedExports(optionsNode), - }); - queue.unshift({ - event: 'spreadElements', - handler: () => parsers.spreadElements(optionsNode), - }); + queue.unshift( + { + event: 'defaultExport', + handler: () => parsers.defaultExport(optionsNode), + }, + { + event: 'namedExports', + handler: () => parsers.namedExports(optionsNode), + }, + { + event: 'spreadElements', + handler: () => parsers.spreadElements(optionsNode), + }, + ); break; case 'identifier': queue.unshift({ @@ -92,13 +86,14 @@ const parsers: Parsers = { .map((n) => n.child(1)); for (const ret of returns) { - if (!ret) continue; - - queue.unshift({ - event: 'parseOptions', - handler: () => parsers.parseOptions(ret), - }); + if (ret) { + queue.unshift({ + event: 'parseOptions', + handler: () => parsers.parseOptions(ret), + }); + } } + break; } default: @@ -169,12 +164,12 @@ const parsers: Parsers = { }); } for (const namedPair of fieldValueNode.children()) { - if (!namedPair.is('pair')) continue; - - pairs.push({ - before: namedPair, - after: namedPair.text(), - }); + if (namedPair.is('pair')) { + pairs.push({ + before: namedPair, + after: namedPair.text(), + }); + } } } }, @@ -204,7 +199,7 @@ const parsers: Parsers = { } } }, -}; +} as const satisfies Record>) => void>; export default function transform(root: SgRoot): string | null { const rootNode = root.root(); From 4d1baab0080660681c4643fbd00cc7d6a0af8120 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Mon, 16 Mar 2026 10:01:21 +0000 Subject: [PATCH 13/15] Skip processing when needed and fix typos in package.json Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- recipes/mock-module-exports/package.json | 4 ++-- recipes/mock-module-exports/src/workflow.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/recipes/mock-module-exports/package.json b/recipes/mock-module-exports/package.json index a75a220b..8f0055e0 100644 --- a/recipes/mock-module-exports/package.json +++ b/recipes/mock-module-exports/package.json @@ -9,7 +9,7 @@ "repository": { "type": "git", "url": "git+https://github.com/nodejs/userland-migrations.git", - "directory": "recipes/util-is", + "directory": "recipes/mock-module-exports", "bugs": "https://github.com/nodejs/userland-migrations/issues" }, "author": "Bruno Rodrigues", @@ -19,6 +19,6 @@ "@codemod.com/jssg-types": "^1.3.1" }, "dependencies": { - "@nodejs/codemod-utils": "0.0.0" + "@nodejs/codemod-utils": "*" } } diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index bb49a610..2a7b7c7f 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -208,6 +208,8 @@ export default function transform(root: SgRoot): string | null { const deps = getModuleDependencies(root, 'test'); let moduleFnCalls: SgNode[] = []; + if (!deps.length) return null; + for (const dep of deps) { const moduleFn = resolveBindingPath(dep, '$.mock.module'); @@ -271,7 +273,7 @@ export default function transform(root: SgRoot): string | null { edits.push(change.node.replace(newValue)); } - const sourceCode = rootNode.commitEdits(edits); + if (!edits.length) return null; - return sourceCode; + return rootNode.commitEdits(edits); } From efc756a08cfcf01a30a49e0d844b647a43d975c9 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Wed, 18 Mar 2026 13:27:55 +0000 Subject: [PATCH 14/15] replace unshift with push --- recipes/mock-module-exports/src/workflow.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index 2a7b7c7f..ab41be46 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -31,7 +31,7 @@ const parsers = { parseOptions: (optionsNode: SgNode>) => { switch (optionsNode.kind()) { case 'object': - queue.unshift( + queue.push( { event: 'defaultExport', handler: () => parsers.defaultExport(optionsNode), @@ -47,13 +47,13 @@ const parsers = { ); break; case 'identifier': - queue.unshift({ + queue.push({ event: 'resolveVariables', handler: () => parsers.resolveVariables(optionsNode), }); break; case 'call_expression': - queue.unshift({ + queue.push({ event: 'resolveVariables', handler: () => parsers.resolveVariables(optionsNode.field('function')), @@ -68,7 +68,7 @@ const parsers = { switch (definition.node.parent().kind()) { case 'variable_declarator': { const parent = definition.node.parent<'variable_declarator'>(); - queue.unshift({ + queue.push({ event: 'parseOptions', handler: () => parsers.parseOptions(parent.field('value')), }); @@ -87,7 +87,7 @@ const parsers = { for (const ret of returns) { if (ret) { - queue.unshift({ + queue.push({ event: 'parseOptions', handler: () => parsers.parseOptions(ret), }); @@ -236,17 +236,19 @@ export default function transform(root: SgRoot): string | null { if (args.length < 2) continue; const optionsArg = args[1]; - queue.unshift({ + queue.push({ event: 'parseOptions', handler: () => parsers.parseOptions(optionsArg), }); } const indentUnit = detectIndentUnit(rootNode.text()); - while (queue.length) { - const event = queue.at(-1); + + let i = 0; + while (queue.length > i) { + const event = queue.at(i); event.handler(); - queue.pop(); + i += 1; } for (const [_nodeId, change] of Array.from(exportedValues)) { From 1631f969ca9dd1379949dd91dba249dc86b2e93a Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Thu, 19 Mar 2026 22:21:16 +0000 Subject: [PATCH 15/15] chore: use increment operator Co-authored-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- recipes/mock-module-exports/src/workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/mock-module-exports/src/workflow.ts b/recipes/mock-module-exports/src/workflow.ts index ab41be46..76105183 100644 --- a/recipes/mock-module-exports/src/workflow.ts +++ b/recipes/mock-module-exports/src/workflow.ts @@ -248,7 +248,7 @@ export default function transform(root: SgRoot): string | null { while (queue.length > i) { const event = queue.at(i); event.handler(); - i += 1; + i++; } for (const [_nodeId, change] of Array.from(exportedValues)) {