Skip to content

Commit 3d5bef7

Browse files
update
1 parent 6bb3e44 commit 3d5bef7

5 files changed

Lines changed: 126 additions & 204 deletions

File tree

recipes/axios-to-whatwg-fetch/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"description": "Replace `axios` with `fetch`",
55
"type": "module",
66
"scripts": {
7-
"test": "npx codemod jssg test -l tsx ./src/workflow.ts ./"
7+
"test": "node --run test:workflow && node --run test:remove-dependencies",
8+
"test:workflow": "npx codemod jssg test -l tsx ./src/workflow.ts ./",
9+
"test:remove-dependencies": "npx codemod jssg test -l json ./src/remove-dependencies.ts ./tests/remove-dependencies --allow-child-process --allow-fs --strictness cst"
810
},
911
"repository": {
1012
"type": "git",
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import type { Transform } from '@codemod.com/jssg-types/main';
2+
import type Json from '@codemod.com/jssg-types/langs/json';
13
import removeDependencies from '@nodejs/codemod-utils/remove-dependencies';
24

35
/**
4-
* Remove `chalk` and `@types/chalk` dependencies from package.json
6+
* Remove `axios` and `@types/axios` dependencies from package.json
57
*/
6-
export default function removeAxiosDependencies(): string | null {
7-
return removeDependencies(['axios']);
8-
}
8+
const transform: Transform<Json> = async (root) => {
9+
return removeDependencies(['axios', '@types/axios'], {
10+
packageJsonPath: root.filename(),
11+
runInstall: false,
12+
persistFileWrite: false,
13+
});
14+
};
15+
16+
export default transform;

recipes/axios-to-whatwg-fetch/src/workflow.ts

Lines changed: 89 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { EOL } from 'node:os';
22
import dedent from 'dedent';
3-
import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call';
4-
import {
5-
getNodeImportCalls,
6-
getNodeImportStatements,
7-
} from '@nodejs/codemod-utils/ast-grep/import-statement';
83
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
94
import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding';
105
import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines';
@@ -16,6 +11,7 @@ import type {
1611
SgRoot,
1712
} from '@codemod.com/jssg-types/main';
1813
import type Js from '@codemod.com/jssg-types/langs/javascript';
14+
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
1915

2016
type BindingToReplace = {
2117
rule: Rule<Js>;
@@ -36,6 +32,16 @@ type CreateOptionsType = {
3632
payloadKind?: 'json' | 'form';
3733
};
3834

35+
type AxiosMethodUpdateConfig = {
36+
name: string;
37+
method?: string;
38+
oldOptionsIndex: number;
39+
bodyIndex?: number;
40+
payloadKind?: NonNullable<CreateOptionsType['payloadKind']>;
41+
responseAlias?: string;
42+
optionalOptionsArg?: boolean;
43+
};
44+
3945
const formatLocation = ({
4046
root,
4147
node,
@@ -124,210 +130,98 @@ const getFormBodyExpression = (
124130
`;
125131
};
126132

127-
const baseUpdates: {
128-
oldBind: string;
129-
replaceFn: BindingToReplace['replaceFn'];
130-
supportDefaultAccess?: boolean;
131-
}[] = [
132-
{
133-
oldBind: '$.get',
134-
replaceFn: (args, context) => {
135-
const [url, oldOptions] = args;
136-
if (!url) {
137-
warnWithLocation(context, 'Missing URL in axios.get. Skipping.');
138-
return '';
139-
}
140-
const options = createOptions({ oldOptions });
141-
return dedent.withOptions({ alignValues: true })`
142-
fetch(${url.text()}${options ? `, ${options}` : ''})
143-
.then(async (res) => Object.assign(res, { data: await res.json() }))
144-
.catch(() => null)
145-
`;
146-
},
147-
},
148-
{
149-
oldBind: '$.post',
150-
replaceFn: (args, context) => {
151-
const url = args[0];
152-
if (!url) {
153-
warnWithLocation(context, 'Missing URL in axios.post. Skipping.');
154-
return '';
155-
}
156-
const options = createOptions({
157-
oldOptions: args[2],
158-
method: 'POST',
159-
bodyNode: args[1] ?? null,
160-
payloadKind: 'json',
161-
});
162-
return dedent.withOptions({ alignValues: true })`
163-
fetch(${url.text()}${options ? `, ${options}` : ''})
164-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
165-
.catch(() => null)
166-
`;
167-
},
168-
},
169-
{
170-
oldBind: '$.put',
171-
replaceFn: (args, context) => {
172-
const url = args[0];
173-
if (!url) {
174-
warnWithLocation(context, 'Missing URL in axios.put. Skipping.');
175-
return '';
176-
}
177-
const options = createOptions({
178-
oldOptions: args[2],
179-
method: 'PUT',
180-
bodyNode: args[1] ?? null,
181-
payloadKind: 'json',
182-
});
183-
return dedent.withOptions({ alignValues: true })`
184-
fetch(${url.text()}${options ? `, ${options}` : ''})
185-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
186-
.catch(() => null)
187-
`;
188-
},
189-
},
190-
{
191-
oldBind: '$.patch',
192-
replaceFn: (args, context) => {
193-
const url = args[0];
194-
if (!url) {
195-
warnWithLocation(context, 'Missing URL in axios.patch. Skipping.');
196-
return '';
197-
}
198-
const options = createOptions({
199-
oldOptions: args[2],
200-
method: 'PATCH',
201-
bodyNode: args[1] ?? null,
202-
payloadKind: 'json',
203-
});
204-
return dedent.withOptions({ alignValues: true })`
205-
fetch(${url.text()}${options ? `, ${options}` : ''})
206-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
133+
const createAxiosMethodUpdate = ({
134+
name,
135+
method,
136+
oldOptionsIndex,
137+
bodyIndex,
138+
payloadKind = 'json',
139+
responseAlias = 'resp',
140+
optionalOptionsArg = true,
141+
}: AxiosMethodUpdateConfig) => ({
142+
oldBind: `$.${name}`,
143+
replaceFn: (args: SgNode<Js>[], context: WarningContext) => {
144+
const url = args[0];
145+
if (!url) {
146+
warnWithLocation(context, `Missing URL in axios.${name}. Skipping.`);
147+
return '';
148+
}
149+
150+
const options = createOptions({
151+
oldOptions: args[oldOptionsIndex],
152+
method,
153+
bodyNode: bodyIndex === undefined ? undefined : (args[bodyIndex] ?? null),
154+
payloadKind,
155+
});
156+
157+
const fetchCall = optionalOptionsArg
158+
? `fetch(${url.text()}${options ? `, ${options}` : ''})`
159+
: `fetch(${url.text()}, ${options})`;
160+
161+
return dedent.withOptions({ alignValues: true })`
162+
${fetchCall}
163+
.then(async (${responseAlias}) => Object.assign(${responseAlias}, { data: await ${responseAlias}.json() }))
207164
.catch(() => null)
208165
`;
209-
},
210166
},
167+
});
168+
169+
const axiosMethodUpdates: AxiosMethodUpdateConfig[] = [
170+
{ name: 'post', method: 'POST', oldOptionsIndex: 2, bodyIndex: 1 },
171+
{ name: 'put', method: 'PUT', oldOptionsIndex: 2, bodyIndex: 1 },
172+
{ name: 'patch', method: 'PATCH', oldOptionsIndex: 2, bodyIndex: 1 },
211173
{
212-
oldBind: '$.postForm',
213-
replaceFn: (args, context) => {
214-
const url = args[0];
215-
if (!url) {
216-
warnWithLocation(context, 'Missing URL in axios.postForm. Skipping.');
217-
return '';
218-
}
219-
const options = createOptions({
220-
oldOptions: args[2],
221-
method: 'POST',
222-
bodyNode: args[1] ?? null,
223-
payloadKind: 'form',
224-
});
225-
return dedent.withOptions({ alignValues: true })`
226-
fetch(${url.text()}${options ? `, ${options}` : ''})
227-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
228-
.catch(() => null)
229-
`;
230-
},
174+
name: 'postForm',
175+
method: 'POST',
176+
oldOptionsIndex: 2,
177+
bodyIndex: 1,
178+
payloadKind: 'form',
231179
},
232180
{
233-
oldBind: '$.putForm',
234-
replaceFn: (args, context) => {
235-
const url = args[0];
236-
if (!url) {
237-
warnWithLocation(context, 'Missing URL in axios.putForm. Skipping.');
238-
return '';
239-
}
240-
const options = createOptions({
241-
oldOptions: args[2],
242-
method: 'PUT',
243-
bodyNode: args[1] ?? null,
244-
payloadKind: 'form',
245-
});
246-
return dedent.withOptions({ alignValues: true })`
247-
fetch(${url.text()}${options ? `, ${options}` : ''})
248-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
249-
.catch(() => null)
250-
`;
251-
},
181+
name: 'putForm',
182+
method: 'PUT',
183+
oldOptionsIndex: 2,
184+
bodyIndex: 1,
185+
payloadKind: 'form',
252186
},
253187
{
254-
oldBind: '$.patchForm',
255-
replaceFn: (args, context) => {
256-
const url = args[0];
257-
if (!url) {
258-
warnWithLocation(context, 'Missing URL in axios.patchForm. Skipping.');
259-
return '';
260-
}
261-
const options = createOptions({
262-
oldOptions: args[2],
263-
method: 'PATCH',
264-
bodyNode: args[1] ?? null,
265-
payloadKind: 'form',
266-
});
267-
return dedent.withOptions({ alignValues: true })`
268-
fetch(${url.text()}${options ? `, ${options}` : ''})
269-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
270-
.catch(() => null)
271-
`;
272-
},
188+
name: 'patchForm',
189+
method: 'PATCH',
190+
oldOptionsIndex: 2,
191+
bodyIndex: 1,
192+
payloadKind: 'form',
273193
},
274194
{
275-
oldBind: '$.delete',
276-
replaceFn: (args, context) => {
277-
const url = args[0];
278-
if (!url) {
279-
warnWithLocation(context, 'Missing URL in axios.delete. Skipping.');
280-
return '';
281-
}
282-
const options = createOptions({
283-
oldOptions: args[1],
284-
method: 'DELETE',
285-
});
286-
return dedent.withOptions({ alignValues: true })`
287-
fetch(${url.text()}, ${options})
288-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
289-
.catch(() => null)
290-
`;
291-
},
195+
name: 'delete',
196+
method: 'DELETE',
197+
oldOptionsIndex: 1,
198+
optionalOptionsArg: false,
292199
},
293200
{
294-
oldBind: '$.head',
295-
replaceFn: (args, context) => {
296-
const url = args[0];
297-
if (!url) {
298-
warnWithLocation(context, 'Missing URL in axios.head. Skipping.');
299-
return '';
300-
}
301-
const options = createOptions({
302-
oldOptions: args[1],
303-
method: 'HEAD',
304-
});
305-
return dedent.withOptions({ alignValues: true })`
306-
fetch(${url.text()}, ${options})
307-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
308-
.catch(() => null)
309-
`;
310-
},
201+
name: 'head',
202+
method: 'HEAD',
203+
oldOptionsIndex: 1,
204+
optionalOptionsArg: false,
311205
},
312206
{
313-
oldBind: '$.options',
314-
replaceFn: (args, context) => {
315-
const url = args[0];
316-
if (!url) {
317-
warnWithLocation(context, 'Missing URL in axios.options. Skipping.');
318-
return '';
319-
}
320-
const options = createOptions({
321-
oldOptions: args[1],
322-
method: 'OPTIONS',
323-
});
324-
return dedent.withOptions({ alignValues: true })`
325-
fetch(${url.text()}, ${options})
326-
.then(async (resp) => Object.assign(resp, { data: await resp.json() }))
327-
.catch(() => null)
328-
`;
329-
},
207+
name: 'options',
208+
method: 'OPTIONS',
209+
oldOptionsIndex: 1,
210+
optionalOptionsArg: false,
330211
},
212+
];
213+
214+
const baseUpdates: {
215+
oldBind: string;
216+
replaceFn: BindingToReplace['replaceFn'];
217+
supportDefaultAccess?: boolean;
218+
}[] = [
219+
createAxiosMethodUpdate({
220+
name: 'get',
221+
oldOptionsIndex: 1,
222+
responseAlias: 'res',
223+
}),
224+
...axiosMethodUpdates.map(createAxiosMethodUpdate),
331225
{
332226
oldBind: '$.request',
333227
replaceFn: (args, context) => {
@@ -475,11 +369,7 @@ export default function transform(root: SgRoot<Js>): string | null {
475369
const linesToRemove: Range[] = [];
476370
const bindsToReplace: BindingToReplace[] = [];
477371

478-
const importRequireStatement = [
479-
...getNodeRequireCalls(root, 'axios'),
480-
...getNodeImportStatements(root, 'axios'),
481-
...getNodeImportCalls(root, 'axios'),
482-
];
372+
const importRequireStatement = getModuleDependencies(root, 'axios');
483373

484374
if (!importRequireStatement.length) return null;
485375

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "fixture",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"ky": "^1.8.1"
6+
},
7+
"devDependencies": {
8+
"typescript": "^5.6.0"
9+
}
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "fixture",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"axios": "^1.11.0",
6+
"ky": "^1.8.1"
7+
},
8+
"devDependencies": {
9+
"@types/axios": "^0.14.4",
10+
"typescript": "^5.6.0"
11+
}
12+
}

0 commit comments

Comments
 (0)