From d2832e1830c684a3420b9611e039f020650681c1 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 26 May 2026 07:49:37 +0100 Subject: [PATCH 1/2] feat: migrate `xtend` to ast-grep --- codemods/xtend/index.js | 49 ++++++++++++++++------------ test/fixtures/xtend/case-1/after.js | 6 ++-- test/fixtures/xtend/case-1/before.js | 2 +- test/fixtures/xtend/case-1/result.js | 6 ++-- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/codemods/xtend/index.js b/codemods/xtend/index.js index 3fcf4b85..45fe3d2d 100644 --- a/codemods/xtend/index.js +++ b/codemods/xtend/index.js @@ -1,5 +1,5 @@ -import jscodeshift from 'jscodeshift'; -import { removeImport } from '../shared.js'; +import { ts } from '@ast-grep/napi'; +import { removeImport } from '../shared-ast-grep.js'; /** * @typedef {import('../../types.js').Codemod} Codemod @@ -15,25 +15,32 @@ export default function (options) { name: 'xtend', to: 'native', transform: ({ file }) => { - const j = jscodeshift; - const root = j(file.source); - - const { identifier } = removeImport('xtend', root, j); - - root - .find(j.CallExpression, { - callee: { - name: identifier, - }, - }) - .replaceWith(({ node }) => { - return j.objectExpression( - //@ts-ignore - node.arguments.map((arg) => j.spreadElement(arg)), - ); - }); - - return root.toSource(options); + const ast = ts.parse(file.source); + const root = ast.root(); + + const { edits, localNames } = removeImport(root, 'xtend'); + + if (localNames.length === 0) { + return file.source; + } + + const identifierName = localNames[0]; + const calls = root.findAll({ + rule: { + pattern: `${identifierName}($$$ARGS)`, + }, + }); + + for (const call of calls) { + const argsMatch = call.getMultipleMatches('ARGS'); + if (!argsMatch) continue; + + const args = argsMatch.filter((m) => m.kind() !== ','); + const spreadArgs = args.map((a) => `...${a.text()}`); + edits.push(call.replace(`{ ${spreadArgs.join(', ')} }`)); + } + + return edits.length > 0 ? root.commitEdits(edits) : file.source; }, }; } diff --git a/test/fixtures/xtend/case-1/after.js b/test/fixtures/xtend/case-1/after.js index 955879bb..bac0e0a1 100644 --- a/test/fixtures/xtend/case-1/after.js +++ b/test/fixtures/xtend/case-1/after.js @@ -1,3 +1,4 @@ + const assert = require('assert'); const objectA = { @@ -11,7 +12,4 @@ const objectB = { d: 40, }; -assert.equal({ - ...objectA, - ...objectB -}, { a: 20, b: 2, c: 3, d: 40 }); \ No newline at end of file +assert.equal({ ...objectA, ...objectB }, { a: 20, b: 2, c: 3, d: 40 }); diff --git a/test/fixtures/xtend/case-1/before.js b/test/fixtures/xtend/case-1/before.js index 23eca1bf..a37ed01c 100644 --- a/test/fixtures/xtend/case-1/before.js +++ b/test/fixtures/xtend/case-1/before.js @@ -12,4 +12,4 @@ const objectB = { d: 40, }; -assert.equal(extend(objectA, objectB), { a: 20, b: 2, c: 3, d: 40 }); \ No newline at end of file +assert.equal(extend(objectA, objectB), { a: 20, b: 2, c: 3, d: 40 }); diff --git a/test/fixtures/xtend/case-1/result.js b/test/fixtures/xtend/case-1/result.js index 955879bb..bac0e0a1 100644 --- a/test/fixtures/xtend/case-1/result.js +++ b/test/fixtures/xtend/case-1/result.js @@ -1,3 +1,4 @@ + const assert = require('assert'); const objectA = { @@ -11,7 +12,4 @@ const objectB = { d: 40, }; -assert.equal({ - ...objectA, - ...objectB -}, { a: 20, b: 2, c: 3, d: 40 }); \ No newline at end of file +assert.equal({ ...objectA, ...objectB }, { a: 20, b: 2, c: 3, d: 40 }); From 62cef251b847ec321f2de095b58b13fe8daed4e7 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 May 2026 14:40:01 +0100 Subject: [PATCH 2/2] chore: use `computeCallReplacementEdits` util --- codemods/xtend/index.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/codemods/xtend/index.js b/codemods/xtend/index.js index 45fe3d2d..c3eb4dd7 100644 --- a/codemods/xtend/index.js +++ b/codemods/xtend/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; /** * @typedef {import('../../types.js').Codemod} Codemod @@ -25,20 +28,15 @@ export default function (options) { } const identifierName = localNames[0]; - const calls = root.findAll({ - rule: { - pattern: `${identifierName}($$$ARGS)`, + const callEdits = computeCallReplacementEdits( + root, + identifierName, + (args) => { + const spreadArgs = args.map((a) => `...${a}`); + return `{ ${spreadArgs.join(', ')} }`; }, - }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - const spreadArgs = args.map((a) => `...${a.text()}`); - edits.push(call.replace(`{ ${spreadArgs.join(', ')} }`)); - } + ); + edits.push(...callEdits); return edits.length > 0 ? root.commitEdits(edits) : file.source; },