Skip to content

fix(core): bundling a project no longer blocks the host's thread on esbuild - #5714

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/bundle-async-comment-strip
Oct 16, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/bundle-async-comment-strip

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

Bundling a project (bundleToSingleHtml) no longer blocks the host's thread on esbuild. Script comment stripping used esbuild's transformSync, which holds the calling thread until esbuild answers. A host that bundles on its main thread (a desktop app re-bundling after every edit) stalled on each script. It now uses esbuild's async transform, which leaves the thread free while esbuild works.

No public API changes: bundleToSingleHtml was already async, and every call site of the comment stripper sits inside the async bundleProject. The output is the same, since the same esbuild transform runs with the same options.

Why this fix

I considered three options:

  • An async variant for hosts: not needed, because the public entry point is already async.
  • Comment stripping without esbuild: that would mean a second JavaScript tokenizer to keep in step with esbuild's, a larger and riskier change for the same output.
  • Async esbuild (chosen): the smallest change that removes the sync call.

Verification

  • New htmlBundler.esbuildAsync.test.ts: esbuild's sync APIs are mocked to throw and record the call, and the async ones stay real. Bundling a project whose scripts carry block and line comments must strip both and make no sync call. On main it fails with expected [ 'transformSync' ] to deeply equal [].

  • All packages/core/src/compiler tests green 3 runs in a row (562 tests). Both core typechecks (tsc and tsc -p tsconfig.runtime.json) are clean, as are oxlint and oxfmt.

  • Desktop main-process before/after: a desktop app that bundles on its main process after each agent write, profiled over a ~140 s agent run on a Linux box. Both sides use a locally packed core, built from main and from this branch, and run twice each, alternating:

    Main-thread time in esbuild's sync call (runCallSync) Main process busy Max event-loop delay
    Before (main) 1183 ms, 1006 ms 6876 ms, 6467 ms 102 ms, 87 ms
    After (this branch) 0 ms, 0 ms 6080 ms, 5824 ms 55 ms, 47 ms

    Before, the heaviest sync chain was transformSync ← stripJsCommentsParserSafe ← bundleProject. After, no esbuild call appears on the main thread. The remaining bundling work there is about 150 ms per run.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 144 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Source-only review at c093a4bf8f9bad0a8fd5597ba333a19902b3114f, against effective base d05bc4215d5757da86956217f4a8a4b2ab11b1fd.

  • packages/core/src/compiler/htmlBundler.ts:828–857: the async transform stays inside the existing fallback boundary, retaining transform options, provenance construction and escaping after resolution.
  • packages/core/src/compiler/htmlBundler.ts:736–767,1365–1384: every private-helper consumer awaits its result; sequential script/anchor placement and completion before font deferral/serialization are preserved. The public API remains Promise<string>.
  • packages/core/src/compiler/htmlBundler.esbuildAsync.test.ts:10–38: the sync-API guards record before throwing, so fallback cannot conceal a forbidden call; output assertions also require real comment removal and preserved code.

Audited: both changed files in full, existing BASE before HEAD; complete private-helper call census; relevant script-run/escaping helpers and bounded CLI/Studio/Inspector integration evidence. Two independent source-only audits found no introduced reachable blocker. No existing blocking review or exact-head approval was present in the reviewed public record.

Not verified: test/typecheck execution, native responsiveness/profile claims, or universal byte-for-byte esbuild output parity. This is not a whole-repository semantic audit.

Verdict: APPROVE
Reasoning: The async migration propagates through every changed helper consumer without a demonstrated ordering, error-fallback, provenance or served-output regression.

— tai

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit b1cf6e3 Oct 16, 2026
164 of 165 checks passed
@miguel-heygen
miguel-heygen deleted the fix/bundle-async-comment-strip branch October 16, 2026 21:41
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.

2 participants