diff --git a/package-lock.json b/package-lock.json index 70afe966..9b86c00a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1453,6 +1453,10 @@ "node": ">=22 || ^20.6.0 || ^18.19.0" } }, + "node_modules/@nodejs/buffer-atob-btoa": { + "resolved": "recipes/buffer-atob-btoa", + "link": true + }, "node_modules/@nodejs/codemod-utils": { "resolved": "utils", "link": true @@ -4213,6 +4217,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "recipes/buffer-atob-btoa": { + "name": "@nodejs/buffer-atob-btoa", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + } + }, "recipes/correct-ts-specifiers": { "name": "@nodejs/correct-ts-specifiers", "version": "1.0.0", diff --git a/recipes/buffer-atob-btoa/README.md b/recipes/buffer-atob-btoa/README.md new file mode 100644 index 00000000..47b06355 --- /dev/null +++ b/recipes/buffer-atob-btoa/README.md @@ -0,0 +1,42 @@ +# Migrate legacy `buffer.atob()` and `buffer.btoa()` APIs + +Migrates usage of the legacy APIs `buffer.atob()` and `buffer.btoa()` to the current recommended approaches. + +## Example + +### Migrating buffer.atob(data) + +**Before:** +```js +const buffer = require('node:buffer'); +const data = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 +const decodedData = buffer.atob(data); +console.log(decodedData); // Outputs: Hello World! +``` + +**After:** +```js +const data = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 +const decodedData = Buffer.from(data, 'base64').toString('binary'); +console.log(decodedData); // Outputs: Hello World! +``` + +### Migrating buffer.btoa(data) + +**Before:** +```js +const buffer = require('node:buffer'); +const data = 'Hello World!'; +const encodedData = buffer.btoa(data); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh +``` + +**After:** +```js +const data = 'Hello World!'; +const encodedData = Buffer.from(data, 'binary').toString('base64'); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh +``` + +## REFS +* [Node.js Documentation: Buffer](https://nodejs.org/api/buffer.html) \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/codemod.yaml b/recipes/buffer-atob-btoa/codemod.yaml new file mode 100644 index 00000000..cb501dc9 --- /dev/null +++ b/recipes/buffer-atob-btoa/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/buffer-atob-btoa" +version: "1.0.0" +description: Migrates usage of the legacy APIs `buffer.atob()` and `buffer.btoa()` to the current recommended approaches +author: nekojanai (Jana) +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/buffer-atob-btoa/package.json b/recipes/buffer-atob-btoa/package.json new file mode 100644 index 00000000..071a4b32 --- /dev/null +++ b/recipes/buffer-atob-btoa/package.json @@ -0,0 +1,21 @@ +{ + "name": "@nodejs/buffer-atob-btoa", + "version": "1.0.0", + "description": "Migrates usage of the legacy APIs `buffer.atob()` and `buffer.btoa()` to the current recommended approaches", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/buffer-atob-btoa", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "nekojanai (Jana)", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/buffer-atob-btoa/README.md", + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/src/workflow.ts b/recipes/buffer-atob-btoa/src/workflow.ts new file mode 100644 index 00000000..45bc88f9 --- /dev/null +++ b/recipes/buffer-atob-btoa/src/workflow.ts @@ -0,0 +1,79 @@ +import type { Edit, Kinds, Range, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; +import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const bindingStatementFnTuples: [string, SgNode>, (arg: string) => string][] = []; + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + const updates = [ + { + oldBind: "$.atob", + replaceFn: (arg: string) => `Buffer.from(${arg}, 'base64').toString('binary')` + }, + { + oldBind: "$.btoa", + replaceFn: (arg: string) => `Buffer.from(${arg}, 'binary').toString('base64')` + } + ]; + + const statements = [...getNodeRequireCalls(root, 'buffer'), ...getNodeImportStatements(root, 'buffer')]; + + for (const statement of statements) { + for (const update of updates) { + const binding = resolveBindingPath(statement, update.oldBind); + if (binding) bindingStatementFnTuples.push([binding, statement, update.replaceFn]); + } + } + + for (const [binding, statement, fn] of bindingStatementFnTuples) { + + const result = removeBinding(statement, binding); + + if (result?.edit) edits.push(result.edit); + if (result?.lineToRemove) linesToRemove.push(result.lineToRemove); + + const calls = rootNode.findAll({ + rule: { + pattern: `${binding}($ARG)` + } + }); + + const otherCalls = rootNode.findAll({ + rule: { + all: [ + { + pattern: 'buffer.$FN' + }, + { + not: { + pattern: '$.btoa($ARG)' + } + }, + { + not: { + pattern: '$.atob($ARG)' + } + } + ] + } + }); + + for (const call of calls) { + const argMatch = call.getMatch("ARG"); + if (argMatch) edits.push(call.replace(fn(argMatch.text()))); + } + + if (calls.length === otherCalls.length) { + linesToRemove.push(statement.range()); + } + } + + return removeLines(rootNode.commitEdits(edits), linesToRemove); +} diff --git a/recipes/buffer-atob-btoa/tests/expected/file-00.js b/recipes/buffer-atob-btoa/tests/expected/file-00.js new file mode 100644 index 00000000..fd9cf97f --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-00.js @@ -0,0 +1,3 @@ +const data = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 +const decodedData = Buffer.from(data, 'base64').toString('binary'); +console.log(decodedData); // Outputs: Hello World! \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/expected/file-01.js b/recipes/buffer-atob-btoa/tests/expected/file-01.js new file mode 100644 index 00000000..62d152d2 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-01.js @@ -0,0 +1,3 @@ +const data = 'Hello World!'; +const encodedData = Buffer.from(data, 'binary').toString('base64'); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/expected/file-02.js b/recipes/buffer-atob-btoa/tests/expected/file-02.js new file mode 100644 index 00000000..62d152d2 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-02.js @@ -0,0 +1,3 @@ +const data = 'Hello World!'; +const encodedData = Buffer.from(data, 'binary').toString('base64'); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/expected/file-03.js b/recipes/buffer-atob-btoa/tests/expected/file-03.js new file mode 100644 index 00000000..62d152d2 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-03.js @@ -0,0 +1,3 @@ +const data = 'Hello World!'; +const encodedData = Buffer.from(data, 'binary').toString('base64'); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/expected/file-04.js b/recipes/buffer-atob-btoa/tests/expected/file-04.js new file mode 100644 index 00000000..a358057d --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-04.js @@ -0,0 +1,4 @@ +import buffer from "node:buffer"; +buffer.constants.MAX_LENGTH; +const data = 'Hello World!'; +Buffer.from(data, 'binary').toString('base64'); \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/expected/file-05.js b/recipes/buffer-atob-btoa/tests/expected/file-05.js new file mode 100644 index 00000000..7cb29f7a --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/expected/file-05.js @@ -0,0 +1,4 @@ +import { isUtf8 } from "node:buffer"; +const data = 'Hello World!'; +isUtf8(data); +Buffer.from(data, 'base64').toString('binary'); \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-00.js b/recipes/buffer-atob-btoa/tests/input/file-00.js new file mode 100644 index 00000000..8ca779f7 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-00.js @@ -0,0 +1,4 @@ +const buffer = require('node:buffer'); +const data = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 +const decodedData = buffer.atob(data); +console.log(decodedData); // Outputs: Hello World! \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-01.js b/recipes/buffer-atob-btoa/tests/input/file-01.js new file mode 100644 index 00000000..179ec7a5 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-01.js @@ -0,0 +1,4 @@ +const buffer = require('node:buffer'); +const data = 'Hello World!'; +const encodedData = buffer.btoa(data); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-02.js b/recipes/buffer-atob-btoa/tests/input/file-02.js new file mode 100644 index 00000000..6ed1e665 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-02.js @@ -0,0 +1,4 @@ +import buffer from "node:buffer"; +const data = 'Hello World!'; +const encodedData = buffer.btoa(data); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-03.js b/recipes/buffer-atob-btoa/tests/input/file-03.js new file mode 100644 index 00000000..4a195ce9 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-03.js @@ -0,0 +1,4 @@ +const buffer = require("node:buffer"); +const data = 'Hello World!'; +const encodedData = buffer.btoa(data); +console.log(encodedData); // Outputs: SGVsbG8gV29ybGQh \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-04.js b/recipes/buffer-atob-btoa/tests/input/file-04.js new file mode 100644 index 00000000..0d33acc1 --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-04.js @@ -0,0 +1,4 @@ +import buffer from "node:buffer"; +buffer.constants.MAX_LENGTH; +const data = 'Hello World!'; +buffer.btoa(data); \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/tests/input/file-05.js b/recipes/buffer-atob-btoa/tests/input/file-05.js new file mode 100644 index 00000000..ff65cf2a --- /dev/null +++ b/recipes/buffer-atob-btoa/tests/input/file-05.js @@ -0,0 +1,4 @@ +import { atob, isUtf8 } from "node:buffer"; +const data = 'Hello World!'; +isUtf8(data); +atob(data); \ No newline at end of file diff --git a/recipes/buffer-atob-btoa/workflow.yaml b/recipes/buffer-atob-btoa/workflow.yaml new file mode 100644 index 00000000..d71f6e03 --- /dev/null +++ b/recipes/buffer-atob-btoa/workflow.yaml @@ -0,0 +1,27 @@ +# 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 + runtime: + type: direct + steps: + - name: Migrates usage of the legacy APIs `buffer.atob()` and `buffer.btoa()` to the current recommended approaches + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript