Skip to content

Deduplicate bundler dispatch and output glue, the JS string escaper, and the exports-map retry - #31998

Open
alii wants to merge 7 commits into
mainfrom
claude/split/bundler
Open

Deduplicate bundler dispatch and output glue, the JS string escaper, and the exports-map retry#31998
alii wants to merge 7 commits into
mainfrom
claude/split/bundler

Conversation

@alii

@alii alii commented Jun 8, 2026

Copy link
Copy Markdown
Member

What this does

Behavior-preserving dedup in the bundler, the JS string escaper, the resolver's exports-map retry, and the standalone loader. Net -236 lines. Rebuilt on current main (see below), as six commits:

  • Bundler (bundle_v2.rs, linker_context/, transpiler.rs): five parse-task enqueue sites shared an identical tail (scheduling fields, onLoad plugin hand-off, copy-for-bundling bookkeeping, pool schedule), now configure_and_dispatch_parse_task. The linked-sourcemap trailer, bytecode generation, and standalone placeholder output file were each written twice across generateChunksInParallel and writeOutputFilesToDisk, now one helper each (also drops a write-only fdpath). The text/md/data/wasm loaders built identical export-default statements and ParseResult literals, now export_default_parse_result / ParseResult::with_ast.
  • String escaper (bun_core/string/mod.rs, js_printer/lib.rs): bun_core::printer and bun_js_printer each carried a full copy of the quoted-string escape loop; printer: emit \u escapes for BEL/VT when quoting for JSON #36746 (BEL/VT in JSON) had to patch both. The bun_core copy is now canonical, gaining the const-generic Encoding and write_pre_quoted_string_inner (body taken from current js_printer, so it carries the printer: emit \u escapes for BEL/VT when quoting for JSON #36746 fix), with the runtime entry point dispatching over it; js_printer re-exports. Two knock-on effects for the existing bun_core::quote_for_json callers (sourcemap chunks, macros): it now pre-reserves ~12.5% slack up front like the js_printer copy did (same output, one fewer regrowth on typical input), and the runtime entry point is monomorphized once per encoding instead of branching on encoding inside the loop.
  • Resolver (resolver.rs): the two sequential exports-map attempts in load_node_modules (plain, then with .js stripped) become one loop in resolve_esm_exports. handle_esm_resolution is called unchanged, so resolver: auto-resolve extensions for wildcard exports/imports targets #36299's wildcard extension probing is unaffected.
  • Standalone (StandaloneModuleGraph.rs): from_executable validated the trailer once per platform; only get_data() differs, so it is selected under cfg and validated once.
  • One behavior change, in its own commit: generate_chunk_bytecode no longer ref_()s the source provider URL before wrapping it in OwnedString. create_format already returns +1 and bytecode generation only borrows the string (both C++ entry points use toWTFString; NodeCompileCache pairs clone_utf8 directly with deref), so the extra ref leaked one WTF::StringImpl per bytecode chunk. All three original inline sites had it; consolidating them made it a one-line fix. Flagged by review.
  • Tests pinning the consolidated paths: exports-map .js retry (and that .mjs does not retry), linked sourcemap with publicPath, and the UTF-16 ${ escape.

Salvage notes

Main moved ~3000 PRs past the original base, so rather than rebase the branch was rebuilt on main after auditing each piece of the original against current main:

Verification

  • Per-helper equivalence: configure_and_dispatch_parse_task sets exactly what each of the five sites set (the extra is_entry_point = false / io_task.node.next = null at three sites are no-ops via ParseTask::init / Node::default()); standalone_placeholder_output_file elides only fields equal to OutputFileInit::default(); resolve_esm_exports passes the original subpath to handle_esm_resolution on both attempts as before; the moved escaper is the current js_printer body verbatim.
  • cargo check --workspace, rustfmt, and clippy clean on all touched crates.
  • Debug-build suites green: bundler_string (60), bun-build-api (53, includes printer: emit \u escapes for BEL/VT when quoting for JSON #36746's BEL/VT test and the new publicPath test), bundler_edgecase (136), bundler_loader (50, includes the new .xml loader arm), bundler_plugin (53), standalone (23), bundler_compile (60 of 61; the one failure, HelloWorldWithProcessVersionsBun, is a version-string check that fails identically on main's debug build and does not touch from_executable).

no test proof · iteration 23 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bun-build-api.test.ts test/bundler/bundler_edgecase.test.ts

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator
Updated 6:22 PM PT - Aug 10th, 2026

@alii, your commit c0e4c519b9d503a0af7926eb165ba9373d75dae3 passed in Build #91760! 🎉


🧪   To try this PR locally:

bunx bun-pr 31998

That installs a local version of the PR into your bun-31998 executable, so you can run:

bun-31998 --bun

@alii
alii marked this pull request as ready for review June 9, 2026 20:18
@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@alii
alii force-pushed the claude/split/bundler branch from e0aa541 to dfb8e9d Compare June 9, 2026 20:19
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Centralizes duplicated helpers and delegations across components: moves JS string escaping into bun_core::printer with a const-generic Encoding and public can_print_without_escape; unifies ParseTask enqueue/dispatch via a shared helper; extracts linker output helpers and bytecode generation; adds ParseResult/export-default construction helpers; consolidates bundler Fs with bun_resolver::cache; refactors resolver fs to delegate to fs_full; simplifies standalone executable data validation; and adds bundler tests.

Changes

Consolidate duplicated helpers and module delegations

Layer / File(s) Summary
String escaping refactor to const-generic encoding dispatch
src/bun_core/string/mod.rs, src/js_printer/lib.rs
bun_core::printer moves string escaping to a const-generic Encoding, makes can_print_without_escape public, refactors write_pre_quoted_string/inner and quote_for_json to dispatch on encoding at compile-time, and js_printer re-exports those primitives.
Parse-task configuration and dispatch unification
src/bundler/bundle_v2.rs
Introduces configure_and_dispatch_parse_task to centralize ParseTask field initialization, onLoad plugin enqueue vs worker pool scheduling, and copy-for-bundling registration; dev-server, enqueue_entry_item, enqueue_parse_task, enqueue_parse_task2, and JS-thread on_resolve call the helper.
Shared output-writing helpers for linker output
src/bundler/linker_context/writeOutputFilesToDisk.rs, src/bundler/linker_context/generateChunksInParallel.rs
Introduces append_linked_sourcemap_url, generate_chunk_bytecode, standalone_placeholder_output_file, and BYTECODE_EXTENSION (pub(crate)); generateChunksInParallel uses them for linked sourcemap trailers, bytecode cache generation, and standalone-mode chunk placeholders.
Transpiler ParseResult and export-default construction helpers
src/bundler/transpiler.rs
Adds ParseResult::with_ast constructor and export_default_stmt/export_default_parse_result helpers; parse_data_loader, parse_text_loader, parse_md_loader, and parse_wasm_loader use these helpers instead of manual AST/ParseResult struct literals.
Bundler cache Fs consolidation to bun_resolver::cache
src/bundler/cache.rs
Removes the local Fs implementation and related impl Fs methods; re-exports Contents, Entry, ExternalFreeFunction, and Fs from bun_resolver::cache; initializes Set::init via Fs::default().
Resolver filesystem refactoring and fs_full delegation
src/resolver/fs.rs, src/resolver/lib.rs
fs.rs re-exports BOM, adds ModKey::from_file helper, and extracts kind_impl with injected closures for instance-specific side effects. lib.rs delegates RealFS::adjust_ulimit, RealFS::kind, and temp-dir helpers to fs_full, removing inline ModKey constructor.
Resolver node-modules ESM exports resolution consolidation
src/resolver/resolver.rs
Adds private Resolver::resolve_esm_exports helper to centralize ESM exports map resolution, handle_esm_resolution call, and conditional .js-extension retry; load_node_modules uses the helper.
Standalone module-graph executable data validation unification
src/standalone_graph/StandaloneModuleGraph.rs
Unifies platform-specific embedded data selection into a single data initializer and consolidates shared trailer/offset/size validation and from_bytes_alloc handling for all platforms.
Bundler tests
test/bundler/bun-build-api.test.ts, test/bundler/bundler_edgecase.test.ts, test/bundler/bundler_string.test.ts
Adds tests for linked sourcemap publicPath prefix behavior, package-exports .js-extension retry edge-cases, and a UTF-16 template-string EscapedDollarUnicode case.

Possibly related PRs

  • oven-sh/bun#32116: Refactors standalone linker sourcemap emission and placeholder output-file handling in generateChunksInParallel.rs and writeOutputFilesToDisk.rs, overlapping directly with this PR's linker output consolidations.

Suggested reviewers

  • Jarred-Sumner
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary deduplication changes across bundler dispatch, output helpers, string escaping, and exports-map retry logic.
Description check ✅ Passed The description explains the changes, behavior considerations, rebasing notes, and verification results in sufficient detail, despite different section headings from the template.

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

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Adopted and, after main moved ~3000 PRs, rebuilt on current main rather than rebased. Two of the seven original dedups were made moot by #35002 and dropped; the other five were re-applied (two with small adaptations for #37071 and #36746) as five focused commits, net -234 lines. Details in the PR body. Review threads all resolved; CI running on the rebuilt head.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/resolver/resolver.rs`:
- Around line 3664-3674: The global-cache/module-folder branch in
load_node_modules resolves At/AtConditional using a different condition set
(conditions.import) than resolve_esm_exports (which uses
self.opts.conditions.style), causing inconsistent exports selection; update the
load_node_modules branch to call resolve_esm_exports (or otherwise reuse its
logic) for the global-cache path so both code paths use the same condition
resolution, or change resolve_esm_exports and load_node_modules to both consult
the same condition source (e.g., unify to self.opts.conditions.style) for
At/AtConditional handling; target symbols: resolve_esm_exports,
load_node_modules, At, AtConditional, self.opts.conditions.style,
conditions.import.
- Around line 3703-3705: Don't overwrite the resolved module type after
handle_esm_resolution(): remove the assignment that reassigns out.module_type
from the caller's module_type in the success path (the lines setting
out.is_node_module = true; out.module_type = *module_type; self.extension_order
= prev_extension_order). Instead, preserve the module type that
handle_esm_resolution() computed; if the inexact-resolution branch truly needs
the caller's module_type, set that value inside handle_esm_resolution() for only
that branch rather than resetting out.module_type here, and keep restoring
self.extension_order = prev_extension_order and setting out.is_node_module only
where appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3658e8dc-9d9e-41f0-a551-e7caf3677c49

📥 Commits

Reviewing files that changed from the base of the PR and between a988615 and b713d32.

📒 Files selected for processing (11)
  • src/bun_core/string/mod.rs
  • src/bundler/bundle_v2.rs
  • src/bundler/cache.rs
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/bundler/linker_context/writeOutputFilesToDisk.rs
  • src/bundler/transpiler.rs
  • src/js_printer/lib.rs
  • src/resolver/fs.rs
  • src/resolver/lib.rs
  • src/resolver/resolver.rs
  • src/standalone_graph/StandaloneModuleGraph.rs

Comment thread src/resolver/resolver.rs
Comment thread src/resolver/resolver.rs
@robobun
robobun force-pushed the claude/split/bundler branch from 66e55c5 to 165595a Compare June 12, 2026 12:18

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/bundler/linker_context/generateChunksInParallel.rs (1)

615-724: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't bake a single linked sourcemap URL into the standalone chunk cache.

standalone_chunk_contents stores one rendered JS/CSS buffer per chunk, but this block appends //# sourceMappingURL= before that buffer is reused by every HTML output. When public_path is empty and the same chunk is inlined into multiple standalone HTML files under different directories, the relative .map path chosen here is only correct for the first matched HTML entry point and wrong for the others. Move the linked-URL injection to the HTML-specific assembly path, or keep the sourcemap payload separate and append the trailer per consuming HTML output instead of per shared chunk buffer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bundler/linker_context/generateChunksInParallel.rs` around lines 615 -
724, This code currently mutates the shared chunk buffer by appending the linked
sourceMappingURL inside the loop (see chunks[ci], buffer,
source_map_final_rel_path, and standalone_sourcemaps), which bakes one relative
.map path into a chunk reused across multiple HTML files; instead, stop writing
the sourceMappingURL into buffer here—only finalize and store the external map
bytes in standalone_sourcemaps (output_source_map) and, if needed, also store
the computed source_map_final_rel_path (or its components a/b) alongside that
map; then move the actual //# sourceMappingURL= trailer injection into the HTML
assembly path where each HTML output renders the chunk (the code that consumes
standalone_sourcemaps and writes standalone_chunk_contents), appending the
correct per-HTML relative URL at that point.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bundler/bundle_v2.rs`:
- Around line 2509-2518: The code currently uses the original parameter `loader`
when doing copy-for-bundling bookkeeping after calling
`self.enqueue_on_load_plugin_if_needed(task)`, so tasks whose `task.loader` was
changed (e.g., from Loader::File to Loader::Js/Css/Json for dataurl: inputs) get
incorrect bookkeeping; update the branch to inspect and use the task's
post-dispatch loader (e.g., read `task.loader` or call the accessor that returns
the task's current loader) when calling `should_copy_for_bundling()` and when
updating `additional_files` (AdditionalFile::SourceIndex),
`items_side_effects_mut()`, and `self.graph.estimated_file_loader_count` so the
graph state reflects the actual parse path that will run instead of the original
`loader` parameter.

---

Outside diff comments:
In `@src/bundler/linker_context/generateChunksInParallel.rs`:
- Around line 615-724: This code currently mutates the shared chunk buffer by
appending the linked sourceMappingURL inside the loop (see chunks[ci], buffer,
source_map_final_rel_path, and standalone_sourcemaps), which bakes one relative
.map path into a chunk reused across multiple HTML files; instead, stop writing
the sourceMappingURL into buffer here—only finalize and store the external map
bytes in standalone_sourcemaps (output_source_map) and, if needed, also store
the computed source_map_final_rel_path (or its components a/b) alongside that
map; then move the actual //# sourceMappingURL= trailer injection into the HTML
assembly path where each HTML output renders the chunk (the code that consumes
standalone_sourcemaps and writes standalone_chunk_contents), appending the
correct per-HTML relative URL at that point.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc65c06b-533f-4c49-a0f6-6864f783be7f

📥 Commits

Reviewing files that changed from the base of the PR and between efb0f3c and 165595a.

📒 Files selected for processing (14)
  • src/bun_core/string/mod.rs
  • src/bundler/bundle_v2.rs
  • src/bundler/cache.rs
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/bundler/linker_context/writeOutputFilesToDisk.rs
  • src/bundler/transpiler.rs
  • src/js_printer/lib.rs
  • src/resolver/fs.rs
  • src/resolver/lib.rs
  • src/resolver/resolver.rs
  • src/standalone_graph/StandaloneModuleGraph.rs
  • test/bundler/bun-build-api.test.ts
  • test/bundler/bundler_edgecase.test.ts
  • test/bundler/bundler_string.test.ts

Comment thread src/bundler/bundle_v2.rs
@robobun

robobun commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Re the outside-diff review note on generateChunksInParallel.rs lines 615-724 (linked sourceMappingURL baked into the shared standalone chunk buffer): that block was added on main by #32116 (6128f8e) and is untouched by this PR; the inline trailer append there exists verbatim in that commit. This PR's append_linked_sourcemap_url helper is only used in the non-standalone chunk loop, with semantics identical to main. Whether the standalone path should defer the trailer to per-HTML assembly is a follow-up question for #32116.

@claude claude Bot 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.

I didn't find any bugs, but this is a ~900-line refactor across hot bundler/resolver/printer paths with several non-trivial helper extractions (parse-task dispatch, resolve_esm_exports loop, kind_impl closure-injection, the bun_core::printer move) plus a manual rebase over #32116 — worth a human pass to confirm the behavioral-equivalence claims.

Extended reasoning...

Overview

This PR deduplicates ~900 lines across the bundler, resolver, JS printer, and standalone-graph code. The main moves: (1) the const-generic string escaper migrates from js_printer into bun_core::printer and the old runtime-encoding write_pre_quoted_string becomes a dispatcher over it; (2) five parse-task enqueue sites in bundle_v2.rs collapse into configure_and_dispatch_parse_task; (3) linked-sourcemap/bytecode/standalone-placeholder glue is extracted into shared helpers in writeOutputFilesToDisk.rs; (4) the bundler's local cache::Fs is dropped in favor of the resolver's; (5) resolver::fs re-exports bun_core::immutable::BOM, adds ModKey::from_file, and extracts kind_impl with injected closures so the inline lib.rs RealFS and fs_full::RealFS share one body; (6) the two inline ESM-exports resolution blocks in load_node_modules become a single resolve_esm_exports loop; (7) the per-OS StandaloneModuleGraph::from_executable bodies collapse into one. Tests are added to pin the consolidated paths.

Security risks

None apparent. No auth, crypto, permissions, or untrusted-input parsing is introduced; the standalone-graph trailer/offset validation is unchanged, just deduplicated. The string escaper move preserves the same escaping rules.

Level of scrutiny

High. These are production-critical hot paths (module resolution, parse-task scheduling, output writing, string escaping) where a subtle field-initialization or loop-condition divergence would manifest as hard-to-reproduce bundler bugs. Several extractions are not purely mechanical: the consolidated parse-task helper sets io_task.node.next and is_entry_point for call sites that previously relied on ParseTask::init/Default; the resolve_esm_exports two-pass logic is rewritten as a loop with a length-comparison terminator; the two kind implementations being merged had minor Windows-side differences (ffi::zeroed vs zeroed_unchecked, Fd::from_system vs from_native, CreateFileW namespacing); and the branch was manually rebased over #32116's standalone-sourcemap changes.

Other factors

The bug-hunting system found nothing. CodeRabbit raised two concerns on resolve_esm_exports that were both shown to be pre-existing behavior and retracted. The PR description documents a careful field-by-field equivalence audit against ParseTask::init/Default/OutputFileInit::default(), and CI is green except for darwin runner flakes on unrelated files. All of that raises confidence, but given the breadth, the hot-path nature, and the rebase resolution, a human reviewer signing off on the equivalence claims is the right bar here.

@robobun
robobun force-pushed the claude/split/bundler branch from 165595a to 89759f1 Compare June 20, 2026 00:10

@claude claude Bot 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.

I didn't find any bugs, but this is a ~900-line refactor across the bundler, resolver, and the hot JS-printer string escaper, with two manual rebase resolutions — worth a human pass to confirm the field-by-field equivalence claims.

Extended reasoning...

Overview

This PR deduplicates code across 14 files in the bundler, resolver, JS printer, and standalone-graph subsystems, netting roughly -900 lines. The main moves are: (1) the const-generic string-escape loop relocates from js_printer into bun_core::printer with the old runtime-encoding entry point becoming a dispatcher; (2) five parse-task enqueue sites in bundle_v2.rs collapse into configure_and_dispatch_parse_task; (3) linker output glue (linked-sourcemap trailer, bytecode generation, standalone placeholder) is extracted into shared helpers; (4) the bundler's local cache::Fs is dropped in favor of the resolver's; (5) the two ESM exports-map resolution blocks in load_node_modules become a loop in resolve_esm_exports; (6) RealFS::kind and several temp-dir/ulimit/ModKey bodies in the inline resolver/lib.rs delegate to fs_full; and (7) the per-OS StandaloneModuleGraph::from_executable bodies are unified.

Security risks

None introduced. The standalone-graph trailer/offset validation is consolidated verbatim, the resolver fs path-handling bodies are moved rather than rewritten, and no new untrusted-input parsing or auth/permission surfaces are touched.

Level of scrutiny

This warrants a careful human review. Although framed as pure code motion, several extractions are non-mechanical: resolve_esm_exports rewrites two sequential blocks as a retry loop, configure_and_dispatch_parse_task must match every prior call site's field initialization (and now unconditionally clears io_task.node.next, which two of the original sites did not), the string escaper is the hot path for all JS output, and the PR has been rebased twice over conflicting changes (#32116 standalone sourcemaps, #32507 AST shrink) with hand-resolved merges. The author's verification section is thorough and CodeRabbit's three findings were all correctly rebutted as pre-existing behavior, but the equivalence claims rest on per-field audits that a maintainer should spot-check.

Other factors

The bug-hunting system found nothing, new tests pin the consolidated paths (exports-map .js retry, linked-sourcemap publicPath, UTF-16 $\{ escaping), and the relevant bundler suites pass on the rebased head. All review threads are resolved. Still, the breadth (bundler dispatch + resolver + printer + linker output) and the manual rebase resolutions push this out of auto-approve territory.

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

On the one concrete point in the claude bot review ("unconditionally clears io_task.node.next, which two of the original sites did not"): ThreadPoolLib::Node is #[derive(Default)] with next: *mut Node, so Node::default().next is null. All five pre-PR sites construct the task via ParseTask::init() or ..Default::default(), both of which initialize io_task.node to Node::default(), so io_task.node.next is already null before the helper runs. The explicit assignment in configure_and_dispatch_parse_task is a no-op at those two sites; it is kept for the three sites where the original code cleared it explicitly.

@robobun
robobun force-pushed the claude/split/bundler branch from 89759f1 to 2a448a0 Compare June 20, 2026 08:07
@robobun
robobun force-pushed the claude/split/bundler branch from 2a448a0 to 757a2b7 Compare June 29, 2026 09:48
@robobun
robobun force-pushed the claude/split/bundler branch 2 times, most recently from 86c32eb to a4ef5e4 Compare July 13, 2026 18:40

@claude claude Bot 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.

No bugs found, but this is a large behavior-preserving refactor (~900 lines removed across bundler dispatch, resolver fs, js_printer, and standalone-graph) that has been rebased over six conflicting mainline changes — worth a maintainer's eyes on the equivalence claims.

What was reviewed:

  • configure_and_dispatch_parse_task against all five pre-PR inline sites — the added io_task.node.next = null is a no-op at the two sites that omitted it (Node::default() already nulls it), and is_entry_point defaults to false where previously unset.
  • resolve_esm_exports loop: the retry guard (resolve_subpath.len() < subpath.len() || ext != ".js" || len <= 3) reproduces the original two-block sequence, and handle_esm_resolution still receives the original subpath on the retry.
  • The moved write_pre_quoted_string_inner in bun_core::printer matches the deleted js_printer body byte-for-byte on the escape arms; the UTF-16 ${ lookahead and surrogate handling are unchanged.
  • standalone_placeholder_output_file / OutputFileInit::default() — confirmed the elided fields (hash, input_path, display_size, etc.) default to the same values the explicit literals set.
Extended reasoning...

Overview

This PR deduplicates ~900 lines across four subsystems: (1) the bundler's parse-task enqueue sites collapse into configure_and_dispatch_parse_task; (2) the bundler-local cache::Fs is deleted in favor of the re-exported bun_resolver::cache::Fs; (3) linker output helpers (append_linked_sourcemap_url, generate_chunk_bytecode, standalone_placeholder_output_file) are extracted and shared between generateChunksInParallel and writeOutputFilesToDisk; (4) the const-generic string escaper moves from js_printer into bun_core::printer with the runtime-encoding wrapper becoming a dispatcher; (5) resolver/fs.rs extracts kind_impl with injected closures so the inline lib.rs RealFS can delegate to it, and ModKey::from_file / adjust_ulimit / temp-dir helpers become thin forwards; (6) resolve_esm_exports folds the two exports-map resolution passes into a loop; (7) StandaloneModuleGraph::from_executable unifies three per-platform copies of the trailer/offset validation. Three new tests pin the consolidated paths.

Security risks

None introduced. The standalone-graph consolidation touches unsafe pointer arithmetic reading the embedded module trailer, but the unified body is byte-identical to each of the three per-platform copies it replaces (same bounds check, same read_unaligned, same SAFETY comments). The resolver kind_impl extraction preserves the fd close-or-store scopeguard and the Windows reparse-point handling verbatim. No new user-controlled input paths, auth, or crypto surfaces.

Level of scrutiny

High. This touches the bundler's parse-task scheduling (correctness here affects every build), the resolver's filesystem cache (fd lifecycle, symlink resolution), the JS printer's hot escape loop, and the standalone executable loader. Each helper extraction is claimed behavior-preserving, but the equivalence depends on struct defaults (ParseTask::init, OutputFileInit::default(), Node::default()) matching the elided explicit assignments, and on the resolve_esm_exports loop's exit condition reproducing the original two-block control flow exactly. The PR has also been rebased six times over conflicting changes (#32116, #32507, #33035, #33072, #33909, plus the AST-shrink and BOM-dedup), and each resolution is a place a subtle divergence could slip in. A maintainer familiar with the pre-PR shape of these files should confirm the field-by-field equivalence the PR description claims.

Other factors

The bug-hunting pass found nothing. All three CodeRabbit findings were correctly refuted as pre-existing behavior (the out.module_type overwrite, the global-cache condition-set divergence, and the post-dispatch loader bookkeeping all match main). The prior concern about the unconditional io_task.node.next clear was resolved — ThreadPoolLib::Node is #[derive(Default)] so the two sites that didn't clear it explicitly already had it null. New tests cover the exports-map .js retry (both positive and the .mjs-doesn't-retry negative), the linked-sourcemap publicPath prefix, and the UTF-16 template-literal \${ escape. The relevant suites (bundler_string, bundler_loader, bundler_edgecase, bun-build-api, resolver-cache) are reported passing on the rebased head. Given the breadth and the number of rebase resolutions, this should not be auto-approved.

@alii

alii commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@robobun get this mergeable — or close it if the changes are already covered on main / too stale to salvage.

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

On it. Quick findings so far: main is ~3000 PRs past this branch's base and the rebase conflicts in 7 files, largely because #35002 (dead-code sweep) deleted ~1400 lines from resolver/fs.rs and ~1100 from js_printer/lib.rs. One of the seven dedups here (the bundler-local cache::Fs) already landed on main via that sweep; the other six still appear absent from main. Auditing each piece against current main now to decide between salvaging the still-relevant pieces and closing; will report back shortly.

…-loader ParseResult construction

Five parse-task enqueue sites repeated the same tail (scheduling fields,
onLoad plugin hand-off, copy-for-bundling bookkeeping, pool schedule);
fold it into configure_and_dispatch_parse_task. The linked-sourcemap
trailer, bytecode generation, and standalone placeholder output file
were each written out twice across generateChunksInParallel and
writeOutputFilesToDisk; share one helper per concern, and drop the
write-only fdpath in generateChunksInParallel. The text/md/data/wasm
loaders built identical export-default statements and ParseResult
literals; route them through export_default_parse_result and
ParseResult::with_ast.

No behavior change: the helpers set exactly the fields the inline
copies set, and the elided fields match ParseTask::init / Default and
OutputFileInit::default().

Co-authored-by: Alistair Smith <hi@alistair.sh>
@robobun
robobun force-pushed the claude/split/bundler branch from a4ef5e4 to 337d0e4 Compare August 10, 2026 22:49
@robobun robobun changed the title Deduplicate bundler parse-task dispatch and resolver fs cache Deduplicate bundler dispatch and output glue, the JS string escaper, and the exports-map retry Aug 10, 2026
@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@alii Mergeable now. Main had moved ~3000 PRs, so instead of rebasing I audited each of the seven original pieces against current main and rebuilt the branch on top of it:

Now five focused commits on main, net -234 lines, co-authored to you. Workspace check / fmt / clippy clean, the bundler, build-api, edgecase, loader, plugin, standalone, and compile suites pass locally (the compile suite's one failure is a pre-existing debug-build version-string check, same as on main). PR title and body rewritten for the reduced scope. Format and clippy are already green on the new head; Buildkite build 91671 is running.

robobun and others added 4 commits August 10, 2026 22:56
bun_core::printer and bun_js_printer each carried a full copy of the
quoted-string escape loop; the BEL/VT JSON fix had to be applied to
both. Make the bun_core copy canonical: it gains the const-generic
Encoding parameter and write_pre_quoted_string_inner (the body is the
current js_printer loop, including the BEL/VT handling), the runtime
write_pre_quoted_string becomes a dispatcher over it, and quote_for_json
adopts the pre-reservation heuristic. js_printer re-exports Encoding,
write_pre_quoted_string_inner, and quote_for_json; its const facade maps
the bun_core error into the crate error.

Co-authored-by: Alistair Smith <hi@alistair.sh>
…exports

The node_modules exports-map resolution was two sequential copies of
build-ESModule / resolve / handle_esm_resolution, the second with the
.js extension stripped. Express it as one loop; handle_esm_resolution
still receives the original subpath on both attempts and the success
side effects are unchanged.

Co-authored-by: Alistair Smith <hi@alistair.sh>
from_executable repeated the size check, trailer compare, and offsets
read once per platform; only the get_data() source differs, so select
that under cfg and validate once.

Co-authored-by: Alistair Smith <hi@alistair.sh>
… and UTF-16 dollar-brace escaping

Co-authored-by: Alistair Smith <hi@alistair.sh>
@robobun
robobun force-pushed the claude/split/bundler branch from 337d0e4 to 8479ae5 Compare August 10, 2026 22:56
Comment thread src/bundler/linker_context/writeOutputFilesToDisk.rs Outdated
create_format already hands back a +1 reference, and bytecode generation
only borrows the string (both C++ entry points call toWTFString on it, and
NodeCompileCache pairs clone_utf8 directly with deref). The extra ref_()
before wrapping in OwnedString therefore left every chunk's URL string at
refcount 1 after the OwnedString dropped. All three original inline sites
had the same extra ref_(); now that they share one helper, let OwnedString
adopt the constructor's reference directly.

@claude claude Bot 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.

I reviewed this PR and didn't find any bugs (the pre-existing source_provider_url leak flagged in the previous run is now fixed in bfe5ec9). Because it's a wide behavior-preserving refactor across the bundler dispatch path, the linker output glue, the hot-path JS string escaper, the exports-map resolver, and the standalone loader — plus one intentional refcount change — a human look would still be worthwhile.

What was reviewed

  • resolve_esm_exports loop: traced both iterations — second attempt strips .js and passes the original subpath to handle_esm_resolution, and resolve_subpath.len() < subpath.len() terminates after the retry, matching the original two sequential blocks; out.module_type = *module_type preserves main's existing assignment.
  • configure_and_dispatch_parse_task: the unconditional is_entry_point / io_task.node.next = null writes are no-ops at the sites that omitted them (per ParseTask::init / Node::default()).
  • standalone_placeholder_output_file: elided fields checked against OutputFileInit::default(); both original call sites' explicit values match.
  • String escaper: the moved write_pre_quoted_string_inner body is byte-identical to the deleted js_printer copy; the runtime-encoding wrapper now dispatches to it, and quote_for_json picks up the grow_if_needed heuristic that only the js_printer copy had.
Extended reasoning...

Overview

This PR consolidates duplicated code across five subsystems: (1) five parse-task enqueue tails in bundle_v2.rs into configure_and_dispatch_parse_task; (2) linked-sourcemap trailer, bytecode generation, and standalone placeholder output-file construction shared between generateChunksInParallel.rs and writeOutputFilesToDisk.rs; (3) text/md/data/wasm loader export default boilerplate in transpiler.rs; (4) the ~220-line quoted-string escape loop, moved from js_printer into bun_core::printer as canonical with js_printer re-exporting; (5) the two sequential exports-map resolution attempts in resolver.rs into a single loop; (6) three per-platform copies of the standalone trailer validation into one cfg-selected block. Net -236 lines. There is one intentional behavior change in its own commit: dropping an extra .ref_() on the source-provider URL in generate_chunk_bytecode, fixing a per-chunk WTF::StringImpl leak that all three original inline sites had (this was my finding from the previous run, now applied). Three tests are added pinning the consolidated paths.

Security risks

None identified. The changes are internal refactors with no new input parsing, no auth/crypto/permissions surface, and no new FFI signatures. The standalone trailer validation is unchanged in substance (same length check, same trailer comparison, same read_unaligned of Offsets); only the per-platform get_data() selection is factored out.

Level of scrutiny

High. Every touched path is production-critical: parse-task dispatch and copy-for-bundling bookkeeping affect every bundled file; the string escaper is an explicitly hot path used for every string literal printed; resolve_esm_exports gates package exports resolution; the linker helpers shape every chunk's emitted bytes and bytecode. REVIEW.md's "treat every refactor as guilty until proven behavior-preserving" applies directly. The PR description does provide per-helper equivalence arguments, and the CodeRabbit findings on this PR were all correctly rebutted as pre-existing behavior — but a maintainer should confirm the equivalence claims independently, particularly the OutputFileInit::default() field elisions and the string-escaper move (the bun_core quote_for_json now picks up a grow_if_needed pre-reservation it didn't have before, which is a small allocation-pattern change even if output-identical).

Other factors

  • The refcount fix passed the debug-build bytecode/compile tests per the author, which would trip WTF refcount assertions on an over-deref, and matches the NodeCompileCache reference pattern.
  • All prior review threads (CodeRabbit ×3, my leak finding) are resolved with no outstanding requests.
  • Tests were added for the .js retry (positive and negative), linked-sourcemap publicPath, and the UTF-16 ${ escape — good coverage of the consolidated edge cases, though the configure_and_dispatch_parse_task and standalone-trailer consolidations have no new tests (they rely on existing suites).
  • The PR was rebuilt on current main after ~3000 intervening PRs; two of the original seven pieces were dropped as moot. The rebuild is well-documented but adds surface area for a human to verify against the current base.

Given the breadth across critical paths and the one intentional memory-lifetime change, this exceeds the "simple, mechanical, or obvious" bar for auto-approval.

@claude claude Bot 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.

The refcount fix in bfe5ec9 addresses my earlier finding — OwnedString now adopts create_format's +1 directly, matching the NodeCompileCache pattern. This pass on the merged head found no bugs. Given the scope (five parse-task dispatch sites, the hot-path string escaper move to bun_core, the resolver exports-map loop reshaping, standalone trailer consolidation, plus the one intentional refcount behavior change), a human sign-off is still worthwhile.

What was reviewed:

  • configure_and_dispatch_parse_task against all five original sites — the extra is_entry_point = false / io_task.node.next = null are no-ops via ParseTask::init / Node::default().
  • resolve_esm_exports loop: second iteration passes the original subpath to handle_esm_resolution and only strips .js, matching the pre-PR inline paths; termination guaranteed by the resolve_subpath.len() < subpath.len() guard.
  • The moved escaper body is byte-identical to the js_printer copy (including the #36746 BEL/VT arms and the ${ UTF-16 lookahead); quote_for_json's new pre-reserve is additive.
  • standalone_placeholder_output_file elides only fields equal to OutputFileInit::default().
Extended reasoning...

Overview

Behavior-preserving deduplication across five subsystems: bundler parse-task dispatch (bundle_v2.rs, five sites → one helper), linker output glue (generateChunksInParallel.rs / writeOutputFilesToDisk.rs, three shared helpers), transpiler loader helpers (transpiler.rs), the JS string escaper (bun_core/string/mod.rs becomes canonical, js_printer/lib.rs re-exports), the resolver exports-map .js retry (resolver.rs, two sequential attempts → one loop), and the standalone trailer validator (StandaloneModuleGraph.rs, three cfg blocks → one). Net -236 lines. Three new tests pin the consolidated paths. One intentional behavior change in its own commit (bfe5ec9): dropping the extra .ref_() in generate_chunk_bytecode, which I flagged in the prior review pass and which robobun confirmed and fixed.

Security risks

None identified. No auth, crypto, or permission code touched. The standalone trailer validation is unchanged in substance (same length check, same read_unaligned, same TRAILER comparison) — only the per-platform get_data() selection moved under cfg. The resolver change does not alter which paths are accepted, only how the two attempts are expressed.

Level of scrutiny

High. Every touched file is on a hot or correctness-critical path: the string escaper runs on every printed string literal, the resolver on every bare-specifier import with an exports map, parse-task dispatch on every bundled file, and from_executable on every compiled-binary startup. The PR is framed as behavior-preserving, and REVIEW.md's guidance is to treat refactors as guilty until proven so — which is why I traced each helper against its original sites rather than trusting the diff shape. The escaper move also has a knock-on effect on existing bun_core::quote_for_json callers (sourcemap chunks, macros): they now get the const-generic monomorphized loop and a ~12.5% pre-reserve, which the PR body calls out and which is output-identical.

Other factors

My prior review (2026-08-10) found the source_provider_url refcount leak; it was fixed in bfe5ec9 and verified under the debug build's WTF refcount assertions. Since then the only change is a merge from main (c0e4c51). All CodeRabbit threads are resolved (each was a pre-existing behavior, not a PR-introduced regression). The PR body's per-helper equivalence notes check out against the diff. Bundler/build-api/edgecase/loader/plugin/standalone/compile suites reported green locally. Given the breadth across five critical subsystems and the one deliberate behavior change, deferring to a human maintainer for final sign-off rather than auto-approving.

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