Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
f299efe096 wip 2025-07-21 21:41:32 +00:00
Claude Bot
899e327047 Fix error handling and add comprehensive tests for compile: true API
- Update StandaloneModuleGraph.zig to use proper Zig error handling instead of process exits
- Fix OutputFile creation in bundle_v2.zig with all required fields
- Add extensive test suite covering basic functionality, cross-compilation, error handling, and plugin integration
- Ensure BuildArtifact compatibility for compiled executables

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 09:40:27 +00:00
Claude Bot
9444d24104 Add compile: true support to Bun.build() JavaScript API
This adds support for creating standalone executables through the
JavaScript API, mirroring the functionality of `bun build --compile`.

Key features:
- `compile: true` option creates standalone executables
- Cross-compilation support with target strings like "bun-windows-x64"
- Proper error handling instead of process exits for JS API usage
- Integration with existing bundler workflow

Files modified:
- JSBundler.zig: Added compile option parsing and compile_target handling
- bundle_v2.zig: Integrated toExecutable into bundler workflow
- StandaloneModuleGraph.zig: Refactored error handling for JS API compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 08:28:31 +00:00
5 changed files with 562 additions and 43 deletions

View File

@@ -16,6 +16,27 @@ const macho = bun.macho;
const pe = bun.pe;
const w = std.os.windows;
const StandaloneError = error{
TempFileFailed,
CopyFailed,
OpenFailed,
ReadFailed,
WriteFailed,
SeekFailed,
MachoInitFailed,
MachoWriteFailed,
PEInitFailed,
PEWriteFailed,
DownloadFailed,
GetSelfExePathFailed,
MoveFailed,
DisableConsoleFailed,
OutOfMemory,
InvalidSourceMap,
FileNotFound,
@"Corrupted module graph: entry point ID is greater than module list count",
};
pub const StandaloneModuleGraph = struct {
bytes: []const u8 = "",
files: bun.StringArrayHashMap(File),
@@ -359,7 +380,9 @@ pub const StandaloneModuleGraph = struct {
var entry_point_id: ?usize = null;
var string_builder = bun.StringBuilder{};
var module_count: usize = 0;
for (output_files) |output_file| {
std.debug.print("[DEBUG] toBytes - processing {d} output files\n", .{output_files.len});
for (output_files, 0..) |output_file, i| {
std.debug.print("[DEBUG] toBytes - file {d}: dest_path={s}, output_kind={}, side={?}, value={}\n", .{ i, output_file.dest_path, output_file.output_kind, output_file.side, output_file.value });
string_builder.countZ(output_file.dest_path);
string_builder.countZ(prefix);
if (output_file.value == .buffer) {
@@ -374,10 +397,15 @@ pub const StandaloneModuleGraph = struct {
string_builder.cap += (output_file.value.buffer.bytes.len + 255) / 256 * 256 + 256;
} else {
if (entry_point_id == null) {
if (output_file.side == null or output_file.side.? == .server) {
std.debug.print("[DEBUG] toBytes - checking entry-point: side={?}, output_kind={}\n", .{ output_file.side, output_file.output_kind });
// For standalone executables, accept client-side entry points as well as server-side
if (output_file.side == null or output_file.side.? == .server or output_file.side.? == .client) {
if (output_file.output_kind == .@"entry-point") {
std.debug.print("[DEBUG] toBytes - setting entry_point_id = {d}\n", .{module_count});
entry_point_id = module_count;
}
} else {
std.debug.print("[DEBUG] toBytes - skipping entry-point due to side: {?}\n", .{output_file.side});
}
}
@@ -387,7 +415,11 @@ pub const StandaloneModuleGraph = struct {
}
}
if (module_count == 0 or entry_point_id == null) return &[_]u8{};
std.debug.print("[DEBUG] toBytes - module_count: {d}, entry_point_id: {?}\n", .{ module_count, entry_point_id });
if (module_count == 0 or entry_point_id == null) {
std.debug.print("[DEBUG] toBytes - returning empty array because module_count={d} or entry_point_id={?}\n", .{ module_count, entry_point_id });
return &[_]u8{};
}
string_builder.cap += @sizeOf(CompiledModuleGraphFile) * output_files.len;
string_builder.cap += trailer.len;
@@ -505,12 +537,9 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool = false,
};
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) bun.FileDescriptor {
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) anyerror!bun.FileDescriptor {
var buf: bun.PathBuffer = undefined;
var zname: [:0]const u8 = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))) catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get temporary file name: {s}", .{@errorName(err)});
Global.exit(1);
});
var zname: [:0]const u8 = bun.span(try bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))));
const cleanup = struct {
pub fn toClean(name: [:0]const u8, fd: bun.FileDescriptor) void {
@@ -537,10 +566,7 @@ pub const StandaloneModuleGraph = struct {
out_buf[zname.len] = 0;
const out = out_buf[0..zname.len :0];
bun.copyFile(in, out).unwrap() catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to copy bun executable into temporary file: {s}", .{@errorName(err)});
Global.exit(1);
};
try bun.copyFile(in, out).unwrap();
const file = bun.sys.openFileAtWindows(
bun.invalid_fd,
out,
@@ -549,10 +575,7 @@ pub const StandaloneModuleGraph = struct {
.disposition = w.FILE_OPEN,
.options = w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_REPARSE_POINT,
},
).unwrap() catch |e| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to open temporary file to copy bun into\n{}", .{e});
Global.exit(1);
};
).unwrap() catch |err| return err;
break :brk file;
}
@@ -604,8 +627,7 @@ pub const StandaloneModuleGraph = struct {
else => break,
}
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to open temporary file to copy bun into\n{}", .{err});
Global.exit(1);
return err.toZigErr();
}
},
}
@@ -625,9 +647,8 @@ pub const StandaloneModuleGraph = struct {
}
}
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to open bun executable to copy from as read-only\n{}", .{err});
cleanup(zname, fd);
Global.exit(1);
return err.toZigErr();
},
}
}
@@ -637,9 +658,8 @@ pub const StandaloneModuleGraph = struct {
defer self_fd.close();
bun.copyFile(self_fd, fd).unwrap() catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to copy bun executable into temporary file: {s}", .{@errorName(err)});
cleanup(zname, fd);
Global.exit(1);
return err;
};
break :brk fd;
};
@@ -843,26 +863,23 @@ pub const StandaloneModuleGraph = struct {
output_format: bun.options.Format,
windows_hide_console: bool,
windows_icon: ?[]const u8,
) !void {
) anyerror!void {
std.debug.print("[DEBUG] StandaloneModuleGraph.toExecutable entry - outfile: {s}\n", .{outfile});
const bytes = try toBytes(allocator, module_prefix, output_files, output_format);
std.debug.print("[DEBUG] toBytes returned {d} bytes\n", .{bytes.len});
if (bytes.len == 0) return;
const fd = inject(
const fd = try inject(
bytes,
if (target.isDefault())
bun.selfExePath() catch |err| {
Output.err(err, "failed to get self executable path", .{});
Global.exit(1);
}
try bun.selfExePath()
else
download(allocator, target, env) catch |err| {
Output.err(err, "failed to download cross-compiled bun executable", .{});
Global.exit(1);
},
try download(allocator, target, env),
.{ .windows_hide_console = windows_hide_console },
target,
);
bun.debugAssert(fd.kind == .system);
std.debug.print("[DEBUG] After inject, about to check Environment.isWindows: {}\n", .{Environment.isWindows});
if (Environment.isWindows) {
var outfile_buf: bun.OSPathBuffer = undefined;
@@ -883,7 +900,7 @@ pub const StandaloneModuleGraph = struct {
_ = bun.windows.deleteOpenedFile(fd);
Global.exit(1);
return err;
};
fd.close();
@@ -898,18 +915,28 @@ pub const StandaloneModuleGraph = struct {
}
var buf: bun.PathBuffer = undefined;
const temp_location = bun.getFdPath(fd, &buf) catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get path for fd: {s}", .{@errorName(err)});
Global.exit(1);
};
const temp_location = bun.getFdPath(fd, &buf) catch |err| return err;
const dest_basename = std.fs.path.basename(outfile);
std.debug.print("[DEBUG] toExecutable - temp_location: {s}\n", .{temp_location});
std.debug.print("[DEBUG] toExecutable - outfile: {s}\n", .{outfile});
std.debug.print("[DEBUG] toExecutable - dest_basename: {s}\n", .{dest_basename});
// Check the size of the temporary file before moving
if (std.fs.cwd().statFile(temp_location)) |temp_stat| {
std.debug.print("[DEBUG] toExecutable - temp file size: {d} bytes\n", .{temp_stat.size});
} else |err| {
std.debug.print("[DEBUG] toExecutable - failed to stat temp file: {}\n", .{err});
}
bun.sys.moveFileZWithHandle(
fd,
bun.FD.cwd(),
bun.sliceTo(&(try std.posix.toPosixPath(temp_location)), 0),
.fromStdDir(root_dir),
bun.sliceTo(&(try std.posix.toPosixPath(std.fs.path.basename(outfile))), 0),
bun.sliceTo(&(try std.posix.toPosixPath(dest_basename)), 0),
) catch |err| {
std.debug.print("[DEBUG] toExecutable - moveFileZWithHandle failed: {}\n", .{err});
if (err == error.IsDir or err == error.EISDIR) {
Output.prettyErrorln("<r><red>error<r><d>:<r> {} is a directory. Please choose a different --outfile or delete the directory", .{bun.fmt.quote(outfile)});
} else {
@@ -919,7 +946,7 @@ pub const StandaloneModuleGraph = struct {
&(try std.posix.toPosixPath(temp_location)),
);
Global.exit(1);
return StandaloneError.MoveFailed;
};
}

View File

@@ -19,6 +19,7 @@ const logger = bun.logger;
const Loader = options.Loader;
const Target = options.Target;
const Index = @import("../../ast/base.zig").Index;
const CompileTarget = @import("../../compile_target.zig");
const debug = bun.Output.scoped(.Transpiler, false);
@@ -59,6 +60,8 @@ pub const JSBundler = struct {
env_behavior: Api.DotEnvBehavior = .disable,
env_prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator),
tsconfig_override: OwnedString = OwnedString.initEmpty(bun.default_allocator),
compile: bool = false,
compile_target: ?*CompileTarget = null,
pub const List = bun.StringArrayHashMapUnmanaged(Config);
@@ -80,7 +83,24 @@ pub const JSBundler = struct {
errdefer if (plugins.*) |plugin| plugin.deinit();
var did_set_target = false;
if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| {
if (try config.getOptional(globalThis, "target", ZigString.Slice)) |target_slice| {
defer target_slice.deinit();
const target_str = target_slice.slice();
if (bun.strings.hasPrefixComptime(target_str, "bun-")) {
this.compile_target = try allocator.create(CompileTarget);
this.compile_target.?.* = CompileTarget.from(target_str);
// Set the build target based on the compile target's OS
this.target = .bun;
did_set_target = true;
} else {
// Try to parse as enum target first
if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| {
this.target = target;
did_set_target = true;
}
}
} else if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| {
this.target = target;
did_set_target = true;
}
@@ -286,6 +306,10 @@ pub const JSBundler = struct {
this.ignore_dce_annotations = flag;
}
if (try config.getBooleanLoose(globalThis, "compile")) |compile_flag| {
this.compile = compile_flag;
}
if (try config.getTruthy(globalThis, "conditions")) |conditions_value| {
if (conditions_value.isString()) {
var slice = try conditions_value.toSliceOrNull(globalThis);
@@ -531,6 +555,9 @@ pub const JSBundler = struct {
self.env_prefix.deinit();
self.footer.deinit();
self.tsconfig_override.deinit();
if (self.compile_target) |target| {
allocator.destroy(target);
}
}
};

View File

@@ -1604,8 +1604,13 @@ pub const BundleV2 = struct {
.plugins = plugins,
.log = Logger.Log.init(bun.default_allocator),
.task = undefined,
.compile_target = config.compile_target,
});
completion.task = JSBundleCompletionTask.TaskCompletion.init(completion);
// Important: Null out the pointer in the config so it's not double-freed
var mutable_config = &completion.config;
mutable_config.compile_target = null;
if (plugins) |plugin| {
plugin.setConfig(completion);
@@ -1685,6 +1690,7 @@ pub const BundleV2 = struct {
transpiler: *BundleV2 = undefined,
plugins: ?*bun.JSC.API.JSBundler.Plugin = null,
started_at_ns: u64 = 0,
compile_target: ?*CompileTarget = null,
pub fn configureBundler(
completion: *JSBundleCompletionTask,
@@ -1748,6 +1754,7 @@ pub const BundleV2 = struct {
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.banner = config.banner.slice();
transpiler.options.footer = config.footer.slice();
transpiler.options.compile = config.compile;
transpiler.configureLinker();
try transpiler.configureDefines();
@@ -2283,7 +2290,128 @@ pub const BundleV2 = struct {
return error.BuildFailed;
}
return try this.linker.generateChunksInParallel(chunks, false);
const output_files_list = try this.linker.generateChunksInParallel(chunks, false);
// Handle compile: true option
if (this.transpiler.options.compile) {
std.debug.print("[DEBUG] Entering compile mode\n", .{});
// Extract the compile target from the completion task
const compile_target_from_js = if (this.completion) |completion| completion.compile_target else null;
var default_compile_target: CompileTarget = .{};
const target = compile_target_from_js orelse &default_compile_target;
// Determine output file name - use proper basename logic
const entry_point = this.transpiler.options.entry_points[0];
const basename = std.fs.path.basename(entry_point);
const name_without_ext = if (std.mem.lastIndexOfScalar(u8, basename, '.')) |dot_index|
basename[0..dot_index]
else
basename;
// Create output path in the specified output directory
var outfile_buf: bun.PathBuffer = undefined;
const outfile = if (this.transpiler.options.output_dir.len > 0) blk: {
break :blk std.fmt.bufPrint(&outfile_buf, "{s}{c}{s}", .{
this.transpiler.options.output_dir,
std.fs.path.sep,
name_without_ext,
}) catch "a.out";
} else name_without_ext;
std.debug.print("[DEBUG] Output file path: {s}\n", .{outfile});
// For compile mode, we need to load saved files into buffers for embedding
var output_files_for_executable = std.ArrayList(options.OutputFile).init(this.graph.allocator);
defer output_files_for_executable.deinit();
for (output_files_list.items) |*output_file| {
if (output_file.value == .saved) {
// Read the saved file content into a buffer
// Check if dest_path is absolute or relative to output_dir
const file_path = if (std.fs.path.isAbsolute(output_file.dest_path))
output_file.dest_path
else if (this.transpiler.options.output_dir.len > 0) blk: {
var path_buf: bun.PathBuffer = undefined;
break :blk std.fmt.bufPrint(&path_buf, "{s}{c}{s}", .{
this.transpiler.options.output_dir,
std.fs.path.sep,
output_file.dest_path,
}) catch output_file.dest_path;
} else output_file.dest_path;
std.debug.print("[DEBUG] Attempting to read file: {s}\n", .{file_path});
const file_content = std.fs.cwd().readFileAlloc(this.graph.allocator, file_path, 16 * 1024 * 1024) catch |err| {
std.debug.print("[DEBUG] Failed to read saved file {s}: {}\n", .{ file_path, err });
continue;
};
// Create a new output file with buffer content
var new_output_file = output_file.*;
new_output_file.value = .{ .buffer = .{ .allocator = this.graph.allocator, .bytes = file_content } };
try output_files_for_executable.append(new_output_file);
std.debug.print("[DEBUG] Loaded saved file into buffer: {s} ({d} bytes)\n", .{ file_path, file_content.len });
} else {
try output_files_for_executable.append(output_file.*);
}
}
// Generate standalone executable from bundled output
std.debug.print("[DEBUG] Calling toExecutable with {} output files\n", .{output_files_for_executable.items.len});
// For toExecutable to work correctly, we need to pass the directory and filename separately
var output_dir_owned: ?std.fs.Dir = null;
const output_dir = if (std.fs.path.dirname(outfile)) |dirname| blk: {
const dir = std.fs.cwd().openDir(dirname, .{}) catch |err| {
std.debug.print("[DEBUG] Failed to open output directory {s}: {}\n", .{ dirname, err });
return err;
};
output_dir_owned = dir;
break :blk dir;
} else std.fs.cwd();
defer if (output_dir_owned) |*dir| dir.close();
const output_filename = std.fs.path.basename(outfile);
std.debug.print("[DEBUG] Output directory: {s}, filename: {s}\n", .{
if (std.fs.path.dirname(outfile)) |dir| dir else ".",
output_filename
});
bun.StandaloneModuleGraph.toExecutable(
target,
this.graph.allocator,
output_files_for_executable.items,
output_dir,
"", // module_prefix
output_filename,
this.transpiler.env,
this.transpiler.options.output_format,
false, // windows_hide_console, TODO: expose this option
null, // windows_icon, TODO: expose this option
) catch |err| {
std.debug.print("[DEBUG] toExecutable failed with error: {}\n", .{err});
this.transpiler.log.addError(null, .{}, @errorName(err)) catch {};
return err;
};
std.debug.print("[DEBUG] toExecutable completed successfully\n", .{});
// Debug: Check if the executable was actually created
if (std.fs.cwd().access(outfile, .{})) {
std.debug.print("[DEBUG] Executable file created successfully at: {s}\n", .{outfile});
} else |err| {
std.debug.print("[DEBUG] Executable file NOT found after toExecutable: {} at path: {s}\n", .{ err, outfile });
}
// Clean up the intermediate output files since we've created an executable
for (output_files_list.items) |*file| {
file.deinit();
}
output_files_list.deinit();
// Return empty output list for compile mode (executable was written to disk)
return std.ArrayList(options.OutputFile).init(this.graph.allocator);
}
return output_files_list;
}
fn shouldAddWatcherPlugin(bv2: *BundleV2, namespace: []const u8, path: []const u8) bool {
@@ -4154,6 +4282,7 @@ pub const URL = @import("../url.zig").URL;
pub const Resolver = _resolver.Resolver;
pub const TOML = @import("../toml/toml_parser.zig").TOML;
pub const Dependency = js_ast.Dependency;
pub const CompileTarget = @import("../compile_target.zig");
pub const JSAst = js_ast.BundledAst;
pub const Loader = options.Loader;
pub const Index = @import("../ast/base.zig").Index;

View File

@@ -4942,6 +4942,7 @@ pub fn moveFileZWithHandle(from_handle: bun.FileDescriptor, from_dir: bun.FileDe
if (err.getErrno() == .XDEV) {
try copyFileZSlowWithHandle(from_handle, to_dir, destination).unwrap();
_ = unlinkat(from_dir, filename);
return;
}
return bun.errnoToZigErr(err.errno);
@@ -5008,9 +5009,34 @@ pub fn copyFileZSlowWithHandle(in_handle: bun.FileDescriptor, to_dir: bun.FileDe
_ = std.os.linux.fallocate(out_handle.cast(), 0, 0, @intCast(stat_.size));
}
// Seek to the beginning of the input file
switch (lseek(in_handle, 0, std.posix.SEEK.SET)) {
.result => |pos| {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - seeked to position: {d}\n", .{pos});
},
.err => |err| {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - failed to seek input file: {}\n", .{err});
},
}
std.debug.print("[DEBUG] copyFileZSlowWithHandle - copying from fd {d} to fd {d}, source size: {d}\n", .{ in_handle.cast(), out_handle.cast(), stat_.size });
switch (bun.copyFile(in_handle, out_handle)) {
.err => |e| return .{ .err = e },
.result => {},
.err => |e| {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - copyFile failed: {}\n", .{e});
return .{ .err = e };
},
.result => {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - copyFile succeeded\n", .{});
// Check the size of the output file after copying
switch (fstat(out_handle)) {
.result => |out_stat| {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - output file size after copy: {d}\n", .{out_stat.size});
},
.err => |err| {
std.debug.print("[DEBUG] copyFileZSlowWithHandle - failed to stat output file: {}\n", .{err});
},
}
},
}
if (comptime Environment.isPosix) {

View File

@@ -957,3 +957,313 @@ export { greeting };`,
}
});
});
describe("Bun.build compile: true", () => {
test("basic compile functionality", async () => {
const dir = tempDirWithFiles("bun-build-compile-basic", {
"index.ts": `
console.log("Hello from compiled executable!");
process.exit(0);
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0); // Compile returns empty outputs
expect(build.logs).toHaveLength(0);
// Check that an executable was created
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
// Test running the executable
const { exitCode, stdout } = Bun.spawnSync({
cmd: [executablePath],
cwd: dir,
stdout: "pipe",
});
expect(exitCode).toBe(0);
expect(stdout.toString().trim()).toBe("Hello from compiled executable!");
});
test("compile with cross-compilation target", async () => {
const dir = tempDirWithFiles("bun-build-compile-target", {
"index.ts": `
console.log("Cross-compiled executable!");
console.log("Platform:", process.platform);
console.log("Arch:", process.arch);
`,
});
// Test cross-compilation to a different target
const targetPlatform = process.platform === "linux" ? "darwin" : "linux";
const targetArch = "x64";
const targetString = `bun-${targetPlatform}-${targetArch}`;
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
target: targetString,
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
// Check that an executable was created with appropriate extension
const executableName = targetPlatform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
});
test("compile with outfile option", async () => {
const dir = tempDirWithFiles("bun-build-compile-outfile", {
"app.ts": `
console.log("Custom executable name!");
`,
});
const customName = process.platform === "win32" ? "myapp.exe" : "myapp";
const build = await Bun.build({
entrypoints: [join(dir, "app.ts")],
compile: true,
outfile: join(dir, customName),
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
// Check that the custom-named executable was created
const executablePath = join(dir, customName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
});
test("compile with bundling optimizations", async () => {
const dir = tempDirWithFiles("bun-build-compile-optimized", {
"index.ts": `
import { helper } from "./helper";
console.log(helper("World"));
`,
"helper.ts": `
export function helper(name: string): string {
return \`Hello, \${name}!\`;
}
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
minify: true,
target: "bun",
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
// Test that the executable runs correctly with bundled code
const { exitCode, stdout } = Bun.spawnSync({
cmd: [executablePath],
cwd: dir,
stdout: "pipe",
});
expect(exitCode).toBe(0);
expect(stdout.toString().trim()).toBe("Hello, World!");
});
test("compile error handling for missing files", async () => {
const dir = tempDirWithFiles("bun-build-compile-error", {});
try {
await Bun.build({
entrypoints: [join(dir, "nonexistent.ts")],
compile: true,
outdir: dir,
});
expect.unreachable("Should have thrown an error");
} catch (error) {
expect(error).toBeInstanceOf(AggregateError);
expect(error.errors).toHaveLength(1);
expect(error.errors[0].message).toMatch(/ModuleNotFound/);
}
});
test("compile with external dependencies", async () => {
const dir = tempDirWithFiles("bun-build-compile-external", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"lodash": "^4.0.0"
}
}),
"index.ts": `
console.log("Testing external dependencies");
// Only log a message to avoid requiring actual lodash installation
console.log("App started successfully");
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
target: "bun",
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
const { exitCode, stdout } = Bun.spawnSync({
cmd: [executablePath],
cwd: dir,
stdout: "pipe",
});
expect(exitCode).toBe(0);
expect(stdout.toString()).toContain("App started successfully");
});
test("compile with TypeScript configuration", async () => {
const dir = tempDirWithFiles("bun-build-compile-typescript", {
"tsconfig.json": JSON.stringify({
compilerOptions: {
target: "ES2020",
module: "ESNext",
strict: true,
paths: {
"@/*": ["./src/*"]
}
}
}),
"src/utils.ts": `
export const getMessage = (): string => {
return "TypeScript compiled successfully!";
};
`,
"index.ts": `
import { getMessage } from "@/utils";
console.log(getMessage());
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
tsconfig: join(dir, "tsconfig.json"),
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
const { exitCode, stdout } = Bun.spawnSync({
cmd: [executablePath],
cwd: dir,
stdout: "pipe",
});
expect(exitCode).toBe(0);
expect(stdout.toString().trim()).toBe("TypeScript compiled successfully!");
});
test("compile: false should not create executable", async () => {
const dir = tempDirWithFiles("bun-build-no-compile", {
"index.ts": `
console.log("Not compiled");
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: false,
outdir: dir,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(1); // Should have normal JS output
expect(build.outputs[0].kind).toBe("entry-point");
// Check that no executable was created
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(() => Bun.file(executablePath).size).toThrow(); // File should not exist
});
test("compile with invalid target throws error", async () => {
const dir = tempDirWithFiles("bun-build-compile-invalid-target", {
"index.ts": `console.log("test");`,
});
expect(() => {
Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
target: "invalid-target-name",
outdir: dir,
});
}).toThrow();
});
test("compile works with plugins", async () => {
const dir = tempDirWithFiles("bun-build-compile-plugins", {
"index.ts": `
import text from "./data.txt";
console.log("Loaded text:", text);
`,
"data.txt": "Hello from text file!",
});
const build = await Bun.build({
entrypoints: [join(dir, "index.ts")],
compile: true,
outdir: dir,
plugins: [
{
name: "text-loader",
setup(build) {
build.onLoad({ filter: /\.txt$/ }, async (args) => {
const text = await Bun.file(args.path).text();
return {
contents: `export default ${JSON.stringify(text.trim())};`,
loader: "js",
};
});
},
},
],
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(0);
const executableName = process.platform === "win32" ? "index.exe" : "index";
const executablePath = join(dir, executableName);
expect(Bun.file(executablePath).size).toBeGreaterThan(0);
const { exitCode, stdout } = Bun.spawnSync({
cmd: [executablePath],
cwd: dir,
stdout: "pipe",
});
expect(exitCode).toBe(0);
expect(stdout.toString()).toContain("Hello from text file!");
});
});