Skip to content

Release: merge develop into main#2793

Merged
srod merged 108 commits intomainfrom
develop
Jan 22, 2026
Merged

Release: merge develop into main#2793
srod merged 108 commits intomainfrom
develop

Conversation

@srod
Copy link
Owner

@srod srod commented Jan 22, 2026

Summary

Merge develop branch into main for release.

New Features

Feature PR
allowEmptyOutput option #2791
GitHub Action (@node-minify/action) #2760
silence option for run utility #2776
Docs pagination #2783

Bug Fixes

Performance

Refactoring

Dependencies

Package Version
oxc-minify ^0.110.0
ora v9.1.0
terser v5.46.0
bun v1.3.6
tsdown ^0.19.0
@biomejs/biome 2.3.11

Notes

  • This release will fix @node-minify/benchmark having workspace:* in published dependencies
  • ~80 commits total

Summary by cubic

Ships the new official GitHub Action (uses: srod/node-minify@v1), adds allowEmptyOutput to skip writing empty files, and introduces docs pagination. Also includes key fixes (CLI on Commander 14, GCC string flags, cssnano options, OXC sourcemaps), consistent error handling, and I/O/dependency updates.

  • New Features

    • Official bundled GitHub Action with PR comments, annotations, summaries, benchmarks, and thresholds; old composite action is deprecated.
    • New allowEmptyOutput option and --allow-empty-output CLI flag.
    • Add silence option in run (used by GCC/YUI) to suppress stderr noise.
    • Docs: pagination and a new GitHub Action guide; benchmark updates and gzip size utilities.
  • Migration

    • Replace uses: srod/node-minify/.github/actions/node-minify@main with uses: srod/node-minify@v1.
    • Install the compressor package your workflow uses (e.g., npm i @node-minify/terser) before running the action.
    • Type flag is still required for esbuild and yui; lightningcss no longer requires type.

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

Summary by CodeRabbit

  • New Features

    • New GitHub Action (v1) with job summaries, PR reporting, annotations, benchmarking and release workflows
    • Added --allow-empty-output flag to skip writing when output is empty
    • Added oxc and minify-html compressors; pagination added to docs site
  • Bug Fixes

    • Improved error handling and validation across compressor integrations and size reporting
  • Deprecation

    • Legacy composite GitHub Action marked DEPRECATED; migrate to v1
  • Documentation

    • Comprehensive GitHub Action docs, expanded CLI/options guides and examples

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

srod and others added 30 commits January 9, 2026 08:56
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fix(deps): update dependency oxc-minify to ^0.108.0
- Add new packages/action with Node20 runtime for GitHub Marketplace
- Includes minification, PR comments, job summaries, annotations, benchmarking
- Supports 22 compressors with deprecation warnings for legacy ones
- Add getFilesizeGzippedRaw to utils for raw byte gzip sizes
- Add minify-html to compressor registry
- Add oxc to default benchmark compressors
- Update .github/actions/node-minify to use resolveCompressor from utils
- Refactor action imports to use named imports instead of namespace imports
- Create root action.yml for uses: srod/node-minify@v1 syntax
- Switch from ncc to Bun bundler for 673KB self-contained bundle
- Add release-action.yml workflow for automated releases
- Update test-action.yml to build and test the bundled action
- Deprecate old composite action at .github/actions/node-minify
- Add GitHub Action documentation page to Astro docs site
- Add index.d.ts for type exports
The action build requires @node-minify/core and @node-minify/utils
to be built first since Bun's bundler resolves workspace dependencies.
The action imports from @node-minify/core for the minify function.
The bundled action dynamically imports compressor packages at runtime,
so they need to be installed in the test environment.
…ocol

npm fails when run in repo root due to workspace:* dependencies in package.json.
Node's import() looks in the repo's node_modules, not in NODE_PATH.
Also copy the compressor's own dependencies (terser, esbuild, lightningcss).
Copying to root node_modules is sufficient as long as we copy ALL packages.
This prevents the 'No such file or directory' error while keeping the installation clean.
…FilesizeGzippedRaw

chore(scripts): improve error handling in check-published
Docstrings generation was requested by @srod.

* #2760 (comment)

The following files were modified:

* `packages/action/src/checks.ts`
* `packages/action/src/index.ts`
* `packages/action/src/inputs.ts`
* `packages/action/src/minify.ts`
* `packages/action/src/outputs.ts`
* `packages/action/src/reporters/annotations.ts`
* `packages/action/src/reporters/comment.ts`
* `packages/action/src/reporters/summary.ts`
* `packages/benchmark/src/runner.ts`
* `packages/utils/src/getFilesizeGzippedInBytes.ts`
* `scripts/check-published.ts`
srod and others added 19 commits January 16, 2026 18:00
…627895

🛡️ Sentinel: [Security Enhancement] Add silence option to run utility
- Update `Header.astro` to use `aria-label` for top navigation
- Update `MainLayout.astro` to use `aria-label` for sidebar navigation
- Update `LeftSidebar.astro` to use `aria-label` for main navigation
- Add learning to `.Jules/palette.md`
…9028464055844

🎨 Palette: Use aria-label for navigation landmarks in docs
Optimizes `writeFile` and `writeFileAsync` in `packages/utils` by removing the explicit `lstat` check for directories before writing. Instead, it relies on the underlying `fs` write operation to throw `EISDIR` if the target is a directory. This saves one syscall per write operation.

- Removes `isDirectory` and `isDirectoryAsync` checks.
- Updates `writeFile` to catch filesystem errors naturally.
- Maintains backward compatibility by ensuring errors are still wrapped in `FileOperationError` (via `handleWriteError`).
- Verified with existing tests.
…786206695180100

⚡ Bolt: Use EAFP pattern in writeFile to reduce syscalls
- Created `Pagination` component to generate Previous/Next links based on sidebar structure.
- Integrated `Pagination` into `MainLayout` to appear at the bottom of content pages.
- Added accessibility features (aria-labels, focus outlines, semantic HTML).
- Styled with existing design system variables.
…732784843

🎨 Palette: Add pagination to documentation pages
fix(deps): update dependency ora to v9.1.0
Adds allowEmptyOutput option that gracefully handles empty minification
results (e.g., CSS files with only comments) without throwing errors.

- Add allowEmptyOutput to Settings type
- Implement in writeOutput() to skip writing when content is empty
- Add --allow-empty-output CLI flag
- Add tests covering all scenarios
- Add documentation

Closes #2790
Commander 14.x requires explicit .action() on main program when
subcommands exist, otherwise shows help instead of running.

Also includes biome formatting fixes.
Docstrings generation was requested by @srod.

* #2791 (comment)

The following files were modified:

* `packages/cli/src/bin/cli.ts`
* `packages/cli/src/compress.ts`
* `packages/google-closure-compiler/src/index.ts`
* `packages/utils/src/run.ts`
📝 Add docstrings to `feature/issue-2790-allow-empty-output`
- cli.ts: treat empty input array as missing (defaults to [])
- compress.ts: check file existence explicitly, let IO errors propagate
- run.ts: only skip when ALL output channels empty (code, buffer, outputs)
feat(utils): add allowEmptyOutput option + fix CLI Commander 14.x
fix(deps): update dependency oxc-minify to ^0.110.0
@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2026

🦋 Changeset detected

Latest commit: cc5fd76

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 31 packages
Name Type
@node-minify/types Minor
@node-minify/utils Minor
@node-minify/cli Minor
@node-minify/action Minor
@node-minify/benchmark Minor
@node-minify/docs Minor
@node-minify/babel-minify Minor
@node-minify/clean-css Minor
@node-minify/core Minor
@node-minify/crass Minor
@node-minify/cssnano Minor
@node-minify/csso Minor
@node-minify/esbuild Minor
@node-minify/google-closure-compiler Minor
@node-minify/html-minifier Minor
@node-minify/imagemin Minor
@node-minify/jsonminify Minor
@node-minify/lightningcss Minor
@node-minify/minify-html Minor
@node-minify/no-compress Minor
@node-minify/oxc Minor
@node-minify/run Minor
@node-minify/sharp Minor
@node-minify/sqwish Minor
@node-minify/svgo Minor
@node-minify/swc Minor
@node-minify/terser Minor
@node-minify/uglify-es Minor
@node-minify/uglify-js Minor
@node-minify/yui Minor
@node-minify/examples Minor

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 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

Introduces a new, bundled GitHub Action package (@node-minify/action), deprecates the legacy composite action, centralizes compressor resolution and error handling, adds raw gzip/brotli size helpers, enhances benchmarking, updates workflows (Bun → 1.3.6), and expands docs and tests.

Changes

Cohort / File(s) Summary
Composite Action Deprecation
\.github/actions/node-minify/README.md, \.github/actions/node-minify/action.yml, \.github/actions/node-minify/minify.ts
Mark composite action deprecated, add migration instructions to use bundled @node-minify/action@v1, add deprecation warning step, bump Bun to 1.3.6, replace local resolver with @node-minify/utils usage.
New Action Package
packages/action/*, packages/action/src/*, packages/action/action.yml, packages/action/package.json
New @node-minify/action implementation: inputs parsing, run orchestration (minify, benchmark), outputs, reporters (summary/comment/annotations), threshold checks, tests, build/config files.
Action Types & IO
packages/action/src/types.ts, packages/action/src/minify.ts, packages/action/src/outputs.ts, packages/action/src/benchmark.ts, packages/action/src/checks.ts
Define ActionInputs/FileResult/MinifyResult/BenchmarkResult, runMinification & runBenchmark, output emitters, and threshold validation logic.
Action Input Validation
packages/action/src/inputs.ts, packages/action/__tests__/inputs.test.ts
Robust input parsing/validation, deprecation warnings, JSON options parsing, benchmark compressor list handling and tests.
Compressor Resolver & Utils
packages/utils/src/compressor-resolver.ts, packages/utils/src/index.ts, packages/utils/__tests__/*
Modularized resolver (tryResolveBuiltIn / tryResolveNpmPackage / tryResolveLocalFile), expose isLocalPath, add minify-html known export, update public exports and tests.
Error Handling Utilities
packages/utils/src/errors.ts, packages/*/{*/src, __tests__}/*.test.ts
Add wrapMinificationError and validateMinifyResult, apply across many compressors and add corresponding error tests.
Raw Size Helpers
packages/utils/src/getFilesizeGzippedInBytes.ts, packages/utils/src/getFilesizeBrotliInBytes.ts
Introduce private helpers, add raw-size exports (getFilesizeGzippedRaw, getFilesizeBrotliRaw), improve error handling.
Benchmark Refactor
packages/benchmark/src/runner.ts, packages/benchmark/__tests__/runner-edge-cases.test.ts
Decompose benchmark flow (runWarmup, runIterations, calculateCompressorMetrics, createErrorMetrics, cleanupTempFiles), add oxc to defaults, compute summary metrics.
Compressor Packages: Validation & Wrapping
packages/{clean-css,crass,cssnano,csso,html-minifier,jsonminify,minify-html,oxc,sqwish}/src/index.ts + tests
Add try/catch, validate results, and rethrow wrapped errors across many compressor integrations; update some signatures (cssnano settings) and associated tests.
Core & Types Changes
packages/core/src/compress.ts, packages/core/package.json, packages/types/src/types.d.ts
Replace mkdirp with fs.promises.mkdir(..., { recursive: true }), remove mkdirp dep, add allowEmptyOutput to Settings types.
CLI Enhancements
packages/cli/src/{bin/cli.ts,compress.ts,index.ts}, packages/cli/package.json
Add --allow-empty-output, validate output existence before reading sizes, update docs/option text, refactor async CLI flow, bump ora.
Run / Write Output Changes
packages/utils/src/run.ts, packages/run/src/index.ts, packages/yui/src/index.ts
Add allowEmptyOutput short-circuit, improve writeOutput behavior for in-memory/multi-output, add silence option to run APIs and propagate to YUI.
Workflows & Release Automation
.github/workflows/{codeql.yml,publish.yml,release-action.yml,test-action.yml,test.yml}
Bun version bumps (1.3.5→1.3.6), new release-action workflow, test-action now builds/releases action artifact and consumes built dist in tests.
Docs & Site Changes
docs/src/**, docs/src/content/docs/github-action.md, AGENTS.md, packages/action/README.md
Add GitHub Action docs page, pagination component, update AGENTS.md, add packages/action README, add allow-empty-output docs, include oxc references.
Packaging & Config
package.json, packages/action/tsconfig.json, packages/action/vitest.config.ts, scripts/check-published.ts
Bump tsdown, add action package config (Node 20), per-package vitest config, and new script to check published package versions.
Tests / ESM Compatibility
tests/{files-path.ts,fixtures.ts}, many packages/**/__tests__/*
Add ESM-compatible __dirname via fileURLToPath across tests, extensive new tests for action inputs/outputs/benchmark/errors and utilities.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant GHA as GitHub Actions
    participant Inputs as Input Parser (`packages/action/src/inputs.ts`)
    participant Minifier as Minifier (`packages/action/src/minify.ts`)
    participant Benchmark as Benchmark (`packages/action/src/benchmark.ts`)
    participant Reporter as Reporters (summary/comment/annotations)
    participant Threshold as Checks (`packages/action/src/checks.ts`)
    
    User->>GHA: Trigger action (workflow)
    GHA->>Inputs: parseInputs()
    Inputs-->>GHA: ActionInputs
    
    GHA->>Minifier: runMinification(ActionInputs)
    Minifier->>Minifier: resolveCompressor (utils)
    Minifier->>Minifier: perform minify, measure sizes/time
    Minifier-->>GHA: MinifyResult
    
    alt benchmark enabled
        GHA->>Benchmark: runBenchmark(ActionInputs)
        Benchmark->>Benchmark: run iterations across compressors
        Benchmark-->>GHA: BenchmarkResult
        GHA->>Reporter: generateBenchmarkSummary(BenchmarkResult)
    end
    
    GHA->>Reporter: generateSummary(MinifyResult)
    GHA->>Reporter: postPRComment(MinifyResult)
    GHA->>Reporter: addAnnotations(MinifyResult)
    
    GHA->>Threshold: checkThresholds(MinifyResult.totalReduction)
    Threshold-->>GHA: pass | violation
    GHA-->>User: setOutputs & final status (success/fail)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped through code with tiny paws,
Bundled actions, docs, and cause,
Wrapping errors, sizing gzip tight,
Benchmarks racing through the night,
A carrot cake of tests—delight! 🥕🎉

🚥 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 PR title 'Release: merge develop into main' accurately reflects the primary objective of merging the develop branch into main for a release, as confirmed by the PR objectives and ~80 commits being merged.
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

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 100 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

@greptile-apps
Copy link

greptile-apps bot commented Jan 22, 2026

Greptile Summary

  • Ships the new official GitHub Action package (@node-minify/action) that replaces the legacy composite action with a bundled Node.js 20 runtime, enabling marketplace publication and providing enhanced features like PR comments, job summaries, file annotations, and benchmark comparisons
  • Introduces allowEmptyOutput option and CLI flag to gracefully handle cases where minification produces empty results (e.g., CSS files with only comments) by skipping file writes instead of throwing validation errors
  • Implements consistent error handling across 9 compressor packages using standardized utilities (wrapMinificationError, validateMinifyResult) while fixing various bugs including CLI Commander 14.x compatibility, Google Closure Compiler string options support, and OXC sourcemap return

Important Files Changed

Filename Overview
action.yml New root GitHub Action configuration enabling marketplace publication with comprehensive inputs/outputs for minification, reporting, and benchmarking
packages/action/src/index.ts Main entry point for the GitHub Action package orchestrating the complete minification workflow with reporting and threshold validation
packages/core/src/compress.ts Core minification logic updated to replace mkdirp with native fs.mkdir and handle allowEmptyOutput scenarios
packages/cli/src/bin/cli.ts CLI refactored for Commander 14.x compatibility with explicit .action() handler and new --allow-empty-output flag
packages/utils/src/errors.ts New error handling utilities providing standardized error wrapping and result validation across all compressors

Confidence score: 4/5

  • This is a major release with extensive new functionality and refactoring that should be carefully reviewed before merging
  • Score reflects the large scope of changes including new GitHub Action package, consistent error handling refactoring across 9 compressors, and multiple bug fixes that could have integration impacts
  • Pay close attention to the new GitHub Action implementation, error handling changes across compressor packages, and CLI refactoring for Commander compatibility

Sequence Diagram

sequenceDiagram
    participant User as "GitHub Workflow"
    participant Action as "action/index.ts"
    participant Inputs as "inputs.ts"
    participant Minify as "minify.ts"
    participant Core as "@node-minify/core"
    participant Utils as "@node-minify/utils"
    participant Outputs as "outputs.ts"
    participant Summary as "reporters/summary.ts"
    participant Comment as "reporters/comment.ts"
    participant Annotations as "reporters/annotations.ts"
    participant Checks as "checks.ts"

    User->>Action: "Trigger workflow"
    Action->>Inputs: "parseInputs()"
    Inputs-->>Action: "ActionInputs"
    Action->>Inputs: "validateCompressor(compressor)"
    Action->>Minify: "runMinification(inputs)"
    Minify->>Utils: "resolveCompressor(compressor)"
    Utils-->>Minify: "compressor function"
    Minify->>Minify: "getFileSize(inputPath)"
    Minify->>Core: "minify()"
    Core-->>Minify: "compressed output"
    Minify->>Minify: "getFileSize(outputPath)"
    Minify->>Utils: "getFilesizeGzippedRaw(outputPath)"
    Utils-->>Minify: "gzip size"
    Minify-->>Action: "MinifyResult"
    Action->>Outputs: "setMinifyOutputs(result)"
    Outputs->>Outputs: "setOutput()"
    Action->>Summary: "generateSummary(result)"
    Summary->>Summary: "summary.addTable()"
    Action->>Comment: "postPRComment(result, token)"
    Comment->>Comment: "octokit.rest.issues.createComment()"
    Action->>Annotations: "addAnnotations(result)"
    Annotations->>Annotations: "warning/error/notice()"
    Action->>Checks: "checkThresholds(reduction, inputs)"
    Checks-->>Action: "null or error message"
    Action-->>User: "Success or failure"
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

96 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

* - packages with unpublished versions (updates needed)
* - or a confirmation that all packages are up to date
*/
async function main() {
Copy link

Choose a reason for hiding this comment

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

style: Function declared as async but never uses await - can be simplified to synchronous

Suggested change
async function main() {
function main() {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/check-published.ts
Line: 72:72

Comment:
**style:** Function declared as async but never uses await - can be simplified to synchronous

```suggestion
function main() {
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

required: true
type: string

concurrency: release-${{ github.event.release.tag_name || github.event.inputs.tag || github.ref_name }}
Copy link

Choose a reason for hiding this comment

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

logic: Concurrency key uses multiple fallback values but could create conflicts if both release and workflow_dispatch run simultaneously with the same version. Should the concurrency key differentiate between release and workflow_dispatch events to prevent potential race conditions?

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-action.yml
Line: 13:13

Comment:
**logic:** Concurrency key uses multiple fallback values but could create conflicts if both release and workflow_dispatch run simultaneously with the same version. Should the concurrency key differentiate between release and workflow_dispatch events to prevent potential race conditions?

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +105 to +106
git tag -fa "$VERSION" -m "Release $VERSION with built dist"
git push origin "refs/tags/$VERSION" --force
Copy link

Choose a reason for hiding this comment

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

style: Force pushing tags can be dangerous in collaborative environments. Is force pushing tags acceptable for this release workflow, or should there be additional safeguards?

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-action.yml
Line: 105:106

Comment:
**style:** Force pushing tags can be dangerous in collaborative environments. Is force pushing tags acceptable for this release workflow, or should there be additional safeguards?

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +285 to +288
// tryResolveLocalFile throws for local paths that can't be loaded,
// so reaching here means it returned null (which shouldn't happen
// after the isLocalPath guard, but we handle it defensively)
}
Copy link

Choose a reason for hiding this comment

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

logic: unreachable code - tryResolveLocalFile always throws for local paths that can't be loaded

Suggested change
// tryResolveLocalFile throws for local paths that can't be loaded,
// so reaching here means it returned null (which shouldn't happen
// after the isLocalPath guard, but we handle it defensively)
}
// 3. Try as local file path (throws if file exists but has invalid exports)
if (isLocalPath(name)) {
return await tryResolveLocalFile(name);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/utils/src/compressor-resolver.ts
Line: 285:288

Comment:
**logic:** unreachable code - tryResolveLocalFile always throws for local paths that can't be loaded

```suggestion
        // 3. Try as local file path (throws if file exists but has invalid exports)
    if (isLocalPath(name)) {
        return await tryResolveLocalFile(name);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/google-closure-compiler/src/index.ts (1)

75-106: Array-valued flags (e.g., externs) are silently filtered, and null is incorrectly accepted.

The applyOptions function filters out arrays and accepts null as an object, silently ignoring valid multi-value flags that Google Closure Compiler supports.

However, the proposed fix in applyOptions alone is insufficient. Arrays are filtered at a second stage in the pipeline: toBuildArgsOptions (in packages/utils/src/buildArgs.ts lines 21–28) also explicitly rejects arrays by design. Additionally, buildArgs converts values with String(value), which would incorrectly stringify arrays as comma-separated lists instead of repeating flags (e.g., --externs file1.js --externs file2.js).

A complete fix requires:

  1. Updating applyOptions to accept string arrays and exclude null
  2. Updating toBuildArgsOptions to support string arrays
  3. Updating buildArgs to repeat flags for array values instead of stringifying them
Current applyOptions issue
type Flags = {
    [key: string]: string | boolean | Record<string, unknown>;
};
// ... accepts null and rejects arrays at line 98:
(typeof value === "object" && !Array.isArray(value))
🤖 Fix all issues with AI agents
In `@packages/action/src/outputs.ts`:
- Around line 24-30: The reduce uses a falsy check (f.gzipSize || 0) which will
incorrectly coerce a genuine 0 gzipSize; update the aggregation in the block
that computes totalGzip (over result.files) to use nullish coalescing so it
treats undefined/null as 0 but preserves 0 (i.e., replace the fallback with
(f.gzipSize ?? 0)), leaving the surrounding conditional and the
setOutput("gzip-size", totalGzip) call unchanged.

In `@packages/action/src/reporters/comment.ts`:
- Around line 35-64: Wrap the entire Octokit comment flow in a try/catch so API
failures don't fail the action: enclose the calls that use octokit.paginate,
octokit.rest.issues.updateComment and octokit.rest.issues.createComment (the
block that finds existingComment and either updates or creates a comment, using
generateCommentBody(result)) inside a try, and in the catch log a warning (e.g.,
warn(...) or info(...) with a clear message and the caught error) instead of
rethrowing so the action continues on comment post/update failure (common with
read-only GITHUB_TOKEN on forked PRs).

In `@packages/action/src/reporters/summary.ts`:
- Around line 20-27: The gzip size check in the rows mapping uses a truthy test
so a value of 0 becomes "-"—update the mapping in the result.files .map (the
rows variable) to use a nullish check for f.gzipSize (e.g., test f.gzipSize !=
null or the nullish coalescing operator) and only call prettyBytes when a
numeric gzipSize exists; keep everything else the same so 0 displays as "0 B"
(or equivalent) instead of "-".

In `@packages/html-minifier/src/index.ts`:
- Around line 44-50: The thrown error message "html-minifier failed: empty
result" gets double-wrapped by wrapMinificationError; update the thrown Error so
it does not include the compressor name prefix (remove "html-minifier" from the
message) — e.g. throw new Error("failed: empty result" or "empty result") in the
same location so wrapMinificationError produces a single, correctly prefixed
message; ensure wrapMinificationError is still used in the catch block.

In `@scripts/check-published.ts`:
- Around line 34-60: The current try/catch wraps both the package existence
check and the version-specific lookup so when execFileSync for `npm view
${packageName}@${version}` throws it incorrectly sets `exists` to false; modify
the code to perform the existence check (calling `execFileSync` for `latest` and
inspecting `latest`) inside its own try/catch to set `exists`, then separately
run the version-specific `execFileSync` (assign to `specific`) inside its own
try/catch that only sets `publishedVersion` based on whether the call succeeds
and `specific` is non-empty; keep using the existing identifiers `execFileSync`,
`packageName`, `version`, `latest`, `specific`, `exists`, and `publishedVersion`
so a thrown error on the version lookup does not flip `exists`.
🧹 Nitpick comments (14)
packages/utils/__tests__/isValidFileAsync.test.ts (1)

14-15: Minor: Avoid redundant fileURLToPath call.

fileURLToPath(import.meta.url) is invoked twice. Consider computing it once and reusing:

♻️ Suggested refactor
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const __filename = fileURLToPath(import.meta.url);
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
packages/utils/src/writeFile.ts (1)

127-136: Unsafe type cast: error as Error.

The cast on line 134 assumes the caught value is an Error, but JavaScript can throw any value. Per coding guidelines, use instanceof Error checks.

♻️ Safer error normalization
 function handleWriteError(error: unknown, file: string | string[]): never {
     if (error instanceof ValidationError) {
         throw error;
     }
+    const normalizedError =
+        error instanceof Error ? error : new Error(String(error));
     throw new FileOperationError(
         "write to",
         typeof file === "string" ? file : "multiple files",
-        error as Error
+        normalizedError
     );
 }
.github/actions/node-minify/minify.ts (1)

53-61: Use shared file utilities for size/existence checks.
Prefer getFilesizeInBytes and isValidFileAsync from @node-minify/utils over local stat + existsSync to keep behavior consistent with the rest of the repo and avoid re-implementing file checks. Based on learnings, ...

♻️ Proposed refactor
-import { appendFileSync, existsSync } from "node:fs";
-import { stat } from "node:fs/promises";
+import { appendFileSync } from "node:fs";
 import { resolve } from "node:path";
 import { minify } from "@node-minify/core";
 import {
+    getFilesizeInBytes,
     getFilesizeGzippedInBytes,
+    isValidFileAsync,
     resolveCompressor,
 } from "@node-minify/utils";
@@
-async function getFileSize(filePath: string): Promise<number> {
-    const stats = await stat(filePath);
-    return stats.size;
-}
+// getFilesizeInBytes and isValidFileAsync from `@node-minify/utils`
@@
-    if (!existsSync(inputPath)) {
+    if (!(await isValidFileAsync(inputPath))) {
         console.error(`::error::Input file not found: ${inputPath}`);
         process.exit(1);
     }
@@
-        const originalSize = await getFileSize(inputPath);
+        const originalSize = await getFilesizeInBytes(inputPath);
@@
-        const minifiedSize = await getFileSize(outputPath);
+        const minifiedSize = await getFilesizeInBytes(outputPath);

Also applies to: 93-96, 99-100, 135-136

.github/workflows/publish.yml (1)

30-30: Align Bun version with root packageManager to avoid toolchain drift.

CI pins bun-version to 1.3.6 while the repo specifies packageManager "bun@1.3.5" in package.json. Either bump the packageManager field to 1.3.6 or document the intentional mismatch to maintain consistency across local and CI environments.

packages/terser/package.json (1)

56-56: Exact pinning for stable terser 5.x is intentional and valid.

The terser dependency uses exact pinning (5.46.0) while some dependencies use caret ranges. However, this is a reasonable choice for stable versions: exact pinning prevents unexpected updates while caret ranges are more common for pre-release versions like ^0.110.0 (oxc-minify). Terser 5.46.0 contains only minor additions (DOM properties) with no breaking changes. If you prefer consistency with other dependencies that use caret ranges, you can switch to ^5.46.0, but this is optional.

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

25-50: Consider using utils file I/O helpers for consistency. This module still uses existsSync and node:fs/promises directly; aligning with the shared file utilities would keep error handling consistent across the utils package. As per coding guidelines, please align with the @node-minify/utils file helpers where feasible.

Also applies to: 58-96

packages/minify-html/src/index.ts (1)

41-53: Add an explicit Error guard in the catch block.
Guidelines call for if (error instanceof Error) before wrapping unknown throws; this also preserves a proper cause when non-Error values are thrown.

♻️ Proposed fix
     } catch (error) {
-        throw wrapMinificationError("minify-html", error);
+        if (error instanceof Error) {
+            throw wrapMinificationError("minify-html", error);
+        }
+        throw wrapMinificationError("minify-html", new Error(String(error)));
     }

As per coding guidelines, please add the instanceof Error check.

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

30-39: Add an explicit Error guard in the catch block.
This keeps error handling consistent with the repo convention and preserves cause data when non-Error values are thrown.

♻️ Proposed fix
     } catch (error) {
-        throw wrapMinificationError("cssnano", error);
+        if (error instanceof Error) {
+            throw wrapMinificationError("cssnano", error);
+        }
+        throw wrapMinificationError("cssnano", new Error(String(error)));
     }

As per coding guidelines, please add the instanceof Error check.

packages/cssnano/__tests__/cssnano-error.test.ts (1)

9-32: Align compressor tests with shared fixtures helpers.
This suite lives under packages/cssnano/__tests__, where the convention is tests/fixtures.ts with runOneTest/commoncss; consider refactoring or briefly noting why this custom mock-based test is needed. As per coding guidelines, please align the test style with the shared fixtures convention.

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

20-40: Prefer async utils over sync fs checks in getGzipSize.

This helper is async but still uses existsSync/isValidFile (sync). Consider aligning with the repo’s async file utilities to avoid sync I/O and double stat calls. As per coding guidelines, use the utils file operations where possible.

♻️ Suggested refactor
-import { existsSync } from "node:fs";
-import { isValidFile } from "./isValidFile.ts";
+import { isValidFileAsync } from "./isValidFileAsync.ts";
+import { readFileAsync } from "./readFile.ts";

 async function getGzipSize(file: string): Promise<number> {
-    if (!existsSync(file)) {
-        throw new FileOperationError(
-            "access",
-            file,
-            new Error("File does not exist")
-        );
-    }
-
-    if (!isValidFile(file)) {
-        throw new FileOperationError(
-            "access",
-            file,
-            new Error("Path is not a valid file")
-        );
-    }
+    const isValid = await isValidFileAsync(file);
+    if (!isValid) {
+        throw new FileOperationError(
+            "access",
+            file,
+            new Error("Path is not a valid file")
+        );
+    }

     const { gzipSize } = await import("gzip-size");
-    const { readFile } = await import("node:fs/promises");
-    const content = await readFile(file);
+    const content = await readFileAsync(file, true);
     return gzipSize(content);
 }
.github/workflows/release-action.yml (1)

100-116: Consider adding error handling for the release tag push.

The major version tag push (lines 113-116) has explicit error handling, but the release tag push at line 106 does not. If the force push fails, the workflow continues silently to the major tag step.

♻️ Suggested improvement
      - name: Update release tag
        if: steps.commit.outputs.skip_tag_update != 'true'
        run: |
          # Update the release tag to point at HEAD (with dist files) and push
          VERSION="${{ steps.version.outputs.version }}"
          git tag -fa "$VERSION" -m "Release $VERSION with built dist"
-          git push origin "refs/tags/$VERSION" --force
+          if ! git push origin "refs/tags/$VERSION" --force; then
+            echo "Error: Failed to push release tag $VERSION"
+            exit 1
+          fi
.github/workflows/test-action.yml (1)

67-78: Compressor installation approach works but could be simplified.

The current approach creates a temporary directory, installs packages there, then copies to workspace. This works around the monorepo structure but the copy step could fail silently if paths don't exist.

♻️ Consider adding verification
          # Create node_modules and copy packages
          mkdir -p $GITHUB_WORKSPACE/node_modules
          cp -r /tmp/compressors/node_modules/* $GITHUB_WORKSPACE/node_modules/
+          
+          # Verify installation
+          ls -la $GITHUB_WORKSPACE/node_modules/@node-minify/terser || exit 1
+          ls -la $GITHUB_WORKSPACE/node_modules/@node-minify/esbuild || exit 1
packages/action/src/benchmark.ts (1)

26-31: Consider validating that compressors list is not empty after filtering.

If all compressors in benchmarkCompressors require a type and type is not provided, the filtered compressors array will be empty. This could lead to a confusing error from the benchmark package rather than a clear action-specific message.

♻️ Suggested improvement
     // Filter out compressors that require 'type' when type is not provided
     const compressors = inputs.type
         ? inputs.benchmarkCompressors
         : inputs.benchmarkCompressors.filter(
               (c) => !TYPE_REQUIRED_COMPRESSORS.includes(c)
           );
+
+    if (compressors.length === 0) {
+        throw new Error(
+            `No compressors available for benchmark. ` +
+                `All specified compressors require a 'type' option. ` +
+                `Either specify 'type' or include compressors that don't require it.`
+        );
+    }
packages/action/src/minify.ts (1)

7-22: Prefer shared file utilities over direct fs stat.
Use getFilesizeInBytes from @node-minify/utils for consistency and error handling.

♻️ Suggested refactor
-import { stat } from "node:fs/promises";
 import { resolve } from "node:path";
 import { minify } from "@node-minify/core";
-import { getFilesizeGzippedRaw, resolveCompressor } from "@node-minify/utils";
+import {
+    getFilesizeGzippedRaw,
+    getFilesizeInBytes,
+    resolveCompressor,
+} from "@node-minify/utils";
 import type { ActionInputs, FileResult, MinifyResult } from "./types.ts";
 
-/**
- * Get the size of a file in bytes.
- *
- * `@param` filePath - Path to the file
- * `@returns` The file size in bytes
- */
-async function getFileSize(filePath: string): Promise<number> {
-    const stats = await stat(filePath);
-    return stats.size;
-}
-
@@
-    const originalSize = await getFileSize(inputPath);
+    const originalSize = await getFilesizeInBytes(inputPath);
@@
-    const minifiedSize = await getFileSize(outputPath);
+    const minifiedSize = await getFilesizeInBytes(outputPath);

As per coding guidelines, prefer shared file operation utilities.

Also applies to: 45-63

Comment on lines +44 to +50
if (typeof code !== "string") {
throw new Error("html-minifier failed: empty result");
}

return { code };
return { code };
} catch (error) {
throw wrapMinificationError("html-minifier", error);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential double-wrapped error message.

The error thrown at line 45 ("html-minifier failed: empty result") will be caught by the outer catch block and re-wrapped via wrapMinificationError, resulting in: "html-minifier minification failed: html-minifier failed: empty result".

Consider using a message that doesn't include the compressor name prefix since wrapMinificationError will add it:

Proposed fix
         if (typeof code !== "string") {
-            throw new Error("html-minifier failed: empty result");
+            throw new Error("empty result");
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof code !== "string") {
throw new Error("html-minifier failed: empty result");
}
return { code };
return { code };
} catch (error) {
throw wrapMinificationError("html-minifier", error);
if (typeof code !== "string") {
throw new Error("empty result");
}
return { code };
} catch (error) {
throw wrapMinificationError("html-minifier", error);
🤖 Prompt for AI Agents
In `@packages/html-minifier/src/index.ts` around lines 44 - 50, The thrown error
message "html-minifier failed: empty result" gets double-wrapped by
wrapMinificationError; update the thrown Error so it does not include the
compressor name prefix (remove "html-minifier" from the message) — e.g. throw
new Error("failed: empty result" or "empty result") in the same location so
wrapMinificationError produces a single, correctly prefixed message; ensure
wrapMinificationError is still used in the catch block.

Comment on lines +34 to +60
try {
const latest = execFileSync(
"npm",
["view", packageName, "version", "--json"],
{
stdio: "pipe",
}
)
.toString()
.trim();
if (latest === "" || latest === "undefined")
return { exists: false, publishedVersion: false };

const specific = execFileSync(
"npm",
["view", `${packageName}@${version}`, "version", "--json"],
{ stdio: "pipe" }
)
.toString()
.trim();
return {
exists: true,
publishedVersion: specific !== "" && specific !== "undefined",
};
} catch (_e) {
return { exists: false, publishedVersion: false };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual file to see the current state
head -100 scripts/check-published.ts

Repository: srod/node-minify

Length of output: 3572


🏁 Script executed:

# Also check if the file exists and its size
ls -lh scripts/check-published.ts 2>&1 && wc -l scripts/check-published.ts

Repository: srod/node-minify

Length of output: 166


🏁 Script executed:

# Test the npm CLI behavior described in the review
#!/bin/bash
set -euo pipefail

pkg="@node-minify/core"
missing_version="0.0.0"

echo "=== Testing existing package ==="
npm view "$pkg" version --json 2>&1 | head -5

echo ""
echo "=== Testing non-existent version of existing package ==="
set +e
output=$(npm view "${pkg}@${missing_version}" version --json 2>&1)
exit_code=$?
echo "Output: $output"
echo "Exit code: $exit_code"
set -e

Repository: srod/node-minify

Length of output: 803


Fix misclassification when a specific version lookup fails. When npm view ${packageName}@${version} throws (e.g., version does not exist), the current catch block returns { exists: false, publishedVersion: false }, incorrectly marking existing packages with unpublished versions as non-existent. This will cause existing packages with version mismatches to be placed in the "NOT FOUND" bucket instead of "UNPUBLISHED VERSIONS."

Separate the try/catch blocks: wrap the package existence check and version-specific check independently so that a thrown error on the version query only affects the publishedVersion flag, not the exists flag.

🔧 Proposed fix (separate version lookup)
function checkPublished(packageName: string, version: string) {
    // Basic validation to prevent command injection
    if (!/^[\w@/.-]+$/.test(packageName) || !/^[\w.-]+$/.test(version)) {
        return { exists: false, publishedVersion: false };
    }

-    try {
-        const latest = execFileSync(
-            "npm",
-            ["view", packageName, "version", "--json"],
-            {
-                stdio: "pipe",
-            }
-        )
-            .toString()
-            .trim();
-        if (latest === "" || latest === "undefined")
-            return { exists: false, publishedVersion: false };
-
-        const specific = execFileSync(
-            "npm",
-            ["view", `${packageName}@${version}`, "version", "--json"],
-            { stdio: "pipe" }
-        )
-            .toString()
-            .trim();
-        return {
-            exists: true,
-            publishedVersion: specific !== "" && specific !== "undefined",
-        };
-    } catch (_e) {
-        return { exists: false, publishedVersion: false };
-    }
+    let latest: string;
+    try {
+        latest = execFileSync(
+            "npm",
+            ["view", packageName, "version", "--json"],
+            {
+                stdio: "pipe",
+            }
+        )
+            .toString()
+            .trim();
+    } catch (_e) {
+        return { exists: false, publishedVersion: false };
+    }
+
+    if (latest === "" || latest === "undefined") {
+        return { exists: false, publishedVersion: false };
+    }
+
+    let publishedVersion = false;
+    try {
+        const specific = execFileSync(
+            "npm",
+            ["view", `${packageName}@${version}`, "version", "--json"],
+            { stdio: "pipe" }
+        )
+            .toString()
+            .trim();
+        publishedVersion = specific !== "" && specific !== "undefined";
+    } catch (_e) {
+        publishedVersion = false;
+    }
+
+    return {
+        exists: true,
+        publishedVersion,
+    };
}
🤖 Prompt for AI Agents
In `@scripts/check-published.ts` around lines 34 - 60, The current try/catch wraps
both the package existence check and the version-specific lookup so when
execFileSync for `npm view ${packageName}@${version}` throws it incorrectly sets
`exists` to false; modify the code to perform the existence check (calling
`execFileSync` for `latest` and inspecting `latest`) inside its own try/catch to
set `exists`, then separately run the version-specific `execFileSync` (assign to
`specific`) inside its own try/catch that only sets `publishedVersion` based on
whether the call succeeds and `specific` is non-empty; keep using the existing
identifiers `execFileSync`, `packageName`, `version`, `latest`, `specific`,
`exists`, and `publishedVersion` so a thrown error on the version lookup does
not flip `exists`.

- Use nullish checks for gzipSize so 0 displays as '0 B' not '-'
- Wrap Octokit API calls in try/catch for graceful failure on forked PRs
@srod srod merged commit 52a827f into main Jan 22, 2026
25 checks passed
@codecov
Copy link

codecov bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 99.31507% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.37%. Comparing base (b9a6565) to head (cc5fd76).
⚠️ Report is 111 commits behind head on main.

Files with missing lines Patch % Lines
packages/utils/src/getFilesizeBrotliInBytes.ts 93.75% 1 Missing ⚠️
packages/utils/src/getFilesizeGzippedInBytes.ts 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2793      +/-   ##
==========================================
- Coverage   99.54%   99.37%   -0.17%     
==========================================
  Files          58       63       +5     
  Lines        1105     1287     +182     
  Branches      335      397      +62     
==========================================
+ Hits         1100     1279     +179     
- 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