Skip to content

bundler: fix ESMConditions capacity with user-provided conditions - #30470

Closed
robobun wants to merge 1 commit into
mainfrom
farm/8d89bf3d/fix-esm-conditions-capacity
Closed

bundler: fix ESMConditions capacity with user-provided conditions#30470
robobun wants to merge 1 commit into
mainfrom
farm/8d89bf3d/fix-esm-conditions-capacity

Conversation

@robobun

@robobun robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

ESMConditions.init computes how much capacity to reserve for each condition map before inserting with putAssumeCapacity. The expression used was:

defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len

which Zig parses as:

defaults.len + 2 + (if (allow_addons) 1 else (0 + conditions.len))

So when allow_addons is true (the default), the number of user-provided conditions was never counted towards the reserved capacity. With roughly 6+ custom conditions passed to Bun.build({ conditions }) or bun build --conditions, the subsequent putAssumeCapacity calls exceed the allocation — tripping a debug assertion and writing past the buffer in release builds.

Fix by hoisting the conditional into a local so the addition is unambiguous.

How did you verify your code works?

Added a regression test that builds with 1–20 custom conditions across bun, browser, and node targets. Before this change the test panics with reached unreachable code in MultiArrayList.addOneAssumeCapacity; after, it passes.

Fuzzer fingerprint: 2ca2ef9e67423f31

The expression `if (allow_addons) 1 else 0 + conditions.len` parses as
`if (allow_addons) 1 else (0 + conditions.len)`, so when allow_addons is
true the user-provided conditions were not counted towards the reserved
capacity. With enough custom conditions this tripped the
addOneAssumeCapacity assertion in debug builds and wrote past the
allocation in release builds.
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:53 PM PT - May 10th, 2026

@robobun, your commit f2076e3 has 1 failures in Build #53218 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30470

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

bun-30470 --bun

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR refactors capacity preallocation in ESMConditions.init to compute the addons overhead once instead of repeating an inline conditional, then adds a regression test validating the bundler handles many custom conditions without crashing.

Changes

Bundler Conditions Capacity Optimization

Layer / File(s) Summary
Capacity Computation Refactoring
src/bundler/options.zig
ESMConditions.init computes an extra capacity term from allow_addons once and reuses it in ensureTotalCapacity calls for default_condition_map, import_condition_map, and require_condition_map.
Regression Test
test/bundler/bun-build-api.test.ts
New test many custom conditions does not crash runs Bun.build across multiple target values and conditions array sizes (1, 4, 6, 8, 12, 20), asserting each build succeeds.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: resolving a capacity calculation bug in ESMConditions when user-provided conditions are present.
Description check ✅ Passed The description fully addresses both template sections: it clearly explains what the PR does (the capacity bug and its fix) and how it was verified (regression test with 1-20 conditions across multiple targets).
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.

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


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

@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

🤖 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 `@test/bundler/bun-build-api.test.ts`:
- Around line 819-830: Replace the nested for-loops that iterate over target and
n with parameterized test cases using describe.each or test.each so each
(target, n) combo is reported independently; specifically, create a table/array
of tuples for the matrix of targets ["bun","browser","node"] and ns
[1,4,6,8,12,20], then use describe.each or test.each to iterate those tuples and
inside each test call Bun.build with the same entrypoints and compute conditions
= Array.from({ length: n }, (_, i) => `cond${i}`) and assert build.success;
remove the outer for (const target ...) and inner for (const n ...) loops and
replace them with the parameterized test harness.
🪄 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: bf7fe81a-c1ba-4954-aa3b-e4388fdf9285

📥 Commits

Reviewing files that changed from the base of the PR and between 03ebdf8 and f2076e3.

📒 Files selected for processing (2)
  • src/bundler/options.zig
  • test/bundler/bun-build-api.test.ts

Comment on lines +819 to +830
for (const target of ["bun", "browser", "node"] as const) {
for (const n of [1, 4, 6, 8, 12, 20]) {
const conditions = Array.from({ length: n }, (_, i) => `cond${i}`);
const build = await Bun.build({
entrypoints: [join(dir, "entry.js")],
target,
conditions,
});
expect(build.success).toBe(true);
}
}
});

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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use parameterized cases instead of nested loops for this matrix.

Please convert the (target, n) combinations into parameterized tests so each combo reports as an independent case in failures.

♻️ Suggested refactor
-test("many custom conditions does not crash", async () => {
+describe.each(["bun", "browser", "node"] as const)("many custom conditions does not crash (%s)", target => {
+  test.concurrent.each([1, 4, 6, 8, 12, 20])("n=%i", async n => {
   const dir = tempDirWithFiles("bun-build-many-conditions", {
     "entry.js": `console.log(1);`,
   });

-  for (const target of ["bun", "browser", "node"] as const) {
-    for (const n of [1, 4, 6, 8, 12, 20]) {
-      const conditions = Array.from({ length: n }, (_, i) => `cond${i}`);
-      const build = await Bun.build({
-        entrypoints: [join(dir, "entry.js")],
-        target,
-        conditions,
-      });
-      expect(build.success).toBe(true);
-    }
-  }
-});
+    const conditions = Array.from({ length: n }, (_, i) => `cond${i}`);
+    const build = await Bun.build({
+      entrypoints: [join(dir, "entry.js")],
+      target,
+      conditions,
+    });
+    expect(build.success).toBe(true);
+  });
+});

As per coding guidelines: "Use describe.each() for parameterized tests".

🤖 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 `@test/bundler/bun-build-api.test.ts` around lines 819 - 830, Replace the
nested for-loops that iterate over target and n with parameterized test cases
using describe.each or test.each so each (target, n) combo is reported
independently; specifically, create a table/array of tuples for the matrix of
targets ["bun","browser","node"] and ns [1,4,6,8,12,20], then use describe.each
or test.each to iterate those tuples and inside each test call Bun.build with
the same entrypoints and compute conditions = Array.from({ length: n }, (_, i)
=> `cond${i}`) and assert build.success; remove the outer for (const target ...)
and inner for (const n ...) loops and replace them with the parameterized test
harness.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix crash in Bun.build with many conditions #30466 - Fixes the same ESMConditions.init operator-precedence capacity bug in src/bundler/options.zig with a regression test for the same crash

🤖 Generated with Claude Code

@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30466, which also fixes a second instance of the same precedence bug in loadersFromTransformOptions.

@robobun robobun closed this May 11, 2026
@robobun
robobun deleted the farm/8d89bf3d/fix-esm-conditions-capacity branch May 11, 2026 04:52

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

LGTM — straightforward operator-precedence fix with a regression test.

Extended reasoning...

Overview

This PR fixes an operator-precedence bug in ESMConditions.init (src/bundler/options.zig). The original expression defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len parsed such that conditions.len bound to the else branch, so when allow_addons was true (the default) the user-provided conditions were never counted toward the reserved capacity. The subsequent putAssumeCapacity calls could then overflow the map. The fix hoists the conditional into const extra: usize = if (allow_addons) 1 else 0; and adds it explicitly, making the arithmetic unambiguous. A regression test in test/bundler/bun-build-api.test.ts exercises 1–20 custom conditions across all three targets.

Security risks

None. This is a capacity-reservation arithmetic fix; if anything it removes a potential out-of-bounds write in release builds. No auth, crypto, or untrusted-input parsing is touched.

Level of scrutiny

Low. The change is four lines of obviously-correct arithmetic refactoring whose intent and effect are easy to verify by inspection. I checked that the new reserved capacity (defaults.len + 2 + extra + conditions.len) is ≥ the actual number of putAssumeCapacity calls for each of the four maps, and it is. The style_condition_map line is unchanged and was already correct.

Other factors

The only finding from the bug-hunting pass is a nit that the new test should use test.concurrent for consistency with its neighbors — purely stylistic and not a reason to withhold approval. The PR includes a targeted regression test that would have caught the original bug.

expect(text).toContain(" globalThis.");
});

test("many custom conditions does not crash", async () => {

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.

🟡 Nit: this should be test.concurrent to match the neighboring top-level tests in this file (and per test/CLAUDE.md's guidance to prefer concurrent tests when they write files / spawn builds with no shared state).

Extended reasoning...

Summary

The new test "many custom conditions does not crash" is declared with plain test(...) rather than test.concurrent(...). This is inconsistent with both the surrounding code in this file and the project's documented testing conventions.

Project convention

test/CLAUDE.md states:

Prefer concurrent tests over sequential tests: When multiple tests in the same file spawn processes or write files, make them concurrent with test.concurrent or describe.concurrent unless it's very difficult to make them concurrent.

This test writes files via tempDirWithFiles and runs 18 Bun.build calls — exactly the kind of test the guideline targets.

Local consistency

Looking at the immediate neighbors at the top level of this file (outside the describe("Bun.build") block):

  • test.concurrent("macro with nested object", ...)
  • test.concurrent("regression/NODE_PATHBuild api", ...)
  • test.concurrent("regression/GlobalThis", ...) — directly above
  • describe.concurrent("sourcemap boolean values", ...) — directly below

Every adjacent top-level test uses .concurrent. The only nearby block that does not is describe("tsconfig option"), and that one has a clear reason: it calls process.chdir(), which is process-global and cannot safely run concurrently. The new test has no such constraint.

Why concurrency is safe here

Step-by-step:

  1. The test creates an isolated temp directory with tempDirWithFiles("bun-build-many-conditions", ...) — no path collisions with other tests.
  2. It calls Bun.build({ entrypoints, target, conditions }) with no outdir, so it doesn't write to any shared location.
  3. It does not call process.chdir(), mutate process.env, or touch any other process-global state.
  4. The loop variables (target, n, conditions) are all locals.

There is therefore no obstacle to running it concurrently with the other tests in this file.

Impact

This is purely a consistency / test-suite-throughput nit — the test is functionally correct as written and will pass either way. It just runs sequentially when it could run in parallel with its neighbors, and it diverges from the established pattern at this location in the file.

Fix

-test("many custom conditions does not crash", async () => {
+test.concurrent("many custom conditions does not crash", async () => {

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant