Skip to content

⚡ Bolt: Use async file writing in compression workflow#2727

Merged
srod merged 6 commits intodevelopfrom
bolt/optimize-file-writing-8256924729521665993
Jan 2, 2026
Merged

⚡ Bolt: Use async file writing in compression workflow#2727
srod merged 6 commits intodevelopfrom
bolt/optimize-file-writing-8256924729521665993

Conversation

@google-labs-jules
Copy link
Contributor

@google-labs-jules google-labs-jules bot commented Jan 2, 2026

💡 What: Replaced synchronous file writing with asynchronous file writing in the core run function.
🎯 Why: Synchronous I/O blocks the Node.js event loop, which can degrade performance when processing multiple files or in a server environment.
📊 Impact: Prevents blocking of the event loop during file write operations, improving concurrency handling.
🔬 Measurement: Verified by running the test suite, including new tests for writeFileAsync.


PR created automatically by Jules for task 8256924729521665993 started by @srod

Summary by CodeRabbit

  • New Features

    • Added an asynchronous file-writing option and exposed it via the public API to enable non-blocking I/O and improved responsiveness.
    • Core write operations now run asynchronously for more robust multi-output handling.
  • Tests

    • Added comprehensive tests covering the async file-writing paths: successes, validations, multi-file scenarios, and error handling.

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

Replaces synchronous `writeFile` with asynchronous `writeFileAsync` in the compression pipeline to prevent event loop blocking during file I/O operations.

- Added `writeFileAsync` to `@node-minify/utils`.
- Updated `run` in `@node-minify/utils` to use `writeFileAsync`.
- Updated tests in `@node-minify/utils`.
@google-labs-jules
Copy link
Contributor Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@changeset-bot
Copy link

changeset-bot bot commented Jan 2, 2026

⚠️ No Changeset found

Latest commit: fea6eb6

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 2, 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

Adds an async file-write API and converts internal write flows to use it: introduces writeFileAsync, refactors shared helpers, exports it publicly, updates run/writeOutput/writeMultipleOutputs to await async writes, and adds tests for the async variant.

Changes

Cohort / File(s) Summary
Core async file writing
packages/utils/src/writeFile.ts
Adds writeFileAsync (async variant). Adds helpers resolveTargetFile, validateContent, isDirectory/isDirectoryAsync, and handleWriteError. Refactors the existing writeFile to reuse helpers and centralizes path/validation logic.
Public API export
packages/utils/src/index.ts
Exports writeFileAsync from the package public API alongside writeFile.
Integration with async writes
packages/utils/src/run.ts
Changes writeOutput and writeMultipleOutputs to return Promise<void>. Replaces writeFile calls with writeFileAsync and adds await usage; run now awaits writeOutput.
Tests
packages/utils/__tests__/utils.test.ts
Adds tests covering writeFileAsync success and error scenarios mirroring existing writeFile tests.

Sequence Diagram(s)

sequenceDiagram
    participant Run as run()
    participant WriteOutput as writeOutput()
    participant MultiWriter as writeMultipleOutputs()
    participant FileWriter as writeFileAsync()
    participant FS as fs/promises

    Run->>WriteOutput: await writeOutput(result, settings, index)
    activate WriteOutput

    alt multiple outputs
        WriteOutput->>MultiWriter: await writeMultipleOutputs(outputs, settings, index)
        activate MultiWriter
        MultiWriter->>FileWriter: await writeFileAsync({file, content, index})
        activate FileWriter
        FileWriter->>FS: writeFile(path, content)
        FS-->>FileWriter: Promise<void>
        deactivate FileWriter
        FileWriter-->>MultiWriter: content
        deactivate MultiWriter
    else single output
        WriteOutput->>FileWriter: await writeFileAsync({file, content, index})
        activate FileWriter
        FileWriter->>FS: writeFile(path, content)
        FS-->>FileWriter: Promise<void>
        deactivate FileWriter
        FileWriter-->>WriteOutput: content
    end

    deactivate WriteOutput
    WriteOutput-->>Run: Promise<void>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
Soft paws tap keys in midnight light,
Files now promise to finish right,
Helpers hum and paths align,
Async writes — a tidy line,
Hooray, the rabbit hops with byte!

Pre-merge checks and finishing touches

✅ 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 clearly summarizes the main change: replacing synchronous file writing with asynchronous file writing in the compression workflow.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

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

@srod srod marked this pull request as ready for review January 2, 2026 13:00
@greptile-apps
Copy link

greptile-apps bot commented Jan 2, 2026

Greptile Summary

Replaced synchronous file writing with asynchronous file operations in the compression workflow. The core run() function and all file write operations now use writeFileAsync() instead of writeFile(), preventing event loop blocking during I/O operations.

Key Changes:

  • Added writeFileAsync() function in packages/utils/src/writeFile.ts using node:fs/promises
  • Refactored helper functions (resolveTargetFile, validateContent, isDirectory, handleWriteError) for reuse between sync and async versions
  • Updated run() and writeOutput() to be fully async with proper await for all file operations
  • Added comprehensive test coverage mirroring existing sync tests

Implementation Quality:
The async implementation correctly mirrors the sync version's logic - both use isDirectory() / isDirectoryAsync() which internally handle non-existent files by catching errors and returning false. The refactoring maintains backward compatibility by keeping the sync version alongside the new async version.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is clean, well-tested, and maintains backward compatibility. The async logic correctly mirrors the sync version, proper error handling is preserved, and comprehensive test coverage validates all scenarios including edge cases.
  • No files require special attention

Important Files Changed

Filename Overview
packages/utils/src/writeFile.ts Added writeFileAsync with proper async/await pattern, extracted helper functions for code reuse, logic matches sync version correctly
packages/utils/src/run.ts Converted all file writes to async using writeFileAsync, properly awaited all async operations, maintains correct control flow

Sequence Diagram

sequenceDiagram
    participant Core as Core/Compressor
    participant Run as run()
    participant WriteOutput as writeOutput()
    participant WriteFile as writeFileAsync()
    participant FS as Node.js fs/promises
    
    Core->>Run: run({ settings, content, index })
    Run->>Run: Validate settings & compressor
    Run->>Core: settings.compressor({ settings, content, index })
    Core-->>Run: CompressorResult { code, map?, buffer?, outputs? }
    Run->>WriteOutput: await writeOutput(result, settings, index)
    
    alt In-Memory Mode
        WriteOutput-->>Run: return (skip disk write)
    else Multiple Outputs
        WriteOutput->>WriteFile: await writeFileAsync() [for each output]
        WriteFile->>FS: await writeFile()
        FS-->>WriteFile: success
        WriteFile-->>WriteOutput: content
    else Buffer Output
        WriteOutput->>WriteFile: await writeFileAsync(buffer)
        WriteFile->>FS: await writeFile()
        FS-->>WriteFile: success
        WriteFile-->>WriteOutput: buffer
    else Code Output
        WriteOutput->>WriteFile: await writeFileAsync(code)
        WriteFile->>FS: await writeFile()
        FS-->>WriteFile: success
        WriteFile-->>WriteOutput: code
        
        opt Source Map Present
            WriteOutput->>WriteFile: await writeFileAsync(map)
            WriteFile->>FS: await writeFile()
            FS-->>WriteFile: success
            WriteFile-->>WriteOutput: map
        end
    end
    
    WriteOutput-->>Run: void
    Run-->>Core: result.code
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.

Additional Comments (1)

  1. foo.js, line 1 (link)

    style: Empty file added accidentally - should be removed

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@srod
Copy link
Owner

srod commented Jan 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #2730

Docstrings generation was requested by @srod.

* #2727 (comment)

The following files were modified:

* `packages/utils/src/run.ts`
* `packages/utils/src/writeFile.ts`
srod and others added 2 commits January 2, 2026 16:21
@srod
Copy link
Owner

srod commented Jan 2, 2026

@greptile

@srod
Copy link
Owner

srod commented Jan 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
packages/utils/src/run.ts (1)

143-189: Consider parallelizing file writes for improved throughput.

The current sequential approach using await inside the loop is correct and safe, but writes files one at a time. If the number of outputs is typically small, you could use Promise.all for parallel writes:

🔎 Optional parallel write approach
-    for (let i = 0; i < outputs.length; i++) {
-        const outputResult = outputs[i];
-        if (!outputResult) {
-            continue;
-        }
-
-        const format = outputResult.format || "out";
-        let targetFile: string;
-
-        const arrayItem = outputsArray[i];
-
-        if (
-            isArrayOutput &&
-            arrayItem !== undefined &&
-            arrayItem !== "$1" &&
-            arrayItem.trim() !== ""
-        ) {
-            // Explicit output path provided in array - use as-is
-            targetFile = arrayItem;
-        } else if (typeof output === "string" && output === "$1") {
-            // $1 only: auto-generate from input filename in input directory
-            const baseName = inputBase || "output";
-            targetFile = inputDir
-                ? join(inputDir, `${baseName}.${format}`)
-                : `${baseName}.${format}`;
-        } else if (typeof output === "string" && output.includes("$1")) {
-            // $1 pattern in path: replace and append format
-            const extensionlessName = inputBase || "output";
-            const outputFile = output.replace(/\$1/g, extensionlessName);
-            targetFile = `${outputFile}.${format}`;
-        } else if (typeof output === "string") {
-            // Single string output: append format extension
-            targetFile = `${output}.${format}`;
-        } else {
-            // Fallback
-            const baseName = inputBase || "output";
-            targetFile = inputDir
-                ? join(inputDir, `${baseName}.${format}`)
-                : `${baseName}.${format}`;
-        }
-
-        await writeFileAsync({
-            file: targetFile,
-            content: outputResult.content,
-            index,
-        });
-    }
+    const writePromises = outputs
+        .map((outputResult, i) => {
+            if (!outputResult) {
+                return null;
+            }
+
+            const format = outputResult.format || "out";
+            let targetFile: string;
+
+            const arrayItem = outputsArray[i];
+
+            if (
+                isArrayOutput &&
+                arrayItem !== undefined &&
+                arrayItem !== "$1" &&
+                arrayItem.trim() !== ""
+            ) {
+                targetFile = arrayItem;
+            } else if (typeof output === "string" && output === "$1") {
+                const baseName = inputBase || "output";
+                targetFile = inputDir
+                    ? join(inputDir, `${baseName}.${format}`)
+                    : `${baseName}.${format}`;
+            } else if (typeof output === "string" && output.includes("$1")) {
+                const extensionlessName = inputBase || "output";
+                const outputFile = output.replace(/\$1/g, extensionlessName);
+                targetFile = `${outputFile}.${format}`;
+            } else if (typeof output === "string") {
+                targetFile = `${output}.${format}`;
+            } else {
+                const baseName = inputBase || "output";
+                targetFile = inputDir
+                    ? join(inputDir, `${baseName}.${format}`)
+                    : `${baseName}.${format}`;
+            }
+
+            return writeFileAsync({
+                file: targetFile,
+                content: outputResult.content,
+                index,
+            });
+        })
+        .filter(Boolean);
+
+    await Promise.all(writePromises);

That said, the sequential approach is simpler and avoids potential filesystem contention with many outputs—feel free to keep it as-is if parallelism isn't a priority.

📜 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 2d765f8 and fea6eb6.

📒 Files selected for processing (2)
  • packages/utils/src/run.ts
  • packages/utils/src/writeFile.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utils/src/writeFile.ts
🔇 Additional comments (3)
packages/utils/src/run.ts (3)

15-15: LGTM!

Import correctly updated to use the new async variant.


26-48: LGTM!

The run function correctly awaits writeOutput, ensuring file writes complete before returning the result code. The public API surface remains unchanged for consumers.


59-102: LGTM!

All writeFileAsync calls are properly awaited, and the sequential write order (main file before source map) is correct. Early returns prevent unnecessary operations.

@srod srod merged commit eee54fc into develop Jan 2, 2026
9 checks passed
@srod srod deleted the bolt/optimize-file-writing-8256924729521665993 branch January 2, 2026 15:42
@codecov
Copy link

codecov bot commented Jan 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.48%. Comparing base (90d9ac7) to head (fea6eb6).
⚠️ Report is 7 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2727      +/-   ##
===========================================
+ Coverage    99.47%   99.48%   +0.01%     
===========================================
  Files           47       47              
  Lines          758      781      +23     
  Branches       247      245       -2     
===========================================
+ Hits           754      777      +23     
  Misses           4        4              

☔ 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