diff --git a/package-lock.json b/package-lock.json index 40220450..1962fab7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -402,6 +402,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1471,6 +1472,10 @@ "node": ">=22 || ^20.6.0 || ^18.19.0" } }, + "node_modules/@nodejs/axios-to-whatwg-fetch": { + "resolved": "recipes/axios-to-whatwg-fetch", + "link": true + }, "node_modules/@nodejs/buffer-atob-btoa": { "resolved": "recipes/buffer-atob-btoa", "link": true @@ -1593,6 +1598,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1790,6 +1796,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2108,6 +2115,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2376,7 +2384,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -4273,6 +4280,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "recipes/axios-to-whatwg-fetch": { + "name": "@nodejs/axios-to-whatwg-fetch", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*", + "dedent": "^1.7.0" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/buffer-atob-btoa": { "name": "@nodejs/buffer-atob-btoa", "version": "1.0.1", diff --git a/recipes/axios-to-whatwg-fetch/README.md b/recipes/axios-to-whatwg-fetch/README.md new file mode 100644 index 00000000..56abdac3 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/README.md @@ -0,0 +1,142 @@ +# Axios to WHATWG Fetch Codemod + +## Description + +This codemod transforms code using Axios to leverage the WHATWG Fetch API, which is now natively available in Node.js. By replacing Axios with Fetch, you can reduce dependencies, mitigate risks, and improve performance. + +## Supported Transformations + +The codemod supports the following Axios methods and converts them to their Fetch equivalents: + +- `axios.request(config)` +- `axios.get(url[, config])` +- `axios.delete(url[, config])` +- `axios.head(url[, config])` +- `axios.options(url[, config])` +- `axios.post(url[, data[, config]])` +- `axios.put(url[, data[, config]])` +- `axios.patch(url[, data[, config]])` +- `axios.postForm(url[, data[, config]])` +- `axios.putForm(url[, data[, config]])` +- `axios.patchForm(url[, data[, config]])` +- `axios.request(config)` + +### Examples + +#### GET Request + +```diff + const base = 'https://dummyjson.com/todos'; + +- const all = await axios.get(base); ++ const all = await fetch(base).then(async (res) => Object.assign(res, { data: await res.json() })).catch(() => null); + console.log('\nGET /todos ->', all.status); + console.log(`Preview: ${all.data.todos.length} todos`); +``` + +#### POST Request + +```diff + const base = 'https://dummyjson.com/todos'; + +- const created = await axios.post( +- `${base}/add`, { +- todo: 'Use DummyJSON in the project', +- completed: false, +- userId: 5, +- }, { +- headers: { 'Content-Type': 'application/json' } +- } +- ); ++ const created = await fetch(`${base}/add`, { ++ method: 'POST', ++ headers: { 'Content-Type': 'application/json' }, ++ body: JSON.stringify({ ++ todo: 'Use DummyJSON in the project', ++ completed: false, ++ userId: 5, ++ }), ++ }).then(async (res) => Object.assign(res, { data: await res.json() })); + console.log('\nPOST /todos/add ->', created.status); + console.log('Preview:', created.data?.id ? `created id ${created.data.id}` : JSON.stringify(created.data).slice(0,200)); +``` + + #### POST Form Request + + ```diff + const formEndpoint = '/submit'; + + - const created = await axios.postForm(formEndpoint, { + - title: 'Form Demo', + - completed: false, + - }); + + const created = await fetch(formEndpoint, { + + method: 'POST', + + body: new URLSearchParams({ + + title: 'Form Demo', + + completed: false, + + }), + + }).then(async (res) => Object.assign(res, { data: await res.json() })); + console.log('Preview:', created.data); + ``` + +#### PUT Request + +```diff + const base = 'https://dummyjson.com/todos'; + +- const updatedPut = await axios.put( +- `${base}/1`, +- { completed: false }, +- { headers: { 'Content-Type': 'application/json' } } +- ); ++ const updatedPut = await fetch(`${base}/1`, { ++ method: 'PUT', ++ headers: { 'Content-Type': 'application/json' }, ++ body: JSON.stringify({ completed: false }), ++ }).then(async (res) => Object.assign(res, { data: await res.json() })); + console.log('\nPUT /todos/1 ->', updatedPut.status); + console.log('Preview:', updatedPut.data?.completed !== undefined ? `completed=${updatedPut.data.completed}` : JSON.stringify(updatedPut.data).slice(0,200)); +``` + +#### DELETE Request + +```diff + const base = 'https://dummyjson.com/todos'; + +- const deleted = await axios.delete(`${base}/1`); ++ const deleted = await fetch(`${base}/1`, { method: 'DELETE' }) ++ .then(async (res) => Object.assign(res, { data: await res.json() })); + console.log('\nDELETE /todos/1 ->', deleted.status); + console.log('Preview:', deleted.data ? JSON.stringify(deleted.data).slice(0,200) : typeof deleted.data); +``` + +#### `request` axios Method + +```diff + const base = 'https://dummyjson.com/todos'; + +- const customRequest = await axios.request({ +- url: `${base}/1`, +- method: 'PATCH', +- headers: { 'Content-Type': 'application/json' }, +- data: { completed: true }, +- }); ++ const customRequest = await fetch(`${base}/1`, { ++ method: 'PATCH', ++ headers: { 'Content-Type': 'application/json' }, ++ body: JSON.stringify({ completed: true }), ++ }).then(async (res) => Object.assign(res, { data: await res.json() })); + console.log('\nPATCH /todos/1 ->', customRequest.status); + console.log('Preview:', customRequest.data?.completed !== undefined ? `completed=${customRequest.data.completed}` : JSON.stringify(customRequest.data).slice(0,200)); +``` + +## Unsupported APIs + +The codemod does not yet cover Axios features outside of direct request helpers, such as interceptors, cancel tokens, or instance configuration from `axios.create()`. + +## References + +- [Fetch Spec](https://fetch.spec.whatwg.org) +- [Axios Documentation](https://axios-http.com) +- [Node.js Documentation](https://nodejs.org/docs/latest/api/globals.html#fetch) diff --git a/recipes/axios-to-whatwg-fetch/codemod.yaml b/recipes/axios-to-whatwg-fetch/codemod.yaml new file mode 100644 index 00000000..de96e3f6 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/codemod.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: "@nodejs/axios-to-whatwg-fetch" +version: 1.0.0 +description: Replace `axios` with `fetch` +author: Bruno Rodrigues +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public + +# needed for removing dependencies +capabilities: + - fs + - child_process diff --git a/recipes/axios-to-whatwg-fetch/package.json b/recipes/axios-to-whatwg-fetch/package.json new file mode 100644 index 00000000..5d30ca26 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/package.json @@ -0,0 +1,25 @@ +{ + "name": "@nodejs/axios-to-whatwg-fetch", + "version": "1.0.0", + "description": "Replace `axios` with `fetch`", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l tsx ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/axios-to-whatwg-fetch", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Bruno Rodrigues", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/axios-to-whatwg-fetch/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*", + "dedent": "^1.7.0" + } +} diff --git a/recipes/axios-to-whatwg-fetch/src/remove-dependencies.ts b/recipes/axios-to-whatwg-fetch/src/remove-dependencies.ts new file mode 100644 index 00000000..08e63227 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/src/remove-dependencies.ts @@ -0,0 +1,8 @@ +import removeDependencies from '@nodejs/codemod-utils/remove-dependencies'; + +/** + * Remove `chalk` and `@types/chalk` dependencies from package.json + */ +export default function removeAxiosDependencies(): string | null { + return removeDependencies(['axios']); +} diff --git a/recipes/axios-to-whatwg-fetch/src/workflow.ts b/recipes/axios-to-whatwg-fetch/src/workflow.ts new file mode 100644 index 00000000..2f0a876a --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/src/workflow.ts @@ -0,0 +1,532 @@ +import { EOL } from 'node:os'; +import dedent from 'dedent'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { + getNodeImportCalls, + getNodeImportStatements, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding'; +import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; +import type { + Edit, + Range, + Rule, + SgNode, + SgRoot, +} from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +type BindingToReplace = { + rule: Rule; + node: SgNode; + binding: string; + replaceFn: (arg: SgNode[], context: WarningContext) => string; +}; + +type WarningContext = { + root: SgRoot; + match: SgNode; +}; + +type CreateOptionsType = { + oldOptions?: SgNode; + method?: string; + bodyNode?: SgNode | null; + payloadKind?: 'json' | 'form'; +}; + +const formatLocation = ({ + root, + node, +}: { + root: SgRoot; + node: SgNode; +}) => { + const { line, column } = node.range().start; + return `${root.filename()}:${line + 1}:${column + 1}`; +}; + +const warnWithLocation = ( + context: WarningContext, + message: string, + node?: SgNode, +) => { + const location = formatLocation({ + root: context.root, + node: node ?? context.match, + }); + console.warn(`[Codemod] ${message} (at ${location})`); +}; + +const getObjectPropertyValue = ( + objectNode: SgNode, + propertyName: string, +) => { + if (objectNode.kind() !== 'object') return undefined; + const pair = objectNode.find({ + rule: { + kind: 'pair', + has: { + kind: 'property_identifier', + field: 'key', + regex: `^${propertyName}$`, + }, + }, + }); + + return pair?.field('value'); +}; + +const getBodyExpression = ( + bodyNode: SgNode, + payloadKind: NonNullable, +) => { + const source = bodyNode.text(); + const trimmed = source.trim(); + if (!trimmed || trimmed === 'undefined' || trimmed === 'null') return null; + + if (payloadKind === 'form') { + return getFormBodyExpression(bodyNode, source, trimmed); + } + + return `JSON.stringify(${source})`; +}; + +const getFormBodyExpression = ( + bodyNode: SgNode, + source: string, + trimmed: string, +) => { + if (bodyNode.kind() === 'object') { + return `new URLSearchParams(${source})`; + } + + // if it's already a FormData or URLSearchParams instance, return as is + // we only check for common instantiation patterns here maybe add complex ones later + if ( + trimmed.startsWith('new URLSearchParams') || + trimmed.startsWith('URLSearchParams(') + ) { + return source; + } + + if (/FormData/.test(trimmed)) { + return source; + } + + return dedent` + (() => { + const value = ${source}; + if (value instanceof FormData || value instanceof URLSearchParams) return value; + return new URLSearchParams(value); + })() + `; +}; + +const baseUpdates: { + oldBind: string; + replaceFn: BindingToReplace['replaceFn']; + supportDefaultAccess?: boolean; +}[] = [ + { + oldBind: '$.get', + replaceFn: (args, context) => { + const [url, oldOptions] = args; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.get. Skipping.'); + return ''; + } + const options = createOptions({ oldOptions }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.post', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.post. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'POST', + bodyNode: args[1] ?? null, + payloadKind: 'json', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.put', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.put. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'PUT', + bodyNode: args[1] ?? null, + payloadKind: 'json', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.patch', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.patch. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'PATCH', + bodyNode: args[1] ?? null, + payloadKind: 'json', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.postForm', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.postForm. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'POST', + bodyNode: args[1] ?? null, + payloadKind: 'form', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.putForm', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.putForm. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'PUT', + bodyNode: args[1] ?? null, + payloadKind: 'form', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.patchForm', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.patchForm. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[2], + method: 'PATCH', + bodyNode: args[1] ?? null, + payloadKind: 'form', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.delete', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.delete. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[1], + method: 'DELETE', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}, ${options}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.head', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.head. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[1], + method: 'HEAD', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}, ${options}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.options', + replaceFn: (args, context) => { + const url = args[0]; + if (!url) { + warnWithLocation(context, 'Missing URL in axios.options. Skipping.'); + return ''; + } + const options = createOptions({ + oldOptions: args[1], + method: 'OPTIONS', + }); + return dedent.withOptions({ alignValues: true })` + fetch(${url.text()}, ${options}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, + { + oldBind: '$.request', + replaceFn: (args, context) => { + const config = args[0]; + if (!config) { + warnWithLocation( + context, + 'Missing config object in axios.request. Skipping.', + ); + return ''; + } + + if (config.kind() !== 'object') { + warnWithLocation( + context, + 'Unsupported axios.request configuration shape. Skipping migration.', + config, + ); + return ''; + } + + const urlNode = getObjectPropertyValue(config, 'url'); + if (!urlNode) { + warnWithLocation( + context, + 'Missing URL in axios.request config. Skipping migration.', + config, + ); + return ''; + } + const url = urlNode.text(); + + const methodNode = getObjectPropertyValue(config, 'method'); + + const method = methodNode.child(1)?.text().toUpperCase(); + + const options = createOptions({ + oldOptions: config, + method: method ?? 'GET', // axios.request's default is GET + bodyNode: getObjectPropertyValue(config, 'data') ?? null, + payloadKind: 'json', + }); + + return dedent.withOptions({ alignValues: true })` + fetch(${url}${options ? `, ${options}` : ''}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null) + `; + }, + }, +]; + +const updates = baseUpdates.flatMap((update) => { + const bindings = [update.oldBind]; + if ( + update.supportDefaultAccess !== false && + !update.oldBind.includes('.default.') + ) { + bindings.push(update.oldBind.replace('$.', '$.default.')); + } + + return bindings.map((binding) => ({ + oldBind: binding, + replaceFn: update.replaceFn, + })); +}); + +/** + * Generates options for the Fetch API based on the provided parameters. + * + * @param {Object} param0 - The parameters for creating options. + * @param {SgNode} [param0.oldOptions] - The old options node to extract headers from. + * @param {string} [param0.method] - The HTTP method to use (e.g., 'POST', 'GET'). + * @param {string} [param0.body] - The body content to include in the request. + * @returns {string} The generated options string for the Fetch API. + */ +const createOptions = ({ + oldOptions, + method, + bodyNode, + payloadKind = 'json', +}: CreateOptionsType) => { + const bodySource = bodyNode?.text(); + const hasBody = Boolean(bodySource?.trim()); + if (!oldOptions && !method && !hasBody) return ''; + + const headers = oldOptions?.find({ + rule: { + kind: 'object', + inside: { + kind: 'pair', + has: { + kind: 'property_identifier', + field: 'key', + regex: 'headers', + }, + }, + }, + }); + + // Build the options object string with proper formatting + const optionParts: string[] = []; + + if (method) { + optionParts.push(`\tmethod: "${method}"`); + } + + if (headers) { + optionParts.push(`\theaders: ${headers.text()}`); + } + + if (bodyNode) { + const bodyExpression = getBodyExpression(bodyNode, payloadKind); + if (bodyExpression) { + // Indent multi-line body expressions properly + const indentedBody = bodyExpression + .split(EOL) + .map((line, i) => (i === 0 ? line : `\t${line}`)) + .join(EOL); + optionParts.push(`\tbody: ${indentedBody}`); + } + } + + if (optionParts.length === 0) return ''; + + if (optionParts.length === 1) { + // Extract the property without leading tab for single-property objects + const property = optionParts[0].trim(); + return `{ ${property} }`; + } + + // Multi-line formatting with proper dedent + return `{${EOL}${optionParts.join(`,${EOL}`)}${EOL}}`; +}; + +/** + * Transforms the AST root by replacing axios bindings with Fetch API calls. + * + * @param {SgRoot} root - The root of the AST to transform. + * @returns {string | null} The transformed source code or null if no changes were made. + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + const bindsToReplace: BindingToReplace[] = []; + + const importRequireStatement = [ + ...getNodeRequireCalls(root, 'axios'), + ...getNodeImportStatements(root, 'axios'), + ...getNodeImportCalls(root, 'axios'), + ]; + + if (!importRequireStatement.length) return null; + + for (const node of importRequireStatement) { + for (const update of updates) { + const bind = resolveBindingPath(node, update.oldBind); + + if (!bind) continue; + + bindsToReplace.push({ + rule: { + pattern: `${bind}($$$ARG)`, + }, + node, + binding: bind, + replaceFn: update.replaceFn, + }); + } + } + + for (const bind of bindsToReplace) { + const matches = rootNode.findAll({ + rule: bind.rule, + }); + + for (const match of matches) { + const argsAndCommaas = match.getMultipleMatches('ARG'); + const args = argsAndCommaas.filter((arg) => arg.text() !== ','); + + const replace = match.replace(bind.replaceFn(args, { root, match })); + edits.push(replace); + + const result = removeBinding(bind.node, bind.binding.split('.').at(0)); + + if (result?.lineToRemove) { + linesToRemove.push(result.lineToRemove); + } + + if (result?.edit) { + edits.push(result.edit); + } + } + } + + if (!edits.length) return null; + + const sourceCode = rootNode.commitEdits(edits); + + return removeLines(sourceCode, linesToRemove); +} diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/component.tsx b/recipes/axios-to-whatwg-fetch/tests/expected/component.tsx new file mode 100644 index 00000000..9ef9eb66 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/component.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +type Todo = { id: number; title: string }; + +export function TodoList() { + const [todos, setTodos] = useState([]); + + useEffect(() => { + let active = true; + + fetch('/api/todos') + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null) + .then((response) => { + if (active) { + setTodos(response.data.todos); + } + }) + .catch(() => {}); + + return () => { + active = false; + }; + }, []); + + return
{todos.length}
; +} diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/delete-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/delete-with-config.js new file mode 100644 index 00000000..72eeb69d --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/delete-with-config.js @@ -0,0 +1,8 @@ + +const deletedTodo = await fetch('https://dummyjson.com/todos/1', { + method: "DELETE", + headers: { 'Content-Type': 'application/json' } +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nDELETE /todos1/1 ->', deletedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/delete.js b/recipes/axios-to-whatwg-fetch/tests/expected/delete.js new file mode 100644 index 00000000..2d946ad6 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/delete.js @@ -0,0 +1,6 @@ +const base = 'https://dummyjson.com/todos/1'; + +const deletedTodo = await fetch(base, { method: "DELETE" }) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nDELETE /todos ->', deletedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/dynamic-import.js b/recipes/axios-to-whatwg-fetch/tests/expected/dynamic-import.js new file mode 100644 index 00000000..c2ed881d --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/dynamic-import.js @@ -0,0 +1,7 @@ + +export async function fetchTodos() { + const response = await fetch('https://dummyjson.com/todos') + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null); + return response.data; +} diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/get-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/get-with-config.js new file mode 100644 index 00000000..d4718e39 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/get-with-config.js @@ -0,0 +1,6 @@ + +const all = await fetch("https://dummyjson.com/todos", { headers: { "Content-Type": "application/json" } }) + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null); +console.log("\nGET /todos ->", all.status); +console.log(`Preview: ${all.data.todos.length} todos`); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/get.js b/recipes/axios-to-whatwg-fetch/tests/expected/get.js new file mode 100644 index 00000000..045d0c94 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/get.js @@ -0,0 +1,7 @@ +const base = "https://dummyjson.com/todos"; + +const all = await fetch(base) + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null); +console.log("\nGET /todos ->", all.status); +console.log(`Preview: ${all.data.todos.length} todos`); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/head-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/head-with-config.js new file mode 100644 index 00000000..3d4c93a5 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/head-with-config.js @@ -0,0 +1,7 @@ + +const all = await fetch('https://dummyjson.com/todos', { + method: "HEAD", + headers: { 'Content-Type': 'application/json' } +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/head.js b/recipes/axios-to-whatwg-fetch/tests/expected/head.js new file mode 100644 index 00000000..49e7fc17 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/head.js @@ -0,0 +1,4 @@ + +const all = await fetch('https://dummyjson.com/todos', { method: "HEAD" }) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/import-alias.js b/recipes/axios-to-whatwg-fetch/tests/expected/import-alias.js new file mode 100644 index 00000000..3db6070d --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/import-alias.js @@ -0,0 +1,9 @@ + +async function loadTodo() { + const response = await fetch('/todos/1') + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null); + return response.data; +} + +export { loadTodo }; diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/options-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/options-with-config.js new file mode 100644 index 00000000..5d58d34a --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/options-with-config.js @@ -0,0 +1,7 @@ + +const all = await fetch('https://dummyjson.com/todos', { + method: "OPTIONS", + headers: { 'Content-Type': 'application/json' } +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/options.js b/recipes/axios-to-whatwg-fetch/tests/expected/options.js new file mode 100644 index 00000000..f75d7e49 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/options.js @@ -0,0 +1,4 @@ + +const all = await fetch('https://dummyjson.com/todos', { method: "OPTIONS" }) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/patch-form-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/patch-form-with-config.js new file mode 100644 index 00000000..caf14ae5 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/patch-form-with-config.js @@ -0,0 +1,11 @@ + +const patched = await fetch('https://dummyjson.com/forms/2', { + method: "PATCH", + headers: { + Accept: 'application/json', + }, + body: new URLSearchParams({ done: true }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log(patched.status); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/patch.js b/recipes/axios-to-whatwg-fetch/tests/expected/patch.js new file mode 100644 index 00000000..aef034aa --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/patch.js @@ -0,0 +1,14 @@ + +// Unsupported method: axios.patch +const base = 'https://dummyjson.com/todos/1'; + +const patchedTodo = await fetch(base, { + method: "PATCH", + body: JSON.stringify({ + todo: 'Updated todo', + completed: true, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nPATCH /todos/1 ->', patchedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/post-form-formdata.js b/recipes/axios-to-whatwg-fetch/tests/expected/post-form-formdata.js new file mode 100644 index 00000000..1f59b0d6 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/post-form-formdata.js @@ -0,0 +1,14 @@ + +const formData = new FormData(); +formData.append('name', 'Node.js'); + +await fetch('https://dummyjson.com/forms', { + method: "POST", + body: (() => { + const value = formData; + if (value instanceof FormData || value instanceof URLSearchParams) return value; + return new URLSearchParams(value); + })() +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/post-form.js b/recipes/axios-to-whatwg-fetch/tests/expected/post-form.js new file mode 100644 index 00000000..42d2e8c6 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/post-form.js @@ -0,0 +1,12 @@ +const base = 'https://dummyjson.com/forms'; + +const created = await fetch(`${base}/submit`, { + method: "POST", + body: new URLSearchParams({ + title: 'Form Demo', + completed: false, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log(created); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/post-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/post-with-config.js new file mode 100644 index 00000000..4a7d9b91 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/post-with-config.js @@ -0,0 +1,14 @@ +const base = 'https://dummyjson.com/todos/add'; + +const createdTodo = await fetch(base, { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nPOST /todos/add ->', createdTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/post.js b/recipes/axios-to-whatwg-fetch/tests/expected/post.js new file mode 100644 index 00000000..5c7e4b0c --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/post.js @@ -0,0 +1,13 @@ +const base = 'https://dummyjson.com/todos/add'; + +const todoCreated = await fetch(base, { + method: "POST", + body: JSON.stringify({ + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nPOST /todos ->', todoCreated); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/put-form.js b/recipes/axios-to-whatwg-fetch/tests/expected/put-form.js new file mode 100644 index 00000000..02347342 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/put-form.js @@ -0,0 +1,14 @@ + +const payload = { status: 'open' }; + +const updated = await fetch('https://dummyjson.com/forms/1', { + method: "PUT", + body: (() => { + const value = payload; + if (value instanceof FormData || value instanceof URLSearchParams) return value; + return new URLSearchParams(value); + })() +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log(updated.status); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/put-with-config.js b/recipes/axios-to-whatwg-fetch/tests/expected/put-with-config.js new file mode 100644 index 00000000..4ed19d5f --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/put-with-config.js @@ -0,0 +1,14 @@ +const base = 'https://dummyjson.com/todos/1'; + +const updatedTodo = await fetch(base, { + method: "PUT", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nPUT /todos/1 ->', updatedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/put.js b/recipes/axios-to-whatwg-fetch/tests/expected/put.js new file mode 100644 index 00000000..127f1d81 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/put.js @@ -0,0 +1,13 @@ +const base = 'https://dummyjson.com/todos/1'; + +const updatedTodo = await fetch(base, { + method: "PUT", + body: JSON.stringify({ + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nPUT /todos/1 ->', updatedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/request.js b/recipes/axios-to-whatwg-fetch/tests/expected/request.js new file mode 100644 index 00000000..0ad2d0ae --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/request.js @@ -0,0 +1,13 @@ + +const base = 'https://dummyjson.com/todos/1'; + +const customRequest = await fetch(base, { + method: "PATCH", + body: JSON.stringify({ + todo: 'Updated todo', + completed: true, + }) +}) + .then(async (resp) => Object.assign(resp, { data: await resp.json() })) + .catch(() => null); +console.log('\nREQUEST /todos/1 ->', customRequest); diff --git a/recipes/axios-to-whatwg-fetch/tests/expected/require.js b/recipes/axios-to-whatwg-fetch/tests/expected/require.js new file mode 100644 index 00000000..e8b4f4a1 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/expected/require.js @@ -0,0 +1,8 @@ + +function fetchAllTodos() { + return fetch('https://dummyjson.com/todos') + .then(async (res) => Object.assign(res, { data: await res.json() })) + .catch(() => null); +} + +module.exports = { fetchAllTodos }; diff --git a/recipes/axios-to-whatwg-fetch/tests/input/component.tsx b/recipes/axios-to-whatwg-fetch/tests/input/component.tsx new file mode 100644 index 00000000..c7c38fa0 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/component.tsx @@ -0,0 +1,27 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +type Todo = { id: number; title: string }; + +export function TodoList() { + const [todos, setTodos] = useState([]); + + useEffect(() => { + let active = true; + + axios + .get('/api/todos') + .then((response) => { + if (active) { + setTodos(response.data.todos); + } + }) + .catch(() => {}); + + return () => { + active = false; + }; + }, []); + + return
{todos.length}
; +} diff --git a/recipes/axios-to-whatwg-fetch/tests/input/delete-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/delete-with-config.js new file mode 100644 index 00000000..5219afa7 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/delete-with-config.js @@ -0,0 +1,6 @@ +import axios from 'axios'; + +const deletedTodo = await axios.delete('https://dummyjson.com/todos/1', { + headers: { 'Content-Type': 'application/json' }, +}); +console.log('\nDELETE /todos1/1 ->', deletedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/delete.js b/recipes/axios-to-whatwg-fetch/tests/input/delete.js new file mode 100644 index 00000000..95d91c94 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/delete.js @@ -0,0 +1,5 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/todos/1'; + +const deletedTodo = await axios.delete(base); +console.log('\nDELETE /todos ->', deletedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/dynamic-import.js b/recipes/axios-to-whatwg-fetch/tests/input/dynamic-import.js new file mode 100644 index 00000000..45350709 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/dynamic-import.js @@ -0,0 +1,6 @@ +const axiosModule = await import('axios'); + +export async function fetchTodos() { + const response = await axiosModule.default.get('https://dummyjson.com/todos'); + return response.data; +} diff --git a/recipes/axios-to-whatwg-fetch/tests/input/get-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/get-with-config.js new file mode 100644 index 00000000..f6103278 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/get-with-config.js @@ -0,0 +1,7 @@ +import axios from "axios"; + +const all = await axios.get("https://dummyjson.com/todos", { + headers: { "Content-Type": "application/json" }, +}); +console.log("\nGET /todos ->", all.status); +console.log(`Preview: ${all.data.todos.length} todos`); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/get.js b/recipes/axios-to-whatwg-fetch/tests/input/get.js new file mode 100644 index 00000000..6efc338f --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/get.js @@ -0,0 +1,6 @@ +import axios from "axios"; +const base = "https://dummyjson.com/todos"; + +const all = await axios.get(base); +console.log("\nGET /todos ->", all.status); +console.log(`Preview: ${all.data.todos.length} todos`); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/head-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/head-with-config.js new file mode 100644 index 00000000..fea04ab6 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/head-with-config.js @@ -0,0 +1,5 @@ +import axios from 'axios'; + +const all = await axios.head('https://dummyjson.com/todos', { + headers: { 'Content-Type': 'application/json' }, +}); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/head.js b/recipes/axios-to-whatwg-fetch/tests/input/head.js new file mode 100644 index 00000000..af8e6368 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/head.js @@ -0,0 +1,3 @@ +import axios from 'axios'; + +const all = await axios.head('https://dummyjson.com/todos'); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/import-alias.js b/recipes/axios-to-whatwg-fetch/tests/input/import-alias.js new file mode 100644 index 00000000..18ba71a5 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/import-alias.js @@ -0,0 +1,8 @@ +import ax from 'axios'; + +async function loadTodo() { + const response = await ax.get('/todos/1'); + return response.data; +} + +export { loadTodo }; diff --git a/recipes/axios-to-whatwg-fetch/tests/input/options-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/options-with-config.js new file mode 100644 index 00000000..11039542 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/options-with-config.js @@ -0,0 +1,5 @@ +import axios from 'axios'; + +const all = await axios.options('https://dummyjson.com/todos', { + headers: { 'Content-Type': 'application/json' }, +}); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/options.js b/recipes/axios-to-whatwg-fetch/tests/input/options.js new file mode 100644 index 00000000..9f46c4d4 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/options.js @@ -0,0 +1,3 @@ +import axios from 'axios'; + +const all = await axios.options('https://dummyjson.com/todos'); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/patch-form-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/patch-form-with-config.js new file mode 100644 index 00000000..f26b9c55 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/patch-form-with-config.js @@ -0,0 +1,12 @@ +import axios from 'axios'; + +const patched = await axios.patchForm( + 'https://dummyjson.com/forms/2', + { done: true }, + { + headers: { + Accept: 'application/json', + }, + }, +); +console.log(patched.status); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/patch.js b/recipes/axios-to-whatwg-fetch/tests/input/patch.js new file mode 100644 index 00000000..e4e92c2d --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/patch.js @@ -0,0 +1,10 @@ +import axios from 'axios'; + +// Unsupported method: axios.patch +const base = 'https://dummyjson.com/todos/1'; + +const patchedTodo = await axios.patch(base, { + todo: 'Updated todo', + completed: true, +}); +console.log('\nPATCH /todos/1 ->', patchedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/post-form-formdata.js b/recipes/axios-to-whatwg-fetch/tests/input/post-form-formdata.js new file mode 100644 index 00000000..c798815e --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/post-form-formdata.js @@ -0,0 +1,6 @@ +import axios from 'axios'; + +const formData = new FormData(); +formData.append('name', 'Node.js'); + +await axios.postForm('https://dummyjson.com/forms', formData); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/post-form.js b/recipes/axios-to-whatwg-fetch/tests/input/post-form.js new file mode 100644 index 00000000..499347c7 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/post-form.js @@ -0,0 +1,8 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/forms'; + +const created = await axios.postForm(`${base}/submit`, { + title: 'Form Demo', + completed: false, +}); +console.log(created); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/post-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/post-with-config.js new file mode 100644 index 00000000..f887b607 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/post-with-config.js @@ -0,0 +1,15 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/todos/add'; + +const createdTodo = await axios.post( + base, + { + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, +); +console.log('\nPOST /todos/add ->', createdTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/post.js b/recipes/axios-to-whatwg-fetch/tests/input/post.js new file mode 100644 index 00000000..2db408de --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/post.js @@ -0,0 +1,9 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/todos/add'; + +const todoCreated = await axios.post(base, { + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, +}); +console.log('\nPOST /todos ->', todoCreated); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/put-form.js b/recipes/axios-to-whatwg-fetch/tests/input/put-form.js new file mode 100644 index 00000000..2acceecf --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/put-form.js @@ -0,0 +1,6 @@ +import axios from 'axios'; + +const payload = { status: 'open' }; + +const updated = await axios.putForm('https://dummyjson.com/forms/1', payload); +console.log(updated.status); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/put-with-config.js b/recipes/axios-to-whatwg-fetch/tests/input/put-with-config.js new file mode 100644 index 00000000..efc0e103 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/put-with-config.js @@ -0,0 +1,15 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/todos/1'; + +const updatedTodo = await axios.put( + base, + { + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, +); +console.log('\nPUT /todos/1 ->', updatedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/put.js b/recipes/axios-to-whatwg-fetch/tests/input/put.js new file mode 100644 index 00000000..d2c98b6d --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/put.js @@ -0,0 +1,9 @@ +import axios from 'axios'; +const base = 'https://dummyjson.com/todos/1'; + +const updatedTodo = await axios.put(base, { + todo: 'Use DummyJSON in the project', + completed: false, + userId: 5, +}); +console.log('\nPUT /todos/1 ->', updatedTodo); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/request.js b/recipes/axios-to-whatwg-fetch/tests/input/request.js new file mode 100644 index 00000000..edb41798 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/request.js @@ -0,0 +1,13 @@ +import axios from 'axios'; + +const base = 'https://dummyjson.com/todos/1'; + +const customRequest = await axios.request({ + url: base, + method: 'PATCH', + data: { + todo: 'Updated todo', + completed: true, + }, +}); +console.log('\nREQUEST /todos/1 ->', customRequest); diff --git a/recipes/axios-to-whatwg-fetch/tests/input/require.js b/recipes/axios-to-whatwg-fetch/tests/input/require.js new file mode 100644 index 00000000..f18a42b5 --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/tests/input/require.js @@ -0,0 +1,7 @@ +const axios = require('axios'); + +function fetchAllTodos() { + return axios.get('https://dummyjson.com/todos'); +} + +module.exports = { fetchAllTodos }; diff --git a/recipes/axios-to-whatwg-fetch/workflow.yaml b/recipes/axios-to-whatwg-fetch/workflow.yaml new file mode 100644 index 00000000..fb3f68bc --- /dev/null +++ b/recipes/axios-to-whatwg-fetch/workflow.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Replace `axios` with `fetch` + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + + - id: remove-dependencies + name: Remove axios dependency + type: automatic + steps: + - name: Detect package manager and remove axios dependency + js-ast-grep: + js_file: src/remove-dependencies.ts + base_path: . + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: typescript + capabilities: + - child_process + - fs diff --git a/recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts b/recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts index 34df1387..8ce2f40b 100644 --- a/recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts +++ b/recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts @@ -3,17 +3,17 @@ import { URL } from 'node:url'; import { bar } from '@dep/bar'; import { foo } from 'foo'; -import { Bird } from './Bird'; +import { Bird } from './Bird/index.ts'; import { Cat } from './Cat.ts'; -import { Dog } from '…/Dog/index.mjs'; +import { Dog } from '…/Dog/index.mts'; import { baseUrl } from '#config.js'; -import { qux } from './qux.js'; +import { qux } from './qux.js/index.ts'; -export { Zed } from './zed'; +export type { Zed } from './zed.d.ts'; // should.js be unchanged -const nil = await import('./nil.js'); +const nil = await import('./nil.ts'); const bird = new Bird('Tweety'); const cat = new Cat('Milo');