diff --git a/pages/userland-migrations/axios-to-whatwg-fetch.md b/pages/userland-migrations/axios-to-whatwg-fetch.md new file mode 100644 index 0000000..1198dbe --- /dev/null +++ b/pages/userland-migrations/axios-to-whatwg-fetch.md @@ -0,0 +1,161 @@ +--- +authors: brunocroh, AugustinMauroy +--- + +# Axios to WHATWG Fetch + +Migrates code from the [Axios](https://axios-http.com) HTTP client to the [WHATWG Fetch](https://fetch.spec.whatwg.org) API that is natively available in Node.js as the global [`fetch`](https://nodejs.org/docs/latest/api/globals.html#fetch), reducing dependencies and improving performance. It rewrites every Axios request helper — `axios.request()`, `axios.get()`, `axios.delete()`, `axios.head()`, `axios.options()`, `axios.post()`, `axios.put()`, `axios.patch()`, `axios.postForm()`, `axios.putForm()`, and `axios.patchForm()` — and recognizes default ESM imports, aliased imports, CommonJS `require()` calls, and dynamic `import()`. Once all call sites are converted, it also removes the `axios` and `@types/axios` entries from `package.json`. + +## Usage + +Run this codemod with: + +```sh +npx codemod @nodejs/axios-to-whatwg-fetch +``` + +## Examples + +### GET request + +A plain `axios.get()` becomes a `fetch()` call with a shim that keeps the `response.data` property working. + +```diff +-import axios from "axios"; + 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 with a JSON body + +The `data` argument of `axios.post()` is serialized with `JSON.stringify()` and passed as the `body` option. + +```diff +-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, +-}); ++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); +``` + +### Form submission + +`axios.postForm()` (and the `putForm`/`patchForm` variants) send the payload as `URLSearchParams`. + +```diff +-import axios from 'axios'; + const base = 'https://dummyjson.com/forms'; + +-const created = await axios.postForm(`${base}/submit`, { +- title: 'Form Demo', +- completed: false, +-}); ++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); +``` + +### `axios.request()` with a config object + +The `url`, `method`, and `data` properties of the config object are mapped onto the `fetch()` call. + +```diff +-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, +- }, +-}); ++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); +``` + +### CommonJS `require()` + +CommonJS modules are handled the same way, and the now-unused `require('axios')` binding is removed. + +```diff +-const axios = require('axios'); + + function fetchAllTodos() { +- return axios.get('https://dummyjson.com/todos'); ++ return fetch('https://dummyjson.com/todos') ++ .then(async (res) => Object.assign(res, { data: await res.json() })) ++ .catch(() => null); + } + + module.exports = { fetchAllTodos }; +``` + +## Notes + +- A `fetch` response exposes its payload through `res.json()` rather than a `data` property, so each converted call is followed by `.then(async (res) => Object.assign(res, { data: await res.json() }))` to keep existing `response.data` accesses working. +- Converted calls end with `.catch(() => null)`, so a failed request resolves to `null` instead of rejecting. Also note that unlike Axios, `fetch` does not reject on HTTP error statuses (4xx/5xx), so error-handling code built around Axios rejections should be reviewed manually. +- Safety first: if any Axios call in a file uses an unsupported configuration option, the entire file is left untouched and a warning with the source location is printed, preserving the original behavior. +- After the transformation, the codemod detects your package manager and removes the `axios` and `@types/axios` dependencies from `package.json`. + +### Limitations + +The codemod skips files whose Axios calls use any of the following configuration options, because they have no direct `fetch` equivalent: + +- `beforeRedirect` +- `cancelToken` +- `decompress` +- `httpAgent` +- `httpsAgent` +- `maxBodyLength` +- `maxContentLength` +- `maxRedirects` +- `paramsSerializer` +- `signal` +- `socketPath` +- `timeout` +- `transformRequest` +- `transformResponse` +- `validateStatus` +- `withCredentials` + +It also does not cover Axios features outside of the direct request helpers, such as interceptors, cancel tokens, or instance configuration created with `axios.create()`. + + diff --git a/pages/userland-migrations/chalk-to-util-styletext.md b/pages/userland-migrations/chalk-to-util-styletext.md new file mode 100644 index 0000000..deca574 --- /dev/null +++ b/pages/userland-migrations/chalk-to-util-styletext.md @@ -0,0 +1,75 @@ +--- +authors: richiemccoll +--- + +# Chalk to `util.styleText()` + +Migrates usage of the `chalk` npm package to the Node.js built-in `util.styleText()` API. Replaces the `chalk` import with `{ styleText }` from `node:util` and rewrites all chalk method calls accordingly. Chained chalk styles are converted to an array of style strings. + +## Usage + +Run this codemod with: + +```sh +npx codemod @nodejs/chalk-to-util-styletext +``` + +## Examples + +### Example 1 + +Basic color methods (ESM default import) + +```diff +-import chalk from "chalk"; ++import { styleText } from "node:util"; + +-console.log(chalk.red("Error message")); +-console.log(chalk.green("Success message")); +-console.log(chalk.blue("Info message")); ++console.log(styleText("red", "Error message")); ++console.log(styleText("green", "Success message")); ++console.log(styleText("blue", "Info message")); +``` + +### Example 2 + +Chained styles + +```diff +-import chalk from "chalk"; ++import { styleText } from "node:util"; + +-console.log(chalk.red.bold("Error: Operation failed")); +-console.log(chalk.green.underline("Success: All tests passed")); +-console.log(chalk.yellow.bgBlack("Warning: Deprecated API usage")); ++console.log(styleText(["red", "bold"], "Error: Operation failed")); ++console.log(styleText(["green", "underline"], "Success: All tests passed")); ++console.log(styleText(["yellow", "bgBlack"], "Warning: Deprecated API usage")); +``` + +### Example 3 + +CommonJS `require` + +```diff +-const chalk = require("chalk"); ++const { styleText } = require("node:util"); + +-const error = chalk.red("Error"); +-const warning = chalk.yellow("Warning"); +-const info = chalk.blue("Info"); ++const error = styleText("red", "Error"); ++const warning = styleText("yellow", "Warning"); ++const info = styleText("blue", "Info"); + + console.log(error, warning, info); +``` + +## Notes + +### Limitations + +Chalk methods that have no direct `util.styleText` equivalent — including `hex()`, `rgb()`, `ansi256()`, `bgAnsi256()`, `visible()`, and `new chalk.Chalk()` — are skipped. A warning is printed for each unsupported call, and those call sites are left unchanged for manual review. + + diff --git a/pages/userland-migrations/correct-ts-specifiers.md b/pages/userland-migrations/correct-ts-specifiers.md new file mode 100644 index 0000000..d4f7c72 --- /dev/null +++ b/pages/userland-migrations/correct-ts-specifiers.md @@ -0,0 +1,101 @@ +--- +authors: JakobJingleheimer +--- + +# Correct TypeScript Specifiers + +Transforms import specifiers from the old `tsc` (TypeScript's compiler) requirement of using `.js` file extensions in source-code to import files that are actually TypeScript; the corrected specifiers enable source-code to be runnable by standards-compliant software like Node.js. This is a one-and-done process, and the updated source-code should be committed to your version control (eg git); thereafter, source-code import statements should be authored compliant with the ECMAScript (JavaScript) standard. + +Supported cases: + +- no file extension → `.cts`, `.mts`, `.js`, `.ts`, `.d.cts`, `.d.mts`, or `.d.ts` +- `.cjs` → `.cts`, `.mjs` → `.mts`, `.js` → `.ts` +- `.js` → `.d.cts`, `.d.mts`, or `.d.ts` +- [Package.json subpath imports](https://nodejs.org/api/packages.html#subpath-imports) +- [tsconfig paths](https://www.typescriptlang.org/tsconfig/#paths) (via [`@nodejs-loaders/alias`](https://github.com/JakobJingleheimer/nodejs-loaders/blob/main/packages/alias?tab=readme-ov-file)) + - In order to subsequently run code via node, you will need to add this (or another) loader to your own project. Or, switch to [subimports](https://nodejs.org/api/packages.html#subpath-imports). +- Commonjs-like directory specifiers + +## Usage + +> [!CAUTION] +> This will change your source-code. Commit any unsaved changes before running this package. + +> [!IMPORTANT] +> [`--experimental-import-meta-resolve`](https://nodejs.org/api/cli.html#--experimental-import-meta-resolve) MUST be enabled; the feature is not really experimental—it's nonstandard because it's not relevant for browsers. + +Run this codemod with: + +```sh +NODE_OPTIONS="--experimental-import-meta-resolve" \ + npx codemod @nodejs/correct-ts-specifiers +``` + +### Monorepos + +For best results, run this _within_ each workspace of the monorepo. + +```text +project-root/ + ├ workspaces/ + ├ foo/ ←--------- RUN HERE + ├ … + ├ package.json + └ tsconfig.json + └ bar/ ←--------- RUN HERE + ├ … + ├ package.json + └ tsconfig.json + └ utils/ ←--------- RUN HERE + ├ qux.js + └ zed.js +``` + +## Examples + +```diff + 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'; + +-const nil = await import('./nil.js'); ++const nil = await import('./nil.ts'); +``` + +> [!TIP] +> Those using `tsc` to compile will need to enable [`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions); using `tsc` for only type-checking (ex via a lint/test step like `npm run test:types`) needs [`allowImportingTsExtensions`](https://www.typescriptlang.org/tsconfig/#allowImportingTsExtensions) (and some additional compile options—see the cited documentation); + +## Notes + +This package does not just blindly find & replace file extensions within specifiers: It confirms that the replacement specifier actually exists; in ambiguous cases (such as two files with the same basename in the same location but different relevant file extensions like `/tmp/foo.js` and `/tmp/foo.ts`), it logs an error, skips that specifier, and continues processing. + +> [!CAUTION] +> This package does not confirm that imported modules contain the desired export(s). This _shouldn't_ actually ever result in a problem because ambiguous cases are skipped (so if there is a problem, it existed before the migration started). Merely running your source-code after the migration completes will confirm all is well (if there are problems, node will error, citing the problems). + +> [!TIP] +> Node.js requires the `type` keyword be present on type imports. For own code, this package usually handles that. However, in some cases and for node modules, it does not. Robust tooling already exists that will automatically fix this, such as +> +> - [`use-import-type` via biome](https://biomejs.dev/linter/rules/use-import-type/) +> - [`typescript/no-import-type-side-effects` via oxlint](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-import-type-side-effects) +> - [`consistent-type-imports` via typescript-lint](https://typescript-eslint.io/rules/consistent-type-imports) +> +> If your source code needs that, first run this codemod and then one of those fixers. + +### Limitations + +When both a `.js` file and a corresponding `.ts` file exist at the same path, the codemod cannot determine which one the specifier refers to. In that case it logs an error, leaves the specifier unchanged, and continues processing the rest of the file. + + diff --git a/pages/userland-migrations/mocha-to-node-test-runner.md b/pages/userland-migrations/mocha-to-node-test-runner.md new file mode 100644 index 0000000..5c2b38c --- /dev/null +++ b/pages/userland-migrations/mocha-to-node-test-runner.md @@ -0,0 +1,150 @@ +--- +authors: Xstoudi +--- + +# Mocha to Node.js Test Runner + +Migrates [Mocha](https://mochajs.org/) 8.x test suites to the built-in [Node.js test runner](https://nodejs.org/api/test.html) (`node:test`, available in Node.js 22.x and 24.x). It adds the required `node:test` imports for the globals a file uses (`describe`, `it`, `before`, `after`, `beforeEach`, `afterEach`), converts `done` callbacks to the `(t, done)` signature, rewrites `this.skip()` to `t.skip()` and `this.timeout(N)` to `{ timeout: N }` options, and preserves the original function style (it never converts between `function()` and arrow functions). Both CommonJS and ESM files are supported, and the `mocha` and `@types/mocha` dependencies are removed from `package.json` afterwards. + +## Usage + +Run this codemod with: + +```sh +npx codemod @nodejs/mocha-to-node-test-runner +``` + +## Examples + +### Adding `node:test` imports (CommonJS) + +Global `describe`/`it` usage keeps working once the matching `require('node:test')` is inserted; modifiers like `describe.skip` are already compatible. + +```diff + const assert = require('assert'); ++const { describe, it } = require('node:test'); + + describe('Array', function() { + describe.skip('#indexOf()', function() { + it('should return -1 when the value is not present', function() { + const arr = [1, 2, 3]; + assert.strictEqual(arr.indexOf(4), -1); + }); + }); + }); +``` + +### Adding `node:test` imports (ESM) + +In ESM files an `import` statement is inserted instead. + +```diff + import assert from 'assert'; ++import { describe, it } from 'node:test'; + + describe('Array', function() { + describe.skip('#indexOf()', function() { + it('should return -1 when the value is not present', function() { +``` + +### Hooks + +Only the hooks actually used in the file are added to the import list. + +```diff + const assert = require('assert'); + const fs = require('fs'); ++const { describe, before, after, it } = require('node:test'); + + describe('File System', () => { + before(function() { + fs.writeFileSync('test.txt', 'Hello, World!'); + }); + + after(() => { + fs.unlinkSync('test.txt'); + }); +``` + +### `done` callbacks + +Mocha passes `done` as the first callback argument; `node:test` passes the test context first, so `(done)` becomes `(t, done)`. + +```diff + const assert = require('assert'); ++const { describe, it } = require('node:test'); + + describe('Callback Test', function() { +- it('should call done when complete', function(done) { ++ it('should call done when complete', function(t, done) { + setTimeout(() => { + assert.strictEqual(1 + 1, 2); + done(); + }, 100); + }); + }); +``` + +### Skipping with `this.skip()` + +`this.skip()` becomes `t.skip()`, with the test context parameter `t` added to the callback signature as needed. + +```diff + const assert = require('assert'); ++const { describe, it } = require('node:test'); + + describe('Skipped Test', () => { + it.skip('should not run this test', () => { + assert.strictEqual(1 + 1, 3); + }); +- it('should also be skipped', () => { +- this.skip(); ++ it('should also be skipped', (t) => { ++ t.skip(); + assert.strictEqual(1 + 1, 3); + }); + +- it('should also be skipped 2', (done) => { +- this.skip(); ++ it('should also be skipped 2', (t, done) => { ++ t.skip(); + assert.strictEqual(1 + 1, 3); + }); + }); +``` + +### Timeouts + +`this.timeout(N)` calls on suites and tests move into the `{ timeout: N }` options argument. + +```diff + const assert = require('assert'); ++const { describe, it } = require('node:test'); + +-describe('Timeout Test', function() { +- this.timeout(500); ++describe('Timeout Test', { timeout: 500 }, function() { + +- it('should complete within 100ms', (done) => { +- this.timeout(100); ++ it('should complete within 100ms', { timeout: 100 }, (t, done) => { + setTimeout(done, 500); // This will fail + }); + +- it('should complete within 200ms', function(done) { +- this.timeout(200); ++ it('should complete within 200ms', { timeout: 200 }, function(t, done) { + setTimeout(done, 100); // This will pass + }); + }); +``` + +## Notes + +- After the transformation, the codemod detects your package manager and removes the `mocha` and `@types/mocha` dependencies from `package.json`. + +### Limitations + +- `node:test` does not support Mocha's `retry` option, so tests relying on it need to be handled separately. + + diff --git a/site.json b/site.json index bea6403..563fe56 100644 --- a/site.json +++ b/site.json @@ -527,6 +527,27 @@ ] } ] + }, + { + "groupName": "Userland Migrations", + "items": [ + { + "link": "/learn/userland-migrations/axios-to-whatwg-fetch", + "label": "Axios to WHATWG Fetch" + }, + { + "link": "/learn/userland-migrations/chalk-to-util-styletext", + "label": "Chalk to util.styleText()" + }, + { + "link": "/learn/userland-migrations/correct-ts-specifiers", + "label": "Correct TypeScript Specifiers" + }, + { + "link": "/learn/userland-migrations/mocha-to-node-test-runner", + "label": "Mocha to Node.js Test Runner" + } + ] } ] }