Skip to content

Commit 485cdc1

Browse files
feat(utils): add package.json (#194)
Co-authored-by: Bruno Rodrigues <[email protected]>
1 parent b4c7cfe commit 485cdc1

File tree

7 files changed

+720
-1
lines changed

7 files changed

+720
-1
lines changed

package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/README.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# `@nodejs/codemod-utils`
2+
3+
This is a local package, it's mean it's not published to npm registry it's only used in the monorepo.
4+
5+
## Why Use These Utilities?
6+
7+
Building Node.js codemods with [ast-grep](https://ast-grep.github.io/) requires handling complex patterns for:
8+
9+
- **Import/Require Analysis**: Finding and analyzing `import` statements and `require()` calls for specific modules
10+
- **Binding Resolution**: Determining how imported functions are accessed locally (destructured, aliased, etc.)
11+
- **Code Transformation**: Safely removing unused imports and modifying code while preserving formatting
12+
- **Package.json Processing**: Analyzing and transforming `package.json` scripts that use Node.js
13+
14+
These utilities provide battle-tested solutions for common codemod operations, reducing boilerplate and ensuring consistent behavior across all migration recipes.
15+
16+
## AST-grep Utilities
17+
18+
### Import and Require Detection
19+
20+
#### `getNodeImportStatements(rootNode, nodeModuleName)`
21+
22+
Finds all ES module import statements for a specific Node.js module.
23+
24+
```typescript
25+
import { getNodeImportStatements } from '@nodejs/codemod-utils';
26+
27+
// Finds: import fs from 'fs'; import { readFile } from 'node:fs';
28+
const fsImports = getNodeImportStatements(ast, 'fs');
29+
```
30+
31+
#### `getNodeImportCalls(rootNode, nodeModuleName)`
32+
33+
Finds dynamic import calls assigned to variables (excludes unassigned imports).
34+
35+
```typescript
36+
import { getNodeImportCalls } from '@nodejs/codemod-utils';
37+
38+
// Finds: const fs = await import('node:fs');
39+
// Ignores: import('node:fs'); // unassigned
40+
const fsImportCalls = getNodeImportCalls(ast, 'fs');
41+
```
42+
43+
#### `getNodeRequireCalls(rootNode, nodeModuleName)`
44+
45+
Finds CommonJS require calls assigned to variables.
46+
47+
```typescript
48+
import { getNodeRequireCalls } from '@nodejs/codemod-utils';
49+
50+
// Finds: const fs = require('fs'); const { readFile } = require('node:fs');
51+
const fsRequires = getNodeRequireCalls(ast, 'fs');
52+
```
53+
54+
### Binding Resolution and Transformation
55+
56+
#### `resolveBindingPath(node, path)`
57+
58+
Resolves how a global API path should be accessed based on the import pattern.
59+
60+
```typescript
61+
import { resolveBindingPath } from '@nodejs/codemod-utils';
62+
63+
// Given: const { types } = require('node:util');
64+
// resolveBindingPath(node, '$.types.isNativeError') → 'types.isNativeError'
65+
66+
// Given: const util = require('node:util');
67+
// resolveBindingPath(node, '$.types.isNativeError') → 'util.types.isNativeError'
68+
69+
// Given: import { types as utilTypes } from 'node:util';
70+
// resolveBindingPath(node, '$.types.isNativeError') → 'utilTypes.isNativeError'
71+
```
72+
73+
#### `removeBinding(node, binding)`
74+
75+
Removes a specific binding from destructured imports/requires, or removes the entire statement if it's the only binding.
76+
77+
```typescript
78+
import { removeBinding } from '@nodejs/codemod-utils';
79+
80+
// Given: const { types, isNativeError } = require('node:util');
81+
// removeBinding(node, 'isNativeError') → Edit to: const { types } = require('node:util');
82+
83+
// Given: const { isNativeError } = require('node:util');
84+
// removeBinding(node, 'isNativeError') → Returns line range to remove entire statement
85+
```
86+
87+
### Code Manipulation
88+
89+
#### `removeLines(sourceCode, ranges)`
90+
91+
Safely removes multiple line ranges from source code, handling overlaps and duplicates.
92+
93+
```typescript
94+
import { removeLines } from '@nodejs/codemod-utils';
95+
96+
const ranges = [
97+
{ start: { line: 5, column: 0 }, end: { line: 5, column: 50 } },
98+
{ start: { line: 12, column: 0 }, end: { line: 14, column: 0 } }
99+
];
100+
101+
const cleanedCode = removeLines(sourceCode, ranges);
102+
```
103+
104+
### Package.json Utilities
105+
106+
#### `getScriptsNode(packageJsonRootNode)`
107+
108+
Finds the "scripts" section in a package.json AST.
109+
110+
```typescript
111+
import { getScriptsNode } from '@nodejs/codemod-utils';
112+
113+
const scriptsNodes = getScriptsNode(packageJsonAst);
114+
```
115+
116+
#### `getNodeJsUsage(packageJsonRootNode)`
117+
118+
Finds all references to `node` or `node.exe` in package.json scripts.
119+
120+
```typescript
121+
import { getNodeJsUsage } from '@nodejs/codemod-utils';
122+
123+
// Finds scripts like: "start": "node server.js", "build": "node.exe build.js"
124+
const nodeUsages = getNodeJsUsage(packageJsonAst);
125+
```
126+
127+
## Practical Examples
128+
129+
### Complete Codemod Workflow
130+
131+
Here's how these utilities work together in a typical Node.js deprecation codemod:
132+
133+
```typescript
134+
import astGrep from '@ast-grep/napi';
135+
import {
136+
getNodeImportStatements,
137+
getNodeRequireCalls,
138+
resolveBindingPath,
139+
removeBinding,
140+
removeLines
141+
} from '@nodejs/codemod-utils';
142+
143+
export default function workflow({ file, options }) {
144+
// 1. Parse the source code
145+
const ast = astGrep.parse(astGrep.Lang.JavaScript, file.source);
146+
147+
// 2. Find all util imports/requires
148+
const importStatements = getNodeImportStatements(ast, 'util');
149+
const requireCalls = getNodeRequireCalls(ast, 'util');
150+
const allUtilNodes = [...importStatements, ...requireCalls];
151+
152+
// 3. Find and transform deprecated API usage
153+
const edits = [];
154+
const linesToRemove = [];
155+
156+
for (const node of allUtilNodes) {
157+
// Resolve how the deprecated API is accessed locally
158+
const localPath = resolveBindingPath(node, '$.types.isNativeError');
159+
160+
if (localPath) {
161+
// Find all usages of the deprecated API
162+
const usages = ast.root().findAll({
163+
rule: {
164+
kind: 'call_expression',
165+
has: {
166+
field: 'function',
167+
kind: 'member_expression',
168+
regex: localPath.replace('.', '\\.')
169+
}
170+
}
171+
});
172+
173+
// Transform each usage
174+
for (const usage of usages) {
175+
edits.push({
176+
startIndex: usage.range().start.index,
177+
endIndex: usage.range().end.index,
178+
newText: usage.text().replace(localPath, 'util.types.isError')
179+
});
180+
}
181+
182+
// Remove the binding if it's no longer needed
183+
const bindingRemoval = removeBinding(node, 'types');
184+
if (bindingRemoval?.edit) {
185+
edits.push(bindingRemoval.edit);
186+
} else if (bindingRemoval?.lineToRemove) {
187+
linesToRemove.push(bindingRemoval.lineToRemove);
188+
}
189+
}
190+
}
191+
192+
// 4. Apply all transformations
193+
let transformedSource = file.source;
194+
195+
// Apply edits
196+
for (const edit of edits.reverse()) { // reverse to maintain indices
197+
transformedSource = transformedSource.slice(0, edit.startIndex) +
198+
edit.newText +
199+
transformedSource.slice(edit.endIndex);
200+
}
201+
202+
// Remove entire lines
203+
if (linesToRemove.length > 0) {
204+
transformedSource = removeLines(transformedSource, linesToRemove);
205+
}
206+
207+
return {
208+
...file,
209+
source: transformedSource
210+
};
211+
}
212+
```
213+
214+
### Handling Different Import Patterns
215+
216+
The utilities automatically handle various import/require patterns:
217+
218+
```typescript
219+
// ES Modules
220+
import util from 'util'; // → util.types.isNativeError
221+
import { types } from 'util'; // → types.isNativeError
222+
import { types as t } from 'util'; // → t.isNativeError
223+
224+
// CommonJS
225+
const util = require('util'); // → util.types.isNativeError
226+
const { types } = require('util'); // → types.isNativeError
227+
const { types: t } = require('util'); // → t.isNativeError
228+
229+
// Mixed with node: protocol
230+
import { types } from 'node:util'; // → types.isNativeError
231+
const util = require('node:util'); // → util.types.isNativeError
232+
```
233+
234+
This unified approach ensures your codemods work correctly regardless of how developers import Node.js modules in their projects.

utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
]
99
},
1010
"scripts": {
11-
"test": "node --no-warnings --experimental-test-snapshots --experimental-strip-types --import='./src/snapshots.ts' --test --experimental-test-coverage --test-coverage-include='src/**/*' --test-coverage-exclude='**/*.test.ts' './**/*.test.ts'"
11+
"test": "node --no-warnings --import=./src/codemod-jssg-context.ts --experimental-test-snapshots --experimental-strip-types --import='./src/snapshots.ts' --test --experimental-test-coverage --test-coverage-include='src/**/*' --test-coverage-exclude='**/*.test.ts' './**/*.test.ts'"
1212
},
1313
"author": "Augustin Maurouy",
1414
"license": "MIT",
1515
"type": "module",
1616
"devDependencies": {
17+
"@ast-grep/lang-bash": "^0.0.4",
18+
"@ast-grep/lang-json": "^0.0.4",
1719
"@ast-grep/napi": "^0.39.5",
1820
"@codemod.com/jssg-types": "^1.0.9",
1921
"dedent": "^1.7.0"

0 commit comments

Comments
 (0)