Skip to content

refactor: decompose complex functions in utils and benchmark#2780

Merged
srod merged 5 commits intodevelopfrom
refactor/p2-decompose-complex-functions
Jan 15, 2026
Merged

refactor: decompose complex functions in utils and benchmark#2780
srod merged 5 commits intodevelopfrom
refactor/p2-decompose-complex-functions

Conversation

@srod
Copy link
Owner

@srod srod commented Jan 13, 2026

Summary

  • Decompose resolveCompressor into 3 focused functions
  • Decompose benchmarkCompressor into 5 focused functions
  • Add 15 new tests for extracted functions

Changes

@node-minify/utils - compressor-resolver.ts

Function Purpose
tryResolveBuiltIn(name) Try resolving from @node-minify/* packages
tryResolveNpmPackage(name) Try resolving from installed npm packages
tryResolveLocalFile(name) Try resolving from local file paths
isLocalPath(name) Check if string is a local path (exported)

resolveCompressor now orchestrates these three in sequence (~15 lines vs ~95 before).

@node-minify/benchmark - runner.ts

Function Purpose
runWarmup(...) Execute warmup iterations
runIterations(...) Execute timed iterations, return times array
calculateCompressorMetrics(...) Compute final metrics from times + output
createErrorMetrics(name, error) Create error result object
cleanupTempFiles(files) Delete temp files

benchmarkCompressor now ~30 lines vs ~97 before.

Testing

  • 15 new tests for resolution functions
  • All 818 existing tests pass

Implements P2 from REFACTORING_PLAN.md


Summary by cubic

Split compressor resolution and benchmarking into small, focused functions to improve readability and testability. Adds input validation, brotli raw size support, and targeted tests.

  • Refactors
    • Utils: resolveCompressor now delegates to tryResolveBuiltIn, tryResolveNpmPackage, and tryResolveLocalFile; exported isLocalPath and the new resolver helpers. Added getFilesizeBrotliRaw and deduplicated brotli size logic; exported in index. Avoids double-wrapping FileOperationError in size helpers. resolveCompressor shrank from ~95 lines to ~15.
    • Benchmark: benchmarkCompressor now uses runWarmup, runIterations, and calculateCompressorMetrics, plus createErrorMetrics and cleanupTempFiles. Added validation for iterationCount and empty timing data. Metrics now include brotliBytes. Shrunk from ~97 lines to ~30.
    • Tests: Added new tests for resolver paths, brotli raw size, and validation; all existing tests pass.

Written for commit 1716594. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Modular benchmarking helpers (warmup, timed iterations, centralized metric calculation, error/cleanup helpers).
    • Compressor resolution exposes separate helpers for built-ins, npm packages, and local files.
    • New raw Brotli filesize API added to public utilities.
  • Bug Fixes

    • Improved validation and error handling for iteration counts, empty timings, and preserved file-operation errors in size utilities.
    • Summary now handles no-success cases.
  • Tests

    • Expanded tests for resolvers, raw Brotli sizing, and benchmarking edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 13, 2026

⚠️ No Changeset found

Latest commit: 1716594

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Extracted and exported benchmark helpers (warmup, iterations, metric calculation, error/cleanup), refactoring runner.ts to use them; expanded compressor resolver into public try-resolve helpers (built-in, npm, local) and a sequenced resolve flow; added Brotli raw size helper and related tests and validations.

Changes

Cohort / File(s) Summary
Benchmarking core
packages/benchmark/src/runner.ts
Added and exported: runWarmup, IterationsResult type, runIterations, calculateCompressorMetrics, createErrorMetrics, cleanupTempFiles. Refactored benchmarkCompressor/runBenchmark to delegate warmup, iteration timing, metric computation, error creation, and temp-file cleanup to these helpers.
Benchmark tests
packages/benchmark/__tests__/runner-edge-cases.test.ts
Added tests for runIterations validation (non-positive counts) and calculateCompressorMetrics validation (empty times).
Compressor resolver implementation
packages/utils/src/compressor-resolver.ts
Exported isLocalPath; added tryResolveBuiltIn, tryResolveNpmPackage, tryResolveLocalFile (each returns `CompressorResolution
Resolver public API
packages/utils/src/index.ts
Re-exported isLocalPath, tryResolveBuiltIn, tryResolveLocalFile, tryResolveNpmPackage, and added getFilesizeBrotliRaw to public exports.
Resolver tests
packages/utils/__tests__/compressor-resolver.test.ts
Expanded tests to cover isLocalPath, built-in resolution, npm-package resolution (present/missing/invalid exports), and local-file resolution scenarios.
Brotli size helper
packages/utils/src/getFilesizeBrotliInBytes.ts, packages/utils/__tests__/utils.test.ts
Introduced internal getBrotliSize and public getFilesizeBrotliRaw(file); tests for raw Brotli size success and FileOperationError on missing/dir paths.
Gzip error handling tweak
packages/utils/src/getFilesizeGzippedInBytes.ts
Preserve existing FileOperationError instances when catching errors (rethrow instead of wrapping).

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Client
  participant RES as resolveCompressor
  participant BUILT as Built-in resolver
  participant NPM as NPM resolver
  participant FILE as Local-file resolver
  CLI->>RES: resolve(name)
  RES->>BUILT: tryResolveBuiltIn(name)
  alt built-in found
    BUILT-->>RES: CompressorResolution
    RES-->>CLI: CompressorResolution
  else not found
    BUILT-->>RES: null
    RES->>NPM: tryResolveNpmPackage(name)
    alt npm found
      NPM-->>RES: CompressorResolution
      RES-->>CLI: CompressorResolution
    else not found
      NPM-->>RES: null
      RES->>FILE: isLocalPath(name) ? tryResolveLocalFile(name)
      alt local found
        FILE-->>RES: CompressorResolution
        RES-->>CLI: CompressorResolution
      else
        FILE-->>RES: null
        RES-->>CLI: throw Error("could not resolve compressor")
      end
    end
  end
Loading
sequenceDiagram
  participant CLI as Client
  participant RUN as Benchmark Runner
  participant WU as runWarmup
  participant IT as runIterations
  participant MET as calculateCompressorMetrics
  participant FS as Filesystem
  CLI->>RUN: start benchmark(file, compressor)
  RUN->>WU: runWarmup(compressor, file, count)
  WU-->>RUN: warmup completed
  RUN->>IT: runIterations(compressor, file, iterations)
  IT->>FS: write temp outputs (per iteration)
  IT-->>RUN: iteration times + final output path
  RUN->>MET: calculateCompressorMetrics(times, output, options)
  MET->>FS: compute gzip/brotli sizes (optional)
  MET-->>RUN: metrics
  RUN-->>CLI: report metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through bytes and timing logs,

Warmups danced and iterations jog.
Built-ins, npm, or a local tile,
I sniffed the best compressor style.
Crunching Brotli, gzip—what a mile!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: decomposing complex functions into smaller helpers across utils and benchmark packages, which is the core objective of this refactoring PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
packages/utils/__tests__/compressor-resolver.test.ts (1)

90-106: Consider adding cleanup to prevent test pollution.

The mock is set up with vi.doMock and unmocked with vi.doUnmock, but vi.resetModules() in afterEach should handle cleanup. However, the dynamic re-import pattern could be fragile if tests run in parallel.

💡 Optional: Consolidate mock cleanup
             const result = await freshTryResolve("terser");
             expect(result).toBeNull();

-            // Restore mock so subsequent tests are unaffected
-            vi.doUnmock("@node-minify/terser");
+            // vi.resetModules() in afterEach handles cleanup
         });

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1308d55 and 1716594.

📒 Files selected for processing (8)
  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/__tests__/utils.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/getFilesizeGzippedInBytes.ts
  • packages/utils/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/benchmark/tests/runner-edge-cases.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use double quotes for strings
Always include semicolons at end of statements
Use ES5 trailing commas in objects and arrays
Use node: prefix for Node.js built-in imports (e.g., node:child_process)
Use camelCase for function and variable names
Use try/catch with if (err instanceof Error) checks for error handling
Use Promise.all for parallel operations where all must succeed, or Promise.allSettled when you need results regardless of individual failures
Limit concurrency to 5-10 for file operations and 2-4 for CPU-intensive compressions to avoid resource exhaustion
Use file operation utilities from @node-minify/utils: readFile, writeFile, getFilesizeInBytes, getContentFromFilesAsync, isValidFileAsync
Use deprecation warnings via warnDeprecation from @node-minify/utils for deprecated packages
Use resolveCompressor and isBuiltInCompressor utilities from @node-minify/utils for dynamic compressor resolution

Files:

  • packages/utils/src/getFilesizeGzippedInBytes.ts
  • packages/utils/__tests__/utils.test.ts
  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use type keyword for type-only imports in TypeScript
Include .ts file extension in local imports (e.g., ./setup.ts)
Use PascalCase for type and interface names

Files:

  • packages/utils/src/getFilesizeGzippedInBytes.ts
  • packages/utils/__tests__/utils.test.ts
  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
packages/**/__tests__/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Write tests using Vitest with describe/test/expect in files located at packages/*/__tests__/*.test.ts

Files:

  • packages/utils/__tests__/utils.test.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
packages/*/src/index.ts

📄 CodeRabbit inference engine (AGENTS.md)

Include copyright header /*! node-minify ... MIT Licensed */ in source files

Files:

  • packages/utils/src/index.ts
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `resolveCompressor` and `isBuiltInCompressor` utilities from `node-minify/utils` for dynamic compressor resolution
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: The monorepo architecture has core packages (`core`, `utils`, `run`, `types`, `cli`, `benchmark`) with `core` depending on `utils` and `run`, and all compressor packages depending on `core`
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/__tests__/*.test.ts : Use shared test fixtures from `tests/fixtures.ts` with `runOneTest` helper and test suites like `commonjs`, `commoncss`, `commonhtml`
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/src/index.ts : Compressor functions must export an async function matching the `Compressor` type that receives `{ settings, content }` and returns `{ code, map? }`
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use file operation utilities from `node-minify/utils`: `readFile`, `writeFile`, `getFilesizeInBytes`, `getContentFromFilesAsync`, `isValidFileAsync`
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use file operation utilities from `node-minify/utils`: `readFile`, `writeFile`, `getFilesizeInBytes`, `getContentFromFilesAsync`, `isValidFileAsync`

Applied to files:

  • packages/utils/src/getFilesizeGzippedInBytes.ts
  • packages/utils/__tests__/utils.test.ts
  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/__tests__/*.test.ts : Use shared test fixtures from `tests/fixtures.ts` with `runOneTest` helper and test suites like `commonjs`, `commoncss`, `commonhtml`

Applied to files:

  • packages/utils/__tests__/utils.test.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/**/__tests__/*.test.ts : Write tests using Vitest with `describe`/`test`/`expect` in files located at `packages/*/__tests__/*.test.ts`

Applied to files:

  • packages/utils/__tests__/utils.test.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `resolveCompressor` and `isBuiltInCompressor` utilities from `node-minify/utils` for dynamic compressor resolution

Applied to files:

  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/src/index.ts : Compressor functions must export an async function matching the `Compressor` type that receives `{ settings, content }` and returns `{ code, map? }`

Applied to files:

  • packages/utils/src/getFilesizeBrotliInBytes.ts
  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx} : Include `.ts` file extension in local imports (e.g., `./setup.ts`)

Applied to files:

  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: The monorepo architecture has core packages (`core`, `utils`, `run`, `types`, `cli`, `benchmark`) with `core` depending on `utils` and `run`, and all compressor packages depending on `core`

Applied to files:

  • packages/utils/src/index.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Limit concurrency to 5-10 for file operations and 2-4 for CPU-intensive compressions to avoid resource exhaustion

Applied to files:

  • packages/utils/src/index.ts
🧬 Code graph analysis (2)
packages/utils/src/getFilesizeGzippedInBytes.ts (1)
packages/utils/src/error.ts (1)
  • FileOperationError (11-25)
packages/utils/__tests__/compressor-resolver.test.ts (1)
packages/utils/src/compressor-resolver.ts (4)
  • isLocalPath (66-73)
  • tryResolveBuiltIn (145-171)
  • tryResolveNpmPackage (180-209)
  • tryResolveLocalFile (218-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (ubuntu-latest, 20.x)
  • GitHub Check: test (windows-latest, 24.x)
  • GitHub Check: test (windows-latest, 20.x)
  • GitHub Check: test (ubuntu-latest, 22.x)
  • GitHub Check: test (ubuntu-latest, 24.x)
  • GitHub Check: test (windows-latest, 22.x)
🔇 Additional comments (24)
packages/utils/src/getFilesizeGzippedInBytes.ts (1)

54-56: LGTM — avoids double-wrapping FileOperationError.

The guard correctly re-throws errors that are already FileOperationError instances (thrown by the internal getGzipSize validation), preventing nested error messages. Pattern is consistently applied to both exported functions.

Also applies to: 77-79

packages/utils/src/getFilesizeBrotliInBytes.ts (2)

25-50: LGTM! Clean extraction of shared Brotli compression logic.

The internal getBrotliSize helper properly validates file existence and type before compression, avoiding redundant checks in the public functions.


58-72: Error handling is correct.

Both public functions properly re-throw FileOperationError instances without double-wrapping, while still wrapping unexpected errors with context.

Also applies to: 83-96

packages/utils/__tests__/utils.test.ts (1)

574-593: LGTM! Good test coverage for the new getFilesizeBrotliRaw utility.

Tests appropriately cover the happy path (positive numeric result), non-existent file error, and directory path error cases — consistent with the existing getFilesizeGzippedRaw test patterns.

packages/utils/__tests__/compressor-resolver.test.ts (3)

43-69: LGTM! Comprehensive isLocalPath test coverage.

Tests cover all path formats: relative (./, ../), absolute POSIX, Windows-style paths, and correctly identifies npm package names as non-local.


109-129: Test uses picocolors as a proxy compressor — verify assumption.

The test assumes picocolors exports a function that passes as a "compressor". While this works for testing the resolution logic, it's worth noting that picocolors isn't actually a compressor — it's a terminal color library.

The test is valid for verifying npm package resolution mechanics, but consider adding a comment clarifying that picocolors is used only because it exports a function, not because it's a compressor:

     describe("tryResolveNpmPackage", () => {
         test("should resolve installed npm package with function export", async () => {
+            // Using picocolors as it exports a function (not actually a compressor)
             const result = await tryResolveNpmPackage("picocolors");

131-169: LGTM! Good coverage for tryResolveLocalFile.

Tests properly verify: non-local path returns null, local file with valid export resolves, non-existent file throws, and invalid export throws with correct message.

packages/utils/src/index.ts (1)

1-79: LGTM! Public API surface correctly expanded.

New exports (isLocalPath, tryResolveBuiltIn, tryResolveLocalFile, tryResolveNpmPackage, getFilesizeBrotliRaw) are properly imported and re-exported, maintaining alphabetical ordering in the export list.

packages/utils/src/compressor-resolver.ts (6)

66-73: LGTM! isLocalPath correctly identifies local file paths.

The function handles all common path formats: relative (./, ../), absolute POSIX (/), and Windows-style (C:\, D:/).


92-124: extractCompressor has a clear priority order for export resolution.

The extraction logic follows a sensible precedence: known export → camelCase from basename → compressor named export → default export → first function export. This is well-documented by the JSDoc.


145-171: LGTM! tryResolveBuiltIn correctly guards against unknown compressors.

Early return for names not in KNOWN_COMPRESSOR_EXPORTS avoids unnecessary dynamic imports. Silent catch for missing packages is appropriate since this is a "try" function.


180-209: LGTM! tryResolveNpmPackage has appropriate error handling.

The function correctly re-throws errors about invalid exports while returning null for resolution failures (package not found). This allows resolveCompressor to try other resolution strategies.


218-255: LGTM! tryResolveLocalFile provides good error context.

The function wraps import failures with descriptive messages including the original error, which aids debugging. The early null return for non-local paths is correct.


264-294: Clean orchestration in resolveCompressor.

The refactored function is much easier to follow: try built-in → try npm → try local (if local path) → throw. The comment at lines 285-288 about defensive handling is appropriate given the isLocalPath guard.

packages/benchmark/src/runner.ts (10)

7-25: LGTM!

Imports are well-organized: uses node:fs prefix for Node.js built-ins, type keyword for type-only imports, and .ts extensions for local imports per coding guidelines.


36-52: LGTM!

The warmup function is clean and focused. Error propagation to the caller is appropriate since benchmarkCompressor wraps this in a try/catch block.


72-100: LGTM!

Good validation for iterationCount >= 1. Using performance.now() for high-resolution timing is appropriate, and reusing the same output file path for all iterations is efficient since only the final output matters for size metrics.


112-152: LGTM!

The brotli handling is now consistent with gzip (both brotliSize and brotliBytes are populated). Good validation for empty timing data. The use of statSync here is acceptable given this runs after async compression operations.


161-174: LGTM!

Clean error metrics factory with proper instanceof Error check per coding guidelines.


181-189: LGTM!

Appropriate cleanup behavior - silently ignoring errors during temp file deletion is correct since these files may already be deleted or the process is exiting.


236-267: LGTM!

Clean implementation with sensible defaults and proper progress callback support. Sequential compressor execution is correct to avoid benchmark interference.


278-332: LGTM!

Excellent refactoring - the function is now clean and delegates to focused helpers. Proper cleanup in both success and error paths, and the unique ID generation prevents temp file collisions.


340-376: LGTM!

Good edge case handling for no successful results. The summary logic correctly identifies best compression, best performance, and recommended compressor using the scoring function.


205-212: The type assertion as { input?: string[] } is correct. The wildcards function returns either:

  • {} (empty object) when no wildcards are found in a string pattern
  • { input: string[] } with the expanded file paths

Both return values match the type assertion. The code at lines 207–210 properly handles both cases by checking if (matched?.input) before spreading and falling back to the original pattern, so there is no silent failure risk.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

@greptile-apps
Copy link

greptile-apps bot commented Jan 13, 2026

Greptile Summary

This PR refactors complex functions in @node-minify/utils and @node-minify/benchmark by decomposing them into smaller, focused helper functions.

  • @node-minify/utils: resolveCompressor (~95 lines) decomposed into tryResolveBuiltIn, tryResolveNpmPackage, and tryResolveLocalFile, reducing main function to ~15 lines. Also added getFilesizeBrotliRaw for raw byte values and fixed potential double-wrapping of FileOperationError in size helpers.

  • @node-minify/benchmark: benchmarkCompressor (~97 lines) decomposed into runWarmup, runIterations, calculateCompressorMetrics, createErrorMetrics, and cleanupTempFiles, reducing main function to ~30 lines. Added input validation for iterationCount and empty timing data, plus brotliBytes metric.

  • Tests: 15 new tests covering the extracted functions with good edge case coverage.

The refactoring improves code readability, testability, and maintainability while preserving existing behavior. All exported functions are now individually testable and reusable.

Confidence Score: 5/5

  • This PR is safe to merge - it's a clean refactoring with no functional changes and comprehensive test coverage.
  • Score of 5 reflects: (1) pure refactoring that preserves existing behavior, (2) 15 new tests for extracted functions, (3) all 818 existing tests pass, (4) no security concerns, (5) improved code quality with better separation of concerns.
  • No files require special attention - all changes are straightforward decomposition of existing logic with test coverage.

Important Files Changed

Filename Overview
packages/benchmark/src/runner.ts Decomposed benchmarkCompressor into 5 focused helper functions (runWarmup, runIterations, calculateCompressorMetrics, createErrorMetrics, cleanupTempFiles). Added input validation and brotliBytes metric. Code quality improvement with no functional issues.
packages/utils/src/compressor-resolver.ts Decomposed resolveCompressor into 3 focused functions (tryResolveBuiltIn, tryResolveNpmPackage, tryResolveLocalFile). Exported isLocalPath. Simplified main function from ~95 lines to ~15 lines. Well-structured refactoring with preserved behavior.
packages/utils/src/getFilesizeBrotliInBytes.ts Extracted shared getBrotliSize helper, added getFilesizeBrotliRaw for raw byte values. Added FileOperationError re-throw guard to prevent double-wrapping errors. Clean refactoring.
packages/utils/src/getFilesizeGzippedInBytes.ts Added FileOperationError re-throw guards in both getFilesizeGzippedInBytes and getFilesizeGzippedRaw to prevent double-wrapping errors. Consistent with brotli implementation.
packages/utils/src/index.ts Exported new functions: isLocalPath, tryResolveBuiltIn, tryResolveLocalFile, tryResolveNpmPackage, getFilesizeBrotliRaw. Clean barrel file update.
packages/benchmark/tests/runner-edge-cases.test.ts Added tests for runIterations validation (iterationCount 0 and negative) and calculateCompressorMetrics validation (empty times array). Good edge case coverage.
packages/utils/tests/compressor-resolver.test.ts Added comprehensive tests for isLocalPath, tryResolveBuiltIn, tryResolveNpmPackage, and tryResolveLocalFile. Tests cover success paths, error cases, and edge conditions like invalid exports.
packages/utils/tests/utils.test.ts Added tests for getFilesizeBrotliRaw: validates number return type, file existence check, and directory path rejection.

Sequence Diagram

sequenceDiagram
    participant User
    participant resolveCompressor
    participant tryResolveBuiltIn
    participant tryResolveNpmPackage
    participant tryResolveLocalFile
    
    User->>resolveCompressor: resolve("terser")
    resolveCompressor->>tryResolveBuiltIn: try built-in first
    tryResolveBuiltIn-->>resolveCompressor: CompressorResolution | null
    alt built-in found
        resolveCompressor-->>User: return resolution
    else built-in not found
        resolveCompressor->>tryResolveNpmPackage: try npm package
        tryResolveNpmPackage-->>resolveCompressor: CompressorResolution | null
        alt npm package found
            resolveCompressor-->>User: return resolution
        else npm not found & isLocalPath
            resolveCompressor->>tryResolveLocalFile: try local file
            tryResolveLocalFile-->>resolveCompressor: CompressorResolution | throws
            resolveCompressor-->>User: return resolution
        else not found anywhere
            resolveCompressor-->>User: throw Error
        end
    end
Loading

@srod srod force-pushed the refactor/p2-decompose-complex-functions branch from 1308d55 to 4d0921c Compare January 14, 2026 20:28
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/benchmark/src/runner.ts`:
- Around line 140-147: The CompressorMetrics population is inconsistent: when
options.includeBrotli is true, set both metrics.brotliSize and
metrics.brotliBytes just like gzip does; call the proper raw-byte helper (e.g.,
add or use getFilesizeBrotliRaw to produce a numeric byte count and assign it to
metrics.brotliBytes) and keep getFilesizeBrotliInBytes (or an equivalent
human-readable formatter) to set metrics.brotliSize—i.e., mirror the gzip branch
(getFilesizeGzippedInBytes / getFilesizeGzippedRaw) for brotli by adding the
missing raw-byte helper or by using getFilesizeBrotliInBytes for bytes and
converting/formatting for brotliSize so both fields are populated.
🧹 Nitpick comments (1)
packages/utils/src/compressor-resolver.ts (1)

145-171: Consider consistency in handling invalid exports.

When extractCompressor returns null (package loads but exports no valid compressor), this function returns null silently, whereas tryResolveNpmPackage throws an explicit error. Since built-in packages are controlled by the monorepo, this is unlikely to occur in practice, but for defensive consistency you could throw here as well.

This is a minor observation; the current behavior is acceptable since built-in packages should always have valid exports.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c75d534 and 1308d55.

📒 Files selected for processing (4)
  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/__tests__/compressor-resolver.test.ts
  • packages/utils/src/compressor-resolver.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utils/tests/compressor-resolver.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use double quotes for strings
Always include semicolons at end of statements
Use ES5 trailing commas in objects and arrays
Use node: prefix for Node.js built-in imports (e.g., node:child_process)
Use camelCase for function and variable names
Use try/catch with if (err instanceof Error) checks for error handling
Use Promise.all for parallel operations where all must succeed, or Promise.allSettled when you need results regardless of individual failures
Limit concurrency to 5-10 for file operations and 2-4 for CPU-intensive compressions to avoid resource exhaustion
Use file operation utilities from @node-minify/utils: readFile, writeFile, getFilesizeInBytes, getContentFromFilesAsync, isValidFileAsync
Use deprecation warnings via warnDeprecation from @node-minify/utils for deprecated packages
Use resolveCompressor and isBuiltInCompressor utilities from @node-minify/utils for dynamic compressor resolution

Files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/src/compressor-resolver.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use type keyword for type-only imports in TypeScript
Include .ts file extension in local imports (e.g., ./setup.ts)
Use PascalCase for type and interface names

Files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/src/compressor-resolver.ts
packages/**/__tests__/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Write tests using Vitest with describe/test/expect in files located at packages/*/__tests__/*.test.ts

Files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `resolveCompressor` and `isBuiltInCompressor` utilities from `node-minify/utils` for dynamic compressor resolution
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: The monorepo architecture has core packages (`core`, `utils`, `run`, `types`, `cli`, `benchmark`) with `core` depending on `utils` and `run`, and all compressor packages depending on `core`
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/src/index.ts : Compressor functions must export an async function matching the `Compressor` type that receives `{ settings, content }` and returns `{ code, map? }`
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/__tests__/*.test.ts : Use shared test fixtures from `tests/fixtures.ts` with `runOneTest` helper and test suites like `commonjs`, `commoncss`, `commonhtml`
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/__tests__/*.test.ts : Use shared test fixtures from `tests/fixtures.ts` with `runOneTest` helper and test suites like `commonjs`, `commoncss`, `commonhtml`

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/{esbuild,google-closure-compiler,oxc,swc,terser,uglify-js,clean-css,cssnano,csso,lightningcss,html-minifier,jsonminify,no-compress}/src/index.ts : Compressor functions must export an async function matching the `Compressor` type that receives `{ settings, content }` and returns `{ code, map? }`

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/src/compressor-resolver.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `resolveCompressor` and `isBuiltInCompressor` utilities from `node-minify/utils` for dynamic compressor resolution

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/src/compressor-resolver.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to packages/**/__tests__/*.test.ts : Write tests using Vitest with `describe`/`test`/`expect` in files located at `packages/*/__tests__/*.test.ts`

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: The monorepo architecture has core packages (`core`, `utils`, `run`, `types`, `cli`, `benchmark`) with `core` depending on `utils` and `run`, and all compressor packages depending on `core`

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
  • packages/utils/src/compressor-resolver.ts
📚 Learning: 2026-01-09T07:41:29.716Z
Learnt from: CR
Repo: srod/node-minify PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T07:41:29.716Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use file operation utilities from `node-minify/utils`: `readFile`, `writeFile`, `getFilesizeInBytes`, `getContentFromFilesAsync`, `isValidFileAsync`

Applied to files:

  • packages/benchmark/__tests__/runner-edge-cases.test.ts
  • packages/benchmark/src/runner.ts
🧬 Code graph analysis (2)
packages/benchmark/__tests__/runner-edge-cases.test.ts (1)
packages/benchmark/src/runner.ts (2)
  • runIterations (71-99)
  • calculateCompressorMetrics (111-150)
packages/benchmark/src/runner.ts (6)
packages/types/src/types.d.ts (1)
  • Compressor (83-84)
packages/benchmark/src/types.ts (2)
  • BenchmarkOptions (7-20)
  • CompressorMetrics (22-37)
packages/core/src/index.ts (1)
  • minify (21-27)
packages/benchmark/src/metrics.ts (1)
  • calculateReduction (7-13)
packages/utils/src/getFilesizeGzippedInBytes.ts (2)
  • getFilesizeGzippedInBytes (49-60)
  • getFilesizeGzippedRaw (70-80)
packages/utils/src/getFilesizeBrotliInBytes.ts (1)
  • getFilesizeBrotliInBytes (17-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: test (ubuntu-latest, 24.x)
  • GitHub Check: test (windows-latest, 20.x)
  • GitHub Check: test (windows-latest, 22.x)
  • GitHub Check: test (ubuntu-latest, 20.x)
  • GitHub Check: test (windows-latest, 24.x)
  • GitHub Check: test (ubuntu-latest, 22.x)
🔇 Additional comments (13)
packages/utils/src/compressor-resolver.ts (5)

7-9: LGTM!

Imports correctly use the node: prefix for Node.js built-ins and the type keyword for type-only imports, following the coding guidelines.


66-73: LGTM!

The export of isLocalPath is appropriate for the public API, and the Windows path detection regex correctly handles both forward and back slashes.


180-209: LGTM!

The error handling correctly distinguishes between "package not found" (returns null to allow fallback) and "package found but invalid exports" (throws to surface the issue). The if (error instanceof Error) pattern follows the coding guidelines.


218-255: LGTM!

The use of pathToFileURL ensures proper ESM import compatibility across platforms. The error handling correctly surfaces invalid-export errors while providing context for file-not-found scenarios.

Minor note: The error message on lines 240-241 mentions only "default export or named export 'compressor'", but extractCompressor also tries camelCase exports and iterates function exports. Consider aligning the message with actual behavior if you want more precise diagnostics.


264-294: Clean orchestration with appropriate resolution order.

The sequential resolution strategy (built-in → npm → local) is logical and efficient. The defensive null check on lines 282-284 with its explanatory comment is a reasonable safeguard, even though tryResolveLocalFile will throw rather than return null for actual local paths.

packages/benchmark/src/runner.ts (6)

35-51: LGTM!

The warmup helper is well-structured with proper type picking and clear documentation. The loop correctly stabilizes JIT and caches before timed iterations.


56-99: LGTM!

Good input validation for iterationCount and clean separation of timing logic. Using performance.now() is appropriate for high-resolution benchmarking.


159-172: LGTM!

Clean error metrics factory with proper handling of both string and Error types.


179-187: LGTM!

Appropriate error suppression for cleanup operations where partial failures are acceptable.


276-330: LGTM!

The refactored benchmarkCompressor is much cleaner with clear orchestration of the extracted helpers. Good patterns: unique temp file naming, cleanup in both success and error paths, and consistent error handling via createErrorMetrics.


338-374: LGTM!

Good handling of the empty results edge case and clean use of reduce for finding best metrics.

packages/benchmark/__tests__/runner-edge-cases.test.ts (2)

11-11: LGTM!

Correct import of the newly exported helper functions with .ts extension per coding guidelines.


281-305: LGTM!

Good validation test coverage using rejects.toThrow with exact error message matching. The mock compressor approach is appropriate since the validation occurs before the compressor is invoked.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/utils/src/getFilesizeBrotliInBytes.ts">

<violation number="1" location="packages/utils/src/getFilesizeBrotliInBytes.ts:27">
P2: Same error double-wrapping issue as `getFilesizeBrotliInBytes`: `getBrotliSize` throws `FileOperationError` for file validation failures, but this catch block unconditionally wraps all errors. Add a check to re-throw `FileOperationError` instances directly.</violation>

<violation number="2" location="packages/utils/src/getFilesizeBrotliInBytes.ts:62">
P2: Error double-wrapping regression: `getBrotliSize` already throws `FileOperationError` for missing/invalid files, but this catch block wraps it in another `FileOperationError`. The original code explicitly preserved `FileOperationError` instances with `if (error instanceof FileOperationError) { throw error; }`. Consider restoring that check to avoid nested error objects.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@srod
Copy link
Owner Author

srod commented Jan 14, 2026

@greptile

srod added 5 commits January 15, 2026 09:11
- Extract tryResolveBuiltIn, tryResolveNpmPackage, tryResolveLocalFile from resolveCompressor
- Export isLocalPath utility for path detection
- Extract runWarmup, runIterations, calculateCompressorMetrics from benchmarkCompressor
- Add createErrorMetrics and cleanupTempFiles helpers
- Add 15 new tests for extracted resolution functions

resolveCompressor now delegates to focused single-responsibility functions.
benchmarkCompressor now ~30 lines vs ~97 lines before.
@srod srod force-pushed the refactor/p2-decompose-complex-functions branch from ceaeece to 1716594 Compare January 15, 2026 08:14
@srod srod merged commit e5452ad into develop Jan 15, 2026
8 checks passed
@srod srod deleted the refactor/p2-decompose-complex-functions branch January 15, 2026 10:17
@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

❌ Patch coverage is 99.04762% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.37%. Comparing base (5e1c56e) to head (1716594).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
packages/utils/src/getFilesizeBrotliInBytes.ts 93.75% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2780      +/-   ##
===========================================
- Coverage    99.59%   99.37%   -0.22%     
===========================================
  Files           63       63              
  Lines         1242     1286      +44     
  Branches       380      390      +10     
===========================================
+ Hits          1237     1278      +41     
- Misses           5        8       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant