fix(bundler): fix --compile with 8+ embedded files (#25859)

## Summary

Fixes #20821

When `bun build --compile` was used with 8 or more embedded files, the
compiled binary would silently fail to execute any code (exit code 0, no
output).

**Root cause:** Chunks were sorted alphabetically by their `entry_bits`
key bytes. For entry point 0, the key starts with bit 0 set (byte
pattern `0x01`), but for entry point 8, the key has bit 8 set in the
byte (pattern `0x00, 0x01`). Alphabetically, `0x00 < 0x01`, so entry
point 8's chunk sorted before entry point 0.

This caused the wrong entry point to be identified as the main entry,
resulting in asset wrapper code being executed instead of the user's
code.

**Fix:** Custom sort that ensures `entry_point_id=0` (the main entry
point) always sorts first, with remaining chunks sorted alphabetically
for determinism.

## Test plan

- Added regression test `compile/ManyEmbeddedFiles` that embeds 8 files
and verifies the main entry point runs correctly
- Verified manually with reproduction case from issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dylan Conway
2026-01-06 15:05:01 -08:00
committed by GitHub
parent 46801ec926
commit 4c492c66b8
3 changed files with 46 additions and 2 deletions

View File

@@ -249,9 +249,29 @@ pub noinline fn computeChunks(
var sorted_keys = try BabyList(string).initCapacity(temp_allocator, js_chunks.count());
// JS Chunks
sorted_keys.appendSliceAssumeCapacity(js_chunks.keys());
sorted_keys.sortAsc();
// sort by entry_point_id to ensure the main entry point (id=0) comes first,
// then by key for determinism among the rest.
const ChunkSortContext = struct {
chunks: *const bun.StringArrayHashMap(Chunk),
pub fn lessThan(ctx: @This(), a_key: string, b_key: string) bool {
const a_chunk = ctx.chunks.get(a_key) orelse return true;
const b_chunk = ctx.chunks.get(b_key) orelse return false;
const a_id = a_chunk.entry_point.entry_point_id;
const b_id = b_chunk.entry_point.entry_point_id;
// Main entry point (id=0) always comes first
if (a_id == 0 and b_id != 0) return true;
if (b_id == 0 and a_id != 0) return false;
// Otherwise sort alphabetically by key for determinism
return bun.strings.order(a_key, b_key) == .lt;
}
};
sorted_keys.sort(ChunkSortContext, .{ .chunks = &js_chunks });
var js_chunk_indices_with_css = try BabyList(u32).initCapacity(temp_allocator, js_chunks_with_css);
for (sorted_keys.slice()) |key| {
const chunk = js_chunks.get(key) orelse unreachable;

View File

@@ -349,6 +349,10 @@ pub fn BabyList(comptime Type: type) type {
bun.strings.sortAsc(this.slice());
}
pub fn sort(this: *Self, comptime Context: type, context: Context) void {
std.sort.pdq(Type, this.slice(), context, Context.lessThan);
}
pub fn writableSlice(
this: *Self,
allocator: std.mem.Allocator,

View File

@@ -735,4 +735,24 @@ const server = serve({
.env(bunEnv)
.throws(true);
});
// When compiling with 8+ entry points, the main entry point should still run correctly.
test("compile with 8+ entry points runs main entry correctly", async () => {
const dir = tempDirWithFiles("compile-many-entries", {
"app.js": `console.log("IT WORKS");`,
"assets/file-1": "",
"assets/file-2": "",
"assets/file-3": "",
"assets/file-4": "",
"assets/file-5": "",
"assets/file-6": "",
"assets/file-7": "",
"assets/file-8": "",
});
await Bun.$`${bunExe()} build --compile app.js assets/* --outfile app`.cwd(dir).env(bunEnv).throws(true);
const result = await Bun.$`./app`.cwd(dir).env(bunEnv).nothrow();
expect(result.stdout.toString().trim()).toBe("IT WORKS");
});
});