Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 40 additions & 116 deletions codemods/rimraf/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ts } from '@ast-grep/napi';
import {
findDefaultImports,
findNamedImports,
generateImport,
} from '../shared-ast-grep.js';

const MODULE_NAME = 'rimraf';

Expand All @@ -9,19 +14,6 @@ const MODULE_NAME = 'rimraf';
*/

const defaultOptions = `{recursive: true, force: true}`;
/**
* @param {boolean} useRequire
* @param {string} quoteType
* @param {string[]} names
* @param {string} source
* @returns {string}
*/
const computeImport = (useRequire, quoteType, names, source) => {
if (useRequire) {
return `const {${names.join(', ')}} = require(${quoteType}${source}${quoteType});`;
}
return `import {${names.join(', ')}} from ${quoteType}${source}${quoteType};`;
};

/**
* @param {CodemodOptions} [options]
Expand All @@ -34,129 +26,61 @@ export default function (options) {
transform: ({ file }) => {
const ast = ts.parse(file.source);
const root = ast.root();
const imports = root.findAll({
const { imports: namedImports, localNames: namedLocals } =
findNamedImports(root, MODULE_NAME);
const { imports: defaultImports, localNames: defaultLocals } =
findDefaultImports(root, MODULE_NAME);

const namespaceImports = root.findAll({
rule: {
any: [
{
pattern: {
context: "import * as $NAME from 'rimraf'",
strictness: 'relaxed',
},
},
{
pattern: {
context: "import {$$$NAMES} from 'rimraf'",
strictness: 'relaxed',
},
},
{
pattern: {
context: "import $NAME from 'rimraf'",
strictness: 'relaxed',
},
},
{
pattern: {
context: "const {$$$NAMES} = require('rimraf')",
strictness: 'relaxed',
},
},
{
pattern: {
context: "const $NAME = require('rimraf')",
strictness: 'relaxed',
},
},
],
pattern: {
context: `import * as $NAME from '${MODULE_NAME}'`,
strictness: 'relaxed',
},
},
});

const imports = [...namedImports, ...defaultImports, ...namespaceImports];

if (imports.length === 0) {
return file.source;
}

/** @type {Edit[]} */
const edits = [];
let quoteType = "'";
const quoteType = imports.some((imp) => imp.text().includes('"'))
? '"'
: "'";
const isCommonJS = imports.some(
(imp) => imp.find('require($SOURCE)') !== null,
);

/** @type {string[]} */
const localNames = [];
let isCommonJS = false;

for (const imp of imports) {
const importSource = imp.field('source');
const requireCall = imp.find('require($SOURCE)');
let source = null;

if (importSource) {
// ESM
source = importSource.text();
} else {
// CJS
source = requireCall?.getMatch('SOURCE')?.text();
}

if (!source) {
continue;
}

if (!isCommonJS) {
isCommonJS = requireCall !== null;
}

if (source?.startsWith('"')) {
quoteType = '"';
}

const importedNames = imp.getMultipleMatches('NAMES');
const importedName = imp.getMatch('NAME');

// Its a default or namespace import
if (importedName) {
const importedNameText = importedName.text();
localNames.push(
importedNameText,
`${importedNameText}.$_METHOD`,
`${importedNameText}.$_METHOD.sync`,
);
}

for (const importSpecifier of importedNames) {
const importedName = importSpecifier.field('name');
const value = importSpecifier.field('value');

let localNameText;

if (importedName) {
// ESM
const localName = importSpecifier.field('alias') ?? importedName;
localNameText = localName.text();
} else if (value) {
// CJS
localNameText = value.text();
} else {
localNameText = importSpecifier.text();
}

localNames.push(localNameText, `${localNameText}.sync`);
for (const name of namedLocals) {
localNames.push(name, `${name}.sync`);
}
for (const name of defaultLocals) {
localNames.push(name, `${name}.$_METHOD`, `${name}.$_METHOD.sync`);
}
for (const imp of namespaceImports) {
const name = imp.getMatch('NAME')?.text();
if (name) {
localNames.push(name, `${name}.$_METHOD`, `${name}.$_METHOD.sync`);
}
}

const usagePatterns = [];
for (const name of localNames) {
usagePatterns.push(
{
pattern: `${name}($PATH, $OPTIONS)`,
},
{
pattern: `${name}($PATH)`,
},
{ pattern: `${name}($PATH, $OPTIONS)` },
{ pattern: `${name}($PATH)` },
);
}

const usages = root.findAll({
rule: {
any: usagePatterns,
},
rule: { any: usagePatterns },
});

let seenSync = false;
Expand Down Expand Up @@ -240,19 +164,19 @@ Promise.all(

if (seenAsync) {
replacedImports.push(
computeImport(isCommonJS, quoteType, ['rm'], 'node:fs/promises'),
generateImport(isCommonJS, quoteType, 'rm', 'node:fs/promises'),
);
}

if (seenSync) {
replacedImports.push(
computeImport(isCommonJS, quoteType, ['rmSync'], 'node:fs'),
generateImport(isCommonJS, quoteType, 'rmSync', 'node:fs'),
);
}

if (seenGlob) {
replacedImports.push(
computeImport(isCommonJS, quoteType, ['glob'], 'tinyglobby'),
generateImport(isCommonJS, quoteType, 'glob', 'tinyglobby'),
);
}

Expand Down
87 changes: 52 additions & 35 deletions codemods/shared-ast-grep.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function removeImport(root, moduleName) {
const localNames = [];

// 1. Default imports/requires and assignments
const { imports, localNames: defaultLocalNames } = findNamedDefaultImports(
const { imports, localNames: defaultLocalNames } = findDefaultImports(
root,
moduleName,
);
Expand Down Expand Up @@ -158,11 +158,17 @@ export function removeImport(root, moduleName) {
/**
* Find all default imports/requires for a package and extract common metadata.
*
* Handles:
* - `import X from 'pkg'`
* - `const/var X = require('pkg')`
* - `const/var X = require('pkg').method(...)`
* - `X = require('pkg')`
*
* @param {SgNode} root - The root of the AST.
* @param {string} moduleName - The package to find imports for.
* @returns {{ imports: SgNode[], localNames: string[], quoteType: string }}
*/
function findNamedDefaultImports(root, moduleName) {
export function findDefaultImports(root, moduleName) {
const imports = root.findAll({
rule: {
any: [
Expand Down Expand Up @@ -243,7 +249,7 @@ function findNamedDefaultImports(root, moduleName) {
* @returns {{ imports: SgNode[], identifierName: string | null }}
*/
export function findDefaultImportIdentifier(root, moduleName) {
const { imports } = findNamedDefaultImports(root, moduleName);
const { imports } = findDefaultImports(root, moduleName);
const identifierName = imports[0]?.getMatch('NAME')?.text() ?? null;
return { imports, identifierName };
}
Expand Down Expand Up @@ -364,7 +370,7 @@ export function replaceDefaultImport(
toIdentifier,
asNamespace = false,
) {
const { imports, localNames, quoteType } = findNamedDefaultImports(
const { imports, localNames, quoteType } = findDefaultImports(
root,
fromPackage,
);
Expand All @@ -379,25 +385,46 @@ export function replaceDefaultImport(
const identifier = toIdentifier || nameMatch.text();
const isCommonJS = imp.find('require($SOURCE)') !== null;

if (isCommonJS) {
edits.push(
imp.replace(
`const ${identifier} = require(${quoteType}${toPackage}${quoteType});`,
),
);
} else {
const prefix = asNamespace ? 'import * as ' : 'import ';
edits.push(
imp.replace(
`${prefix}${identifier} from ${quoteType}${toPackage}${quoteType};`,
),
);
}
const kind = asNamespace ? 'namespace' : 'default';
edits.push(
imp.replace(
generateImport(isCommonJS, quoteType, identifier, toPackage, kind),
),
);
}

return { edits, localNames, quoteType };
}

/**
* Generate an import or require statement as a string.
*
* @param {boolean} useRequire - Use require() instead of import.
* @param {string} quoteType - Quote character (" or ').
* @param {string} name - The local identifier or import name.
* @param {string} source - Module specifier.
* @param {'named' | 'default' | 'namespace'} [kind='named'] - Import kind.
* @returns {string}
*/
export function generateImport(
useRequire,
quoteType,
name,
source,
kind = 'named',
) {
const from = `${quoteType}${source}${quoteType}`;
const specifier =
kind === 'named'
? `{ ${name} }`
: kind === 'namespace'
? `* as ${name}`
: name;
return useRequire
? `const ${specifier} = require(${from});`
: `import ${specifier} from ${from};`;
}

/**
* Remove the import of a polyfill module and replace all references to its
* default import identifier with the given replacement string.
Expand Down Expand Up @@ -462,7 +489,7 @@ export function replaceDefaultWithNamedImport(
toPackage,
namedImport,
) {
const { imports, localNames, quoteType } = findNamedDefaultImports(
const { imports, localNames, quoteType } = findDefaultImports(
root,
fromPackage,
);
Expand All @@ -476,19 +503,11 @@ export function replaceDefaultWithNamedImport(

const isCommonJS = imp.find('require($SOURCE)') !== null;

if (isCommonJS) {
edits.push(
imp.replace(
`const { ${namedImport} } = require(${quoteType}${toPackage}${quoteType});`,
),
);
} else {
edits.push(
imp.replace(
`import { ${namedImport} } from ${quoteType}${toPackage}${quoteType};`,
),
);
}
edits.push(
imp.replace(
generateImport(isCommonJS, quoteType, namedImport, toPackage),
),
);
}

const namespaceImports = root.findAll({
Expand Down Expand Up @@ -522,9 +541,7 @@ export function replaceDefaultWithNamedImport(
}

edits.push(
imp.replace(
`import { ${namedImport} } from ${quoteType}${toPackage}${quoteType};`,
),
imp.replace(generateImport(false, quoteType, namedImport, toPackage)),
);
}

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/rimraf/cjs/after.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {rm} = require('node:fs/promises');
const {rmSync} = require('node:fs');
const { rm } = require('node:fs/promises');
const { rmSync } = require('node:fs');

await rm('./dist', {recursive: true, force: true});

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/rimraf/cjs/result.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {rm} = require('node:fs/promises');
const {rmSync} = require('node:fs');
const { rm } = require('node:fs/promises');
const { rmSync } = require('node:fs');

await rm('./dist', {recursive: true, force: true});

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/rimraf/esm/after.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {rm} from 'node:fs/promises';
import {glob} from 'tinyglobby';
import { rm } from 'node:fs/promises';
import { glob } from 'tinyglobby';

await rm('./dist', {recursive: true, force: true});

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/rimraf/esm/result.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {rm} from 'node:fs/promises';
import {glob} from 'tinyglobby';
import { rm } from 'node:fs/promises';
import { glob } from 'tinyglobby';

await rm('./dist', {recursive: true, force: true});

Expand Down
Loading