mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(bundler): include lazy chunks in frontend.files for compiled fullstack builds (#26024)
## Summary - Fixed lazy-loaded chunks from dynamic imports not appearing in `frontend.files` when using `--splitting` with `--compile` in fullstack builds - Updated `computeChunks.zig` to mark non-entry-point chunks as browser chunks when they contain browser-targeted files - Updated `HTMLImportManifest.zig` to include browser chunks from server builds in the files manifest Fixes #25628 ## Test plan - [ ] Added regression test `test/regression/issue/25628.test.ts` that verifies lazy chunks appear in `frontend.files` - [ ] Manually verified: system bun reports `CHUNK_COUNT:1` (bug), debug bun reports `CHUNK_COUNT:2` (fix) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -166,8 +166,14 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph,
|
||||
defer already_visited_output_file.deinit(bun.default_allocator);
|
||||
|
||||
// Write all chunks that have files associated with this entry point.
|
||||
// Also include browser chunks from server builds (lazy-loaded chunks from dynamic imports).
|
||||
// When there's only one HTML import, all browser chunks belong to that manifest.
|
||||
// When there are multiple HTML imports, only include chunks that intersect with this entry's bits.
|
||||
const has_single_html_import = graph.html_imports.html_source_indices.len == 1;
|
||||
for (chunks) |*ch| {
|
||||
if (ch.entryBits().hasIntersection(&entry_point_bits)) {
|
||||
if (ch.entryBits().hasIntersection(&entry_point_bits) or
|
||||
(has_single_html_import and ch.flags.is_browser_chunk_from_server_build))
|
||||
{
|
||||
if (!first) try writer.writeAll(",");
|
||||
first = false;
|
||||
|
||||
|
||||
@@ -229,6 +229,16 @@ pub noinline fn computeChunks(
|
||||
.output_source_map = SourceMap.SourceMapPieces.init(this.allocator()),
|
||||
.flags = .{ .is_browser_chunk_from_server_build = is_browser_chunk_from_server_build },
|
||||
};
|
||||
} else if (could_be_browser_target_from_server_build and
|
||||
!js_chunk_entry.value_ptr.entry_point.is_entry_point and
|
||||
!js_chunk_entry.value_ptr.flags.is_browser_chunk_from_server_build and
|
||||
ast_targets[source_index.get()] == .browser)
|
||||
{
|
||||
// If any file in the chunk has browser target, mark the whole chunk as browser.
|
||||
// This handles the case where a lazy-loaded chunk (code splitting chunk, not entry point)
|
||||
// contains browser-targeted files but was first created by a non-browser file.
|
||||
// We only apply this to non-entry-point chunks to preserve the correct side for server entry points.
|
||||
js_chunk_entry.value_ptr.flags.is_browser_chunk_from_server_build = true;
|
||||
}
|
||||
|
||||
const entry = js_chunk_entry.value_ptr.files_with_parts_in_chunk.getOrPut(this.allocator(), @as(u32, @truncate(source_index.get()))) catch unreachable;
|
||||
|
||||
89
test/regression/issue/25628.test.ts
Normal file
89
test/regression/issue/25628.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/25628
|
||||
// Bug: Lazy code-splitting chunks are not accessible via frontend.files in fullstack builds
|
||||
// when using --splitting with --compile. The chunks are physically written to disk and embedded
|
||||
// in the executable, but they're filtered out when accessing the embedded files array.
|
||||
|
||||
test("lazy chunks from code splitting should appear in frontend.files", { timeout: 60000 }, async () => {
|
||||
using dir = tempDir("issue-25628", {
|
||||
// Server entry that prints frontend.files and exits
|
||||
"server.ts": `
|
||||
import frontend from "./client.html";
|
||||
|
||||
// Get all file paths from frontend.files
|
||||
const filePaths = frontend.files?.map((f: any) => f.path) ?? [];
|
||||
|
||||
// Count the number of chunk files (lazy chunks are named chunk-xxx.js)
|
||||
const chunkCount = filePaths.filter((p: string) =>
|
||||
p.includes("chunk-")
|
||||
).length;
|
||||
|
||||
// There should be at least 2 chunks:
|
||||
// 1. The main app entry chunk
|
||||
// 2. The lazy-loaded chunk from the dynamic import
|
||||
console.log("CHUNK_COUNT:" + chunkCount);
|
||||
console.log("FILES:" + filePaths.join(","));
|
||||
|
||||
// Exit immediately after printing
|
||||
process.exit(0);
|
||||
`,
|
||||
"client.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>`,
|
||||
"main.js": `
|
||||
// Dynamic import creates a lazy chunk
|
||||
const lazyMod = () => import("./lazy.js");
|
||||
lazyMod().then(m => m.hello());
|
||||
`,
|
||||
"lazy.js": `
|
||||
export function hello() {
|
||||
console.log("Hello from lazy module!");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Build with splitting and compile
|
||||
await using buildProc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "--compile", "server.ts", "--splitting", "--outfile", "server"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
|
||||
buildProc.stdout.text(),
|
||||
buildProc.stderr.text(),
|
||||
buildProc.exited,
|
||||
]);
|
||||
|
||||
expect(buildStderr).not.toContain("error:");
|
||||
expect(buildExitCode).toBe(0);
|
||||
|
||||
// Run the compiled executable
|
||||
const serverPath = isWindows ? "server.exe" : "./server";
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: [serverPath],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [runStdout, runStderr, runExitCode] = await Promise.all([
|
||||
runProc.stdout.text(),
|
||||
runProc.stderr.text(),
|
||||
runProc.exited,
|
||||
]);
|
||||
|
||||
// There should be at least 2 chunk files in frontend.files:
|
||||
// one for the main entry and one for the lazy-loaded module
|
||||
expect(runStdout).toMatch(/CHUNK_COUNT:[2-9]/);
|
||||
expect(runExitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user