fix(bundler): preserve compile:true backward compat with default target

When compile:true is used without an explicit target (or with
target:"browser" but non-HTML entrypoints), fall through to normal bun
executable compile instead of erroring. Standalone HTML mode only
activates when ALL entrypoints are .html files AND target is browser.

This preserves backward compatibility: compile:true alone still produces
a bun executable. The test harness defaults target to "browser", so
compile tests were breaking with the HTML-only validation.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-02-16 05:27:05 +00:00
parent 42550e3a5c
commit 7463fb0466
4 changed files with 64 additions and 47 deletions

View File

@@ -977,9 +977,17 @@ pub const JSBundler = struct {
}
if (this.compile) |*compile| {
// When compile + target=browser, skip the bun executable compile setup.
// The standalone HTML compilation is handled in the bundler instead.
if (this.target != .browser) {
// When compile + target=browser + all HTML entrypoints, produce standalone HTML.
// Otherwise, default to bun executable compile.
const has_all_html_entrypoints = brk: {
if (this.entry_points.count() == 0) break :brk false;
for (this.entry_points.keys()) |ep| {
if (!strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk true;
};
const is_standalone_html = this.target == .browser and has_all_html_entrypoints;
if (!is_standalone_html) {
this.target = .bun;
const define_keys = compile.compile_target.defineKeys();
@@ -1030,16 +1038,17 @@ pub const JSBundler = struct {
return globalThis.throwInvalidArguments("ESM bytecode requires compile: true. Use format: 'cjs' for bytecode without compile.", .{});
}
// --compile --target=browser: produce self-contained HTML
// Validate standalone HTML mode: compile + browser target + all HTML entrypoints
if (this.compile != null and this.target == .browser) {
if (this.code_splitting) {
return globalThis.throwInvalidArguments("Cannot use compile with target 'browser' and splitting", .{});
}
// All entrypoints must be HTML files
for (this.entry_points.keys()) |ep| {
if (!strings.hasSuffixComptime(ep, ".html")) {
return globalThis.throwInvalidArguments("compile with target 'browser' requires all entrypoints to be HTML files", .{});
const has_all_html = brk: {
if (this.entry_points.count() == 0) break :brk false;
for (this.entry_points.keys()) |ep| {
if (!strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk true;
};
if (has_all_html and this.code_splitting) {
return globalThis.throwInvalidArguments("Cannot use compile with target 'browser' and splitting for standalone HTML", .{});
}
}

View File

@@ -1993,7 +1993,14 @@ pub const BundleV2 = struct {
transpiler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace;
transpiler.options.ignore_dce_annotations = config.ignore_dce_annotations;
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.compile_to_standalone_html = config.compile != null and config.target == .browser;
transpiler.options.compile_to_standalone_html = brk: {
if (config.compile == null or config.target != .browser) break :brk false;
// Only activate standalone HTML when all entrypoints are HTML files
for (config.entry_points.keys()) |ep| {
if (!bun.strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk config.entry_points.count() > 0;
};
// When compiling to standalone HTML, don't use the bun executable compile path
if (transpiler.options.compile_to_standalone_html) {
transpiler.options.compile = false;

View File

@@ -3,14 +3,10 @@ pub const BuildCommand = struct {
Global.configureAllocator(.{ .long_running = true });
const allocator = ctx.allocator;
var log = ctx.log;
const user_requested_browser_target = ctx.args.target != null and ctx.args.target.? == .browser;
if (ctx.bundler_options.compile or ctx.bundler_options.bytecode) {
// set this early so that externals are set up correctly and define is right
// When --compile --target=browser is used, keep browser target for standalone HTML
if (ctx.args.target != null and ctx.args.target.? == .browser) {
// Keep browser target
} else {
ctx.args.target = .bun;
}
ctx.args.target = .bun;
}
if (ctx.bundler_options.bake) {
@@ -109,23 +105,24 @@ pub const BuildCommand = struct {
return;
}
if (ctx.args.target != null and ctx.args.target.? == .browser) {
// --compile --target=browser: produce self-contained HTML with all assets inlined
// Check if all entrypoints are HTML files for standalone HTML mode
const has_all_html_entrypoints = brk: {
if (this_transpiler.options.entry_points.len == 0) break :brk false;
for (this_transpiler.options.entry_points) |entry_point| {
if (!strings.hasSuffixComptime(entry_point, ".html")) break :brk false;
}
break :brk true;
};
if (user_requested_browser_target and has_all_html_entrypoints) {
// --compile --target=browser with all HTML entrypoints: produce self-contained HTML
ctx.args.target = .browser;
if (ctx.bundler_options.code_splitting) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile --target browser with --splitting", .{});
Global.exit(1);
return;
}
// All entrypoints must be HTML files
for (this_transpiler.options.entry_points) |entry_point| {
if (!strings.hasSuffixComptime(entry_point, ".html")) {
Output.prettyErrorln("<r><red>error<r><d>:<r> --compile --target browser requires all entrypoints to be HTML files, but got: {s}", .{entry_point});
Global.exit(1);
return;
}
}
this_transpiler.options.compile_to_standalone_html = true;
// This is not a bun executable compile - clear compile flags
this_transpiler.options.compile = false;

View File

@@ -278,33 +278,37 @@ body { color: blue; }`,
expect(html).toContain("<style>");
});
test("fails with non-HTML entrypoints", async () => {
test("non-HTML entrypoints with compile+browser falls back to normal compile", async () => {
using dir = tempDir("compile-browser-no-html", {
"app.js": `console.log("no html");`,
});
expect(() =>
Bun.build({
entrypoints: [`${dir}/app.js`],
compile: true,
target: "browser",
}),
).toThrow();
// compile: true + target: "browser" with non-HTML entrypoints should
// fall back to normal bun executable compile (not standalone HTML)
const result = await Bun.build({
entrypoints: [`${dir}/app.js`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
});
test("fails with mixed HTML and non-HTML entrypoints", async () => {
using dir = tempDir("compile-browser-mixed", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
test("CLI --compile --target=browser with non-HTML falls back to normal compile", async () => {
using dir = tempDir("compile-browser-cli-no-html", {
"app.js": `console.log("test");`,
});
expect(() =>
Bun.build({
entrypoints: [`${dir}/index.html`, `${dir}/app.js`],
compile: true,
target: "browser",
}),
).toThrow();
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target=browser", `${dir}/app.js`],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Non-HTML entrypoints with --compile --target=browser should fall back to normal bun compile
expect(exitCode).toBe(0);
});
test("fails with splitting", async () => {