diff --git a/package-lock.json b/package-lock.json index d846d1e5..2b5e31f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1501,6 +1501,10 @@ "resolved": "recipes/util-print-to-console-log", "link": true }, + "node_modules/@nodejs/zlib-bytesread-to-byteswritten": { + "resolved": "recipes/zlib-bytesread-to-byteswritten", + "link": true + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -4335,6 +4339,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "recipes/zlib-bytesread-to-byteswritten": { + "name": "@nodejs/zlib-bytesread-to-byteswritten", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "utils": { "name": "@nodejs/codemod-utils", "version": "0.0.0", diff --git a/recipes/zlib-bytesread-to-byteswritten/README.md b/recipes/zlib-bytesread-to-byteswritten/README.md new file mode 100644 index 00000000..6cc4bb65 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/README.md @@ -0,0 +1,139 @@ +# `zlib.bytesRead` → `zlib.bytesWritten` DEP0108 + +This codemod replaces zlib.bytesRead with zlib.bytesWritten for consistent stream property naming. It's useful to migrate code that uses the deprecated property which has been removed. + +It replaces zlib.bytesRead with zlib.bytesWritten in all zlib transform streams and it handles both CommonJS and ESM imports. + +See [DEP0108](https://nodejs.org/api/deprecations.html#DEP0108). + +--- + +## Example + +**Case 1** + +Before: + +```js +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +gzip.on("end", () => { + console.log("Bytes processed:", gzip.bytesRead); +}); +``` + +After: + +```js +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +gzip.on("end", () => { + console.log("Bytes processed:", gzip.bytesWritten); +}); +``` + +**Case 2** + +Before: + +```js +const zlib = require("node:zlib"); +const deflate = zlib.createDeflate(); +deflate.on("finish", () => { + const stats = { + input: deflate.bytesRead, + output: deflate.bytesWritten + }; +}); +``` + +After: + +```js +const zlib = require("node:zlib"); +const deflate = zlib.createDeflate(); +deflate.on("finish", () => { + const stats = { + input: deflate.bytesWritten, + output: deflate.bytesWritten + }; +}); +``` + +**Case 3** + +Before: + +```js +const zlib = require("node:zlib"); +function trackProgress(stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesRead} bytes`); + }, 1000); +} +``` + +After: + +```js +const zlib = require("node:zlib"); +function trackProgress(stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesWritten} bytes`); + }, 1000); +} +``` + +**Case 4** + +Before: + +```js +import { createGzip } from "node:zlib"; +const gzip = createGzip(); +const bytesProcessed = gzip.bytesRead; +``` + +After: + +```js +import { createGzip } from "node:zlib"; +const gzip = createGzip(); +const bytesProcessed = gzip.bytesWritten; +``` + +**Case 5** + +Before: + +```js +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +const processed = gzip.bytesRead; +``` + +After: + +```js +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +const processed = gzip.bytesWritten; +``` + +**Case 6** + +Before: + +```js +const { createGzip } = require("node:zlib"); +const gzip = createGzip(); +const bytes = gzip.bytesRead; +``` + +After: + +```js +const { createGzip } = require("node:zlib"); +const gzip = createGzip(); +const bytes = gzip.bytesWritten; +``` \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/codemod.yaml b/recipes/zlib-bytesread-to-byteswritten/codemod.yaml new file mode 100644 index 00000000..a3b381d1 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: "@nodejs/zlib-bytesread-to-byteswritten" +version: 1.0.0 +description: Handle DEP0108 by replacing deprecated `zlib.bytesRead` with `zlib.bytesWritten` in Node.js transform streams +author: Elie Khoury +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - zlib + - bytesRead + - bytesWritten + +registry: + access: public + visibility: public diff --git a/recipes/zlib-bytesread-to-byteswritten/package.json b/recipes/zlib-bytesread-to-byteswritten/package.json new file mode 100644 index 00000000..d9ee0d56 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/zlib-bytesread-to-byteswritten", + "version": "1.0.0", + "description": "Replace deprecated `zlib.bytesRead` with `zlib.bytesWritten` in Node.js transform streams", + "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/zlib-bytesread-to-byteswritten", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Elie Khoury", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/zlib-bytesread-to-byteswritten/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/zlib-bytesread-to-byteswritten/src/workflow.ts b/recipes/zlib-bytesread-to-byteswritten/src/workflow.ts new file mode 100644 index 00000000..04703cba --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/src/workflow.ts @@ -0,0 +1,118 @@ +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 { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; +import type { Edit, Range, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const ZLIB_FACTORIES = [ + 'createGzip', + 'createGunzip', + 'createDeflate', + 'createInflate', + 'createBrotliCompress', + 'createBrotliDecompress', + 'createUnzip', +]; + +const FUNC_KINDS = [ + 'function_declaration', + 'function_expression', + 'arrow_function', +] as const; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + // 1 Find all static and dynamic zlib imports/requires + const importNodes = [ + ...getNodeRequireCalls(root, 'node:zlib'), + ...getNodeImportStatements(root, 'node:zlib'), + ...getNodeImportCalls(root, 'node:zlib'), + ]; + + const factoryBindings = new Set(); + const streamVariables: string[] = []; + + // 1.a Resolve all local bindings from "node:zlib" + for (const node of importNodes) { + for (const factory of ZLIB_FACTORIES) { + const binding = resolveBindingPath(node, `$.${factory}`); + if (binding) factoryBindings.add(binding); + } + } + + // If no import is found that means we can skip transformation on this file + if (!importNodes.length) return null; + + // 2 Track variables assigned from factories (const, let, var) + for (const binding of factoryBindings) { + const matches = rootNode.findAll({ + rule: { + kind: 'variable_declarator', + has: { + field: 'value', + kind: 'call_expression', + pattern: `${binding}($$$ARGS)`, + }, + }, + }); + + for (const match of matches) { + const varMatch = match.field('name'); + + if (varMatch) { + const varName = varMatch.text(); + if (!streamVariables.includes(varName)) streamVariables.push(varName); + } + } + } + + // 3 Replace .bytesRead → .bytesWritten for tracked variables + for (const variable of streamVariables) { + const matches = rootNode.findAll({ + rule: { pattern: `${variable}.bytesRead` }, + }); + + for (const match of matches) { + edits.push( + match.replace(match.text().replace('.bytesRead', '.bytesWritten')), + ); + } + } + + // Step 4: Replace .bytesRead → .bytesWritten for function parameters + for (const kind of FUNC_KINDS) { + const funcs = rootNode.findAll({ rule: { kind } }); + + for (const func of funcs) { + const paramNames = func + .field('parameters') + ?.findAll({ rule: { kind: 'identifier' } }); + + for (const paramName of paramNames) { + // replace member_expressions that use ${paramName}.bytesRead inside the function context + const matches = func.findAll({ + rule: { + kind: 'member_expression', + pattern: `${paramName.text()}.bytesRead`, + }, + }); + for (const match of matches) { + edits.push( + match.replace(match.text().replace('.bytesRead', '.bytesWritten')), + ); + } + } + } + } + + if (!edits.length) return null; + + return removeLines(rootNode.commitEdits(edits), linesToRemove); +} diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/assigned_bytesRead_variable.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/assigned_bytesRead_variable.js new file mode 100644 index 00000000..da6adee1 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/assigned_bytesRead_variable.js @@ -0,0 +1,3 @@ +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +const processed = gzip.bytesWritten; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/deflate_finish_stats.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/deflate_finish_stats.js new file mode 100644 index 00000000..fb0bc7b2 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/deflate_finish_stats.js @@ -0,0 +1,8 @@ +const zlib = require("node:zlib"); +const deflate = zlib.createDeflate(); +deflate.on("finish", () => { + const stats = { + input: deflate.bytesWritten, + output: deflate.bytesWritten + }; +}); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/destructured_createGzip.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/destructured_createGzip.js new file mode 100644 index 00000000..2b2938ba --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/destructured_createGzip.js @@ -0,0 +1,3 @@ +const { createGzip } = require("node:zlib"); +const gzip = createGzip(); +const bytes = gzip.bytesWritten; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/dynamic_import.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/dynamic_import.js new file mode 100644 index 00000000..69dc8d09 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/dynamic_import.js @@ -0,0 +1,23 @@ +// CommonJS style dynamic import +async function testCommonJS() { + const zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesWritten); +} + +async function testCommonJSLet() { + let zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesWritten); +} + +async function testCommonJSVar() { + var zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesWritten); +} + +// ESM style dynamic import +const zlibESM = await import("node:zlib"); +const gzipESM = zlibESM.createGzip(); +console.log(gzipESM.bytesWritten); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/esm_createGzip_import.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/esm_createGzip_import.js new file mode 100644 index 00000000..c6c274b6 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/esm_createGzip_import.js @@ -0,0 +1,3 @@ +import { createGzip } from "node:zlib"; +const gzip = createGzip(); +const bytesProcessed = gzip.bytesWritten; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/gzip_end_event.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/gzip_end_event.js new file mode 100644 index 00000000..94411b9f --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/gzip_end_event.js @@ -0,0 +1,5 @@ +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +gzip.on("end", () => { + console.log("Bytes processed:", gzip.bytesWritten); +}); diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/let_var_streams.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/let_var_streams.js new file mode 100644 index 00000000..bfb29b6e --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/let_var_streams.js @@ -0,0 +1,9 @@ +const zlib = require("node:zlib"); + +// using let +let deflateStream = zlib.createDeflate(); +console.log(deflateStream.bytesWritten); + +// using var +var gzipStream = zlib.createGzip(); +console.log(gzipStream.bytesWritten); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress.js new file mode 100644 index 00000000..73489578 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress.js @@ -0,0 +1,6 @@ +const zlib = require("node:zlib"); +function trackProgress(stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesWritten} bytes`); + }, 1000); +} \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress_multiple_args.js b/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress_multiple_args.js new file mode 100644 index 00000000..fa409aa1 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/expected/track_stream_progress_multiple_args.js @@ -0,0 +1,7 @@ +const zlib = require("node:zlib"); +function trackProgress(test, stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesWritten} bytes`); + }, 1000); + console.log(test) +} diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/assigned_bytesRead_variable.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/assigned_bytesRead_variable.js new file mode 100644 index 00000000..ea1ad847 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/assigned_bytesRead_variable.js @@ -0,0 +1,3 @@ +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +const processed = gzip.bytesRead; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/deflate_finish_stats.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/deflate_finish_stats.js new file mode 100644 index 00000000..fe479432 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/deflate_finish_stats.js @@ -0,0 +1,8 @@ +const zlib = require("node:zlib"); +const deflate = zlib.createDeflate(); +deflate.on("finish", () => { + const stats = { + input: deflate.bytesRead, + output: deflate.bytesWritten + }; +}); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/destructured_createGzip.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/destructured_createGzip.js new file mode 100644 index 00000000..1c2f48a2 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/destructured_createGzip.js @@ -0,0 +1,3 @@ +const { createGzip } = require("node:zlib"); +const gzip = createGzip(); +const bytes = gzip.bytesRead; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/dynamic_import.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/dynamic_import.js new file mode 100644 index 00000000..ecfb7bd5 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/dynamic_import.js @@ -0,0 +1,23 @@ +// CommonJS style dynamic import +async function testCommonJS() { + const zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesRead); +} + +async function testCommonJSLet() { + let zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesRead); +} + +async function testCommonJSVar() { + var zlib = await import("node:zlib"); + const gzip = zlib.createGzip(); + console.log(gzip.bytesRead); +} + +// ESM style dynamic import +const zlibESM = await import("node:zlib"); +const gzipESM = zlibESM.createGzip(); +console.log(gzipESM.bytesRead); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/esm_createGzip_import.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/esm_createGzip_import.js new file mode 100644 index 00000000..0832352a --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/esm_createGzip_import.js @@ -0,0 +1,3 @@ +import { createGzip } from "node:zlib"; +const gzip = createGzip(); +const bytesProcessed = gzip.bytesRead; diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/gzip_end_event.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/gzip_end_event.js new file mode 100644 index 00000000..e65323a5 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/gzip_end_event.js @@ -0,0 +1,5 @@ +const zlib = require("node:zlib"); +const gzip = zlib.createGzip(); +gzip.on("end", () => { + console.log("Bytes processed:", gzip.bytesRead); +}); diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/let_var_streams.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/let_var_streams.js new file mode 100644 index 00000000..7baa22a5 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/let_var_streams.js @@ -0,0 +1,9 @@ +const zlib = require("node:zlib"); + +// using let +let deflateStream = zlib.createDeflate(); +console.log(deflateStream.bytesRead); + +// using var +var gzipStream = zlib.createGzip(); +console.log(gzipStream.bytesRead); \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress.js new file mode 100644 index 00000000..bbb59457 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress.js @@ -0,0 +1,6 @@ +const zlib = require("node:zlib"); +function trackProgress(stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesRead} bytes`); + }, 1000); +} \ No newline at end of file diff --git a/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress_multiple_args.js b/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress_multiple_args.js new file mode 100644 index 00000000..2c5085a9 --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/tests/input/track_stream_progress_multiple_args.js @@ -0,0 +1,7 @@ +const zlib = require("node:zlib"); +function trackProgress(test, stream) { + setInterval(() => { + console.log(`Progress: ${stream.bytesRead} bytes`); + }, 1000); + console.log(test) +} diff --git a/recipes/zlib-bytesread-to-byteswritten/workflow.yaml b/recipes/zlib-bytesread-to-byteswritten/workflow.yaml new file mode 100644 index 00000000..a6a482da --- /dev/null +++ b/recipes/zlib-bytesread-to-byteswritten/workflow.yaml @@ -0,0 +1,25 @@ +# 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: Handle DEP0108 by replacing deprecated `zlib.bytesRead` with `zlib.bytesWritten` in Node.js transform streams + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript