Compare commits

...

6 Commits

Author SHA1 Message Date
Claude Bot
fddb931601 fix: await promise in "returns a promise" test, use array overload
- "returns a promise" test now awaits the promise and checks exitCode
- "array form works" test uses the array overload (cmd as first arg)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 02:08:17 +00:00
Claude Bot
ead52a0285 fix: address review feedback for spawnAndWait
- buildBufferedResult now includes exitedDueToTimeout (true/false) when
  timeout was configured, and exitedDueToMaxBuffer (true/false) when
  maxBuffer was configured, matching spawnSync behavior
- Tests: replace POSIX-only commands (echo, true) with bunExe() + -e
- Tests: use tempDir() for cwd test instead of hardcoded /tmp
- Tests: use Buffer.alloc() pattern for large output test
- Tests: make timer test deterministic with Promise-based await
- Tests: add exitCode assertions to all tests

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 01:52:42 +00:00
Claude Bot
b913b1cd67 feat: add Bun.spawnAndWait() — async spawn with buffered result
Adds Bun.spawnAndWait(), which spawns a process asynchronously (like
Bun.spawn) but returns a Promise that resolves with the same result
shape as Bun.spawnSync() — buffered stdout/stderr as Buffers, exitCode,
success, signalCode, resourceUsage, and pid.

Unlike spawnSync, this does not block the event loop.
Unlike spawn, the result contains buffered output instead of streams.

Defaults stdout and stderr to "pipe" (same as spawnSync).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 01:09:16 +00:00
HK-SHAO
32a89c4334 fix(docs): code block syntax for server.tsx in SSR guide (#27417)
### What does this PR do?

TSX files may contain XML-like syntax, but TS files cannot.
2026-02-25 16:53:49 +00:00
robobun
c643e0fad8 fix(fuzzilli): prevent crash from fprintf on null FILE* in FUZZILLI_PRINT handler (#27310)
## Summary
- The `fuzzilli('FUZZILLI_PRINT', ...)` native function handler in
FuzzilliREPRL.cpp called `fdopen(103, "w")` on every invocation and
passed the result directly to `fprintf()` without a NULL check
- When running the debug-fuzz binary outside the REPRL harness (where fd
103 is not open), `fdopen()` returns NULL, and `fprintf(NULL, ...)`
causes a SIGSEGV crash
- Fix: make the `FILE*` static (so `fdopen` is called once, avoiding fd
leaks) and guard `fprintf`/`fflush` behind a NULL check

## Crash reproduction
```js
// The crash is triggered when the Fuzzilli explore framework calls
// fuzzilli('FUZZILLI_PRINT', ...) on the native fuzzilli() function
// while running outside the REPRL harness (fd 103 not open).
// Minimal reproduction:
fuzzilli('FUZZILLI_PRINT', 'hello');
```

Running the above with the debug-fuzz binary (which registers the native
`fuzzilli` function) causes SIGSEGV in `__vfprintf_internal` due to NULL
FILE*.

## Test plan
- [x] Verified crash reproduces 10/10 times with the original binary
- [x] Verified 0/10 crashes with the fixed binary
- [x] Fix is trivially correct: static FILE* + NULL guard

Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-25 12:52:12 +00:00
robobun
2222aa9f47 fix(bundler): write external sourcemap .map files for bun build --compile (#27396)
## Summary
- When using `bun build --compile --sourcemap=external`, the `.map`
files were embedded in the executable but never written to disk. This
fix writes them next to the compiled executable.
- With `--splitting` enabled, multiple chunks each produce their own
sourcemap. Previously all would overwrite a single `{outfile}.map`; now
each `.map` file preserves its chunk-specific name (e.g.,
`chunk-XXXXX.js.map`).
- Fixes both the JavaScript API (`Bun.build`) and CLI (`bun build`) code
paths.

## Test plan
- [x] `bun bd test test/bundler/bun-build-compile-sourcemap.test.ts` —
all 8 tests pass
- [x] New test: `compile with sourcemap: external writes .map file to
disk` — verifies a single `.map` file is written and contains valid
sourcemap JSON
- [x] New test: `compile without sourcemap does not write .map file` —
verifies no `.map` file appears without the flag
- [x] New test: `compile with splitting and external sourcemap writes
multiple .map files` — verifies each chunk gets its own uniquely-named
`.map` file on disk
- [x] Verified new tests fail with `USE_SYSTEM_BUN=1` (confirming they
test the fix, not pre-existing behavior)

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 22:02:51 -08:00
13 changed files with 651 additions and 16 deletions

View File

@@ -33,7 +33,7 @@ const stream = await renderToReadableStream(<Component message="Hello from serve
Combining this with `Bun.serve()`, we get a simple SSR HTTP server:
```tsx server.ts icon="/icons/typescript.svg"
```tsx server.tsx icon="/icons/typescript.svg"
Bun.serve({
async fetch() {
const stream = await renderToReadableStream(<Component message="Hello from server!" />);

View File

@@ -437,6 +437,39 @@ As a rule of thumb, the asynchronous `Bun.spawn` API is better for HTTP servers
---
## Async buffered API (`Bun.spawnAndWait()`)
`Bun.spawnAndWait` combines the best of both APIs: it returns a `Promise` that resolves with the same `SyncSubprocess` result as `Bun.spawnSync` (buffered `stdout`/`stderr` as `Buffer`, `exitCode`, `success`, etc.), but **without blocking the event loop**.
```ts
const result = await Bun.spawnAndWait(["echo", "hello"]);
console.log(result.stdout.toString()); // => "hello\n"
console.log(result.exitCode); // => 0
console.log(result.success); // => true
```
Like `Bun.spawnSync`, `stdout` and `stderr` default to `"pipe"` and are returned as `Buffer` objects. Like `Bun.spawn`, the event loop continues running while the process executes — timers, network requests, and other async work proceed normally.
```ts
const result = await Bun.spawnAndWait({
cmd: ["ls", "-la"],
cwd: "/tmp",
env: { ...process.env, MY_VAR: "hello" },
});
if (result.success) {
console.log(result.stdout.toString());
} else {
console.error(`Failed with exit code ${result.exitCode}`);
console.error(result.stderr.toString());
}
```
This is useful when you need the simplicity of `spawnSync`'s buffered result but can't afford to block the event loop — for example, in HTTP servers or when running multiple subprocesses concurrently with `Promise.all`.
---
## Benchmarks
<Note>
@@ -482,9 +515,11 @@ A reference of the Spawn API and types are shown below. The real types have comp
interface Bun {
spawn(command: string[], options?: SpawnOptions.OptionsObject): Subprocess;
spawnSync(command: string[], options?: SpawnOptions.OptionsObject): SyncSubprocess;
spawnAndWait(command: string[], options?: SpawnOptions.OptionsObject): Promise<SyncSubprocess>;
spawn(options: { cmd: string[] } & SpawnOptions.OptionsObject): Subprocess;
spawnSync(options: { cmd: string[] } & SpawnOptions.OptionsObject): SyncSubprocess;
spawnAndWait(options: { cmd: string[] } & SpawnOptions.OptionsObject): Promise<SyncSubprocess>;
}
namespace SpawnOptions {

View File

@@ -7237,6 +7237,54 @@ declare module "bun" {
options?: SpawnOptions.SpawnSyncOptions<In, Out, Err>,
): SyncSubprocess<Out, Err>;
/**
* Spawn a new process, returning a promise that resolves with the buffered
* stdout, stderr, exit code, and other information — the same shape as
* {@link Bun.spawnSync()}.
*
* Unlike `Bun.spawn()`, the result contains buffered `stdout` and `stderr`
* as `Buffer` objects instead of `ReadableStream`.
* Unlike `Bun.spawnSync()`, this does not block the event loop.
*
* @category Process Management
*
* ```js
* const { stdout, exitCode } = await Bun.spawnAndWait(["echo", "hello"]);
* console.log(stdout.toString()); // "hello\n"
* ```
*/
function spawnAndWait<
const In extends SpawnOptions.Writable = "ignore",
const Out extends SpawnOptions.Readable = "pipe",
const Err extends SpawnOptions.Readable = "pipe",
>(
options: SpawnOptions.SpawnOptions<In, Out, Err> & {
cmd: string[];
},
): Promise<SyncSubprocess<Out, Err>>;
/**
* Spawn a new process, returning a promise that resolves with the buffered
* stdout, stderr, exit code, and other information — the same shape as
* {@link Bun.spawnSync()}.
*
* Unlike `Bun.spawn()`, the result contains buffered `stdout` and `stderr`
* as `Buffer` objects instead of `ReadableStream`.
* Unlike `Bun.spawnSync()`, this does not block the event loop.
*
* @category Process Management
*
* ```js
* const { stdout, exitCode } = await Bun.spawnAndWait(["echo", "hello"]);
* console.log(stdout.toString()); // "hello\n"
* ```
*/
function spawnAndWait<
const In extends SpawnOptions.Writable = "ignore",
const Out extends SpawnOptions.Readable = "pipe",
const Err extends SpawnOptions.Readable = "pipe",
>(cmds: string[], options?: SpawnOptions.SpawnOptions<In, Out, Err>): Promise<SyncSubprocess<Out, Err>>;
/** Utility type for any process from {@link Bun.spawn()} with both stdout and stderr set to `"pipe"` */
type ReadableSubprocess = Subprocess<any, "pipe", "pipe">;
/** Utility type for any process from {@link Bun.spawn()} with stdin set to `"pipe"` */

View File

@@ -37,6 +37,7 @@ pub const BunObject = struct {
pub const stringWidth = toJSCallback(Bun.stringWidth);
pub const sleepSync = toJSCallback(Bun.sleepSync);
pub const spawn = toJSCallback(host_fn.wrapStaticMethod(api.Subprocess, "spawn", false));
pub const spawnAndWait = toJSCallback(host_fn.wrapStaticMethod(api.Subprocess, "spawnAndWait", false));
pub const spawnSync = toJSCallback(host_fn.wrapStaticMethod(api.Subprocess, "spawnSync", false));
pub const udpSocket = toJSCallback(host_fn.wrapStaticMethod(api.UDPSocket, "udpSocket", false));
pub const which = toJSCallback(Bun.which);
@@ -183,6 +184,7 @@ pub const BunObject = struct {
@export(&BunObject.stringWidth, .{ .name = callbackName("stringWidth") });
@export(&BunObject.sleepSync, .{ .name = callbackName("sleepSync") });
@export(&BunObject.spawn, .{ .name = callbackName("spawn") });
@export(&BunObject.spawnAndWait, .{ .name = callbackName("spawnAndWait") });
@export(&BunObject.spawnSync, .{ .name = callbackName("spawnSync") });
@export(&BunObject.udpSocket, .{ .name = callbackName("udpSocket") });
@export(&BunObject.which, .{ .name = callbackName("which") });

View File

@@ -94,12 +94,39 @@ fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd
/// Bun.spawn() calls this.
pub fn spawn(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue {
return spawnMaybeSync(globalThis, args, secondaryArgsValue, false);
return spawnMaybeSync(globalThis, args, secondaryArgsValue, false, false);
}
/// Bun.spawnSync() calls this.
pub fn spawnSync(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue {
return spawnMaybeSync(globalThis, args, secondaryArgsValue, true);
return spawnMaybeSync(globalThis, args, secondaryArgsValue, true, true);
}
/// Bun.spawnAndWait() calls this.
/// Like Bun.spawn() but returns a Promise that resolves with the same result
/// shape as Bun.spawnSync() (buffered stdout/stderr, exitCode, etc).
pub fn spawnAndWait(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue {
// Use the async spawn path but with stderr defaulting to pipe (like spawnSync)
const subprocess_js = try spawnMaybeSync(globalThis, args, secondaryArgsValue, false, true);
const subprocess = Subprocess.fromJSDirect(subprocess_js) orelse
return globalThis.throwInvalidArguments("failed to create subprocess", .{});
// Mark as buffered async mode
subprocess.flags.is_buffered_async = true;
// Create the promise that will resolve with the buffered result
const promise = jsc.JSPromise.create(globalThis);
subprocess.spawn_and_wait_promise.set(globalThis, promise.toJS());
// Take an extra ref to prevent deallocation before the promise resolves.
// This is balanced by deref() in maybeResolveBufferedAsync.
subprocess.ref();
// If the process already exited and stdio is already closed, resolve immediately
subprocess.maybeResolveBufferedAsync();
return promise.toJS();
}
pub fn spawnMaybeSync(
@@ -107,6 +134,7 @@ pub fn spawnMaybeSync(
args_: JSValue,
secondaryArgsValue: ?JSValue,
comptime is_sync: bool,
comptime default_stderr_to_pipe: bool,
) bun.JSError!JSValue {
if (comptime is_sync) {
// We skip this on Windows due to test failures.
@@ -134,7 +162,7 @@ pub fn spawnMaybeSync(
.{ .inherit = {} },
};
if (comptime is_sync) {
if (comptime is_sync or default_stderr_to_pipe) {
stdio[1] = .{ .pipe = {} };
stdio[2] = .{ .pipe = {} };
}

View File

@@ -51,6 +51,10 @@ stdout_maxbuf: ?*MaxBuf = null,
stderr_maxbuf: ?*MaxBuf = null,
exited_due_to_maxbuf: ?MaxBuf.Kind = null,
/// Promise for Bun.spawnAndWait() — resolves with SyncSubprocess-shaped result
/// when process exits AND all stdio pipes are closed.
spawn_and_wait_promise: jsc.Strong.Optional = .empty,
pub const Flags = packed struct(u8) {
is_sync: bool = false,
killed: bool = false,
@@ -58,7 +62,8 @@ pub const Flags = packed struct(u8) {
finalized: bool = false,
deref_on_stdin_destroyed: bool = false,
is_stdin_a_readable_stream: bool = false,
_: u2 = 0,
is_buffered_async: bool = false,
_: u1 = 0,
};
pub const SignalCode = bun.SignalCode;
@@ -147,6 +152,10 @@ pub fn hasExited(this: *const Subprocess) bool {
}
pub fn computeHasPendingActivity(this: *const Subprocess) bool {
if (this.spawn_and_wait_promise.has()) {
return true;
}
if (this.ipc_data != null) {
return true;
}
@@ -225,6 +234,88 @@ pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void {
}
},
}
if (this.flags.is_buffered_async) {
this.maybeResolveBufferedAsync();
}
}
/// Called from onProcessExit and onCloseIO when is_buffered_async is set.
/// Resolves the spawnAndWait promise once ALL conditions are met:
/// 1. The process has exited
/// 2. stdout is not an active pipe (has been closed/buffered)
/// 3. stderr is not an active pipe (has been closed/buffered)
pub fn maybeResolveBufferedAsync(this: *Subprocess) void {
// Condition 1: process must have exited
if (!this.process.hasExited()) return;
// Condition 2: stdout must not be an active pipe
if (this.stdout == .pipe) return;
// Condition 3: stderr must not be an active pipe
if (this.stderr == .pipe) return;
// All conditions met — resolve the promise
const promise_js = this.spawn_and_wait_promise.trySwap() orelse return;
const globalThis = this.globalThis;
const loop = globalThis.bunVM().eventLoop();
loop.enter();
defer loop.exit();
if (this.buildBufferedResult(globalThis)) |result| {
if (promise_js.asAnyPromise()) |promise| {
promise.resolve(globalThis, result) catch {};
}
} else |_| {
if (promise_js.asAnyPromise()) |promise| {
const err = if (globalThis.hasException())
globalThis.takeException(error.JSError)
else
JSValue.zero;
if (err != .zero) {
promise.reject(globalThis, err) catch {};
}
}
}
this.updateHasPendingActivity();
// Balance the ref() taken in spawnAndWait
this.deref();
}
/// Build a result object with the same shape as spawnSync's return value.
fn buildBufferedResult(this: *Subprocess, globalThis: *jsc.JSGlobalObject) bun.JSError!JSValue {
const signalCode = this.getSignalCode(globalThis);
const exitCode = this.getExitCode(globalThis);
const stdout = try this.stdout.toBufferedValue(globalThis);
const stderr = try this.stderr.toBufferedValue(globalThis);
const resource_usage: JSValue = if (!globalThis.hasException()) try this.createResourceUsageObject(globalThis) else .zero;
const resultPid = jsc.JSValue.jsNumberFromInt32(this.pid());
const sync_value = jsc.JSValue.createEmptyObject(globalThis, 0);
sync_value.put(globalThis, jsc.ZigString.static("exitCode"), exitCode);
if (!signalCode.isEmptyOrUndefinedOrNull()) {
sync_value.put(globalThis, jsc.ZigString.static("signalCode"), signalCode);
}
sync_value.put(globalThis, jsc.ZigString.static("stdout"), stdout);
sync_value.put(globalThis, jsc.ZigString.static("stderr"), stderr);
sync_value.put(globalThis, jsc.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0));
sync_value.put(globalThis, jsc.ZigString.static("resourceUsage"), resource_usage);
// Match spawnSync: include exitedDueToTimeout when a timeout was configured
if (this.event_loop_timer.next.ns() != 0 or this.event_loop_timer.state == .FIRED) {
sync_value.put(globalThis, jsc.ZigString.static("exitedDueToTimeout"), if (this.event_loop_timer.state == .FIRED) .true else .false);
}
// Match spawnSync: include exitedDueToMaxBuffer when maxBuffer was configured
if (this.stdout_maxbuf != null or this.stderr_maxbuf != null or this.exited_due_to_maxbuf != null) {
sync_value.put(globalThis, jsc.ZigString.static("exitedDueToMaxBuffer"), if (this.exited_due_to_maxbuf != null) .true else .false);
}
sync_value.put(globalThis, jsc.ZigString.static("pid"), resultPid);
return sync_value;
}
pub fn jsRef(this: *Subprocess) void {
@@ -699,6 +790,10 @@ pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Sta
);
}
}
if (this.flags.is_buffered_async) {
this.maybeResolveBufferedAsync();
}
}
}
@@ -772,6 +867,7 @@ pub fn finalize(this: *Subprocess) callconv(.c) void {
// access it after it's been freed We cannot call any methods which
// access GC'd values during the finalizer
this.this_value.finalize();
this.spawn_and_wait_promise.deinit();
this.clearAbortSignal();
@@ -917,6 +1013,7 @@ pub const Writable = @import("./subprocess/Writable.zig").Writable;
pub const MaxBuf = bun.io.MaxBuf;
pub const spawnSync = js_bun_spawn_bindings.spawnSync;
pub const spawn = js_bun_spawn_bindings.spawn;
pub const spawnAndWait = js_bun_spawn_bindings.spawnAndWait;
const IPC = @import("../../ipc.zig");
const Terminal = @import("./Terminal.zig");

View File

@@ -71,6 +71,7 @@
macro(shrink) \
macro(sleepSync) \
macro(spawn) \
macro(spawnAndWait) \
macro(spawnSync) \
macro(stringWidth) \
macro(udpSocket) \

View File

@@ -1017,6 +1017,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
sleep functionBunSleep DontDelete|Function 1
sleepSync BunObject_callback_sleepSync DontDelete|Function 1
spawn BunObject_callback_spawn DontDelete|Function 1
spawnAndWait BunObject_callback_spawnAndWait DontDelete|Function 1
spawnSync BunObject_callback_spawnSync DontDelete|Function 1
stderr BunObject_lazyPropCb_wrap_stderr DontDelete|PropertyCallback
stdin BunObject_lazyPropCb_wrap_stdin DontDelete|PropertyCallback

View File

@@ -144,9 +144,15 @@ static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES functionFuzzilli(JSC::JSGlob
WTF::String output = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
FILE* f = fdopen(REPRL_DWFD, "w");
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
// Use a static FILE* to avoid repeatedly calling fdopen (which
// duplicates the descriptor and leaks) and to gracefully handle
// the case where REPRL_DWFD is not open (i.e. running outside
// the fuzzer harness).
static FILE* f = fdopen(REPRL_DWFD, "w");
if (f) {
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
}
}
}

View File

@@ -2179,15 +2179,67 @@ pub const BundleV2 = struct {
output_file.is_executable = true;
}
// Write external sourcemap files next to the compiled executable and
// keep them in the output array. Destroy all other non-entry-point files.
// With --splitting, there can be multiple sourcemap files (one per chunk).
var kept: usize = 0;
for (output_files.items, 0..) |*current, i| {
if (i != entry_point_index) {
if (i == entry_point_index) {
output_files.items[kept] = current.*;
kept += 1;
} else if (result == .success and current.output_kind == .sourcemap and current.value == .buffer) {
const sourcemap_bytes = current.value.buffer.bytes;
if (sourcemap_bytes.len > 0) {
// Derive the .map filename from the sourcemap's own dest_path,
// placed in the same directory as the compiled executable.
const map_basename = if (current.dest_path.len > 0)
bun.path.basename(current.dest_path)
else
bun.path.basename(bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}.map", .{full_outfile_path})));
const sourcemap_full_path = if (dirname.len == 0 or strings.eqlComptime(dirname, "."))
bun.handleOom(bun.default_allocator.dupe(u8, map_basename))
else
bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}{c}{s}", .{ dirname, std.fs.path.sep, map_basename }));
// Write the sourcemap file to disk next to the executable
var pathbuf: bun.PathBuffer = undefined;
const write_path = if (Environment.isWindows) sourcemap_full_path else map_basename;
switch (bun.jsc.Node.fs.NodeFS.writeFileWithPathBuffer(
&pathbuf,
.{
.data = .{ .buffer = .{
.buffer = .{
.ptr = @constCast(sourcemap_bytes.ptr),
.len = @as(u32, @truncate(sourcemap_bytes.len)),
.byte_len = @as(u32, @truncate(sourcemap_bytes.len)),
},
} },
.encoding = .buffer,
.dirfd = .fromStdDir(root_dir),
.file = .{ .path = .{
.string = bun.PathString.init(write_path),
} },
},
)) {
.err => |err| {
bun.Output.err(err, "failed to write sourcemap file '{s}'", .{write_path});
current.deinit();
},
.result => {
current.dest_path = sourcemap_full_path;
output_files.items[kept] = current.*;
kept += 1;
},
}
} else {
current.deinit();
}
} else {
current.deinit();
}
}
const entry_point_output_file = output_files.swapRemove(entry_point_index);
output_files.items.len = 1;
output_files.items[0] = entry_point_output_file;
output_files.items.len = kept;
return result;
}

View File

@@ -546,6 +546,57 @@ pub const BuildCommand = struct {
Global.exit(1);
}
// Write external sourcemap files next to the compiled executable.
// With --splitting, there can be multiple .map files (one per chunk).
if (this_transpiler.options.source_map == .external) {
for (output_files) |f| {
if (f.output_kind == .sourcemap and f.value == .buffer) {
const sourcemap_bytes = f.value.buffer.bytes;
if (sourcemap_bytes.len == 0) continue;
// Use the sourcemap's own dest_path basename if available,
// otherwise fall back to {outfile}.map
const map_basename = if (f.dest_path.len > 0)
bun.path.basename(f.dest_path)
else brk: {
const exe_base = bun.path.basename(outfile);
break :brk if (compile_target.os == .windows and !strings.hasSuffixComptime(exe_base, ".exe"))
try std.fmt.allocPrint(allocator, "{s}.exe.map", .{exe_base})
else
try std.fmt.allocPrint(allocator, "{s}.map", .{exe_base});
};
// root_dir already points to the outfile's parent directory,
// so use map_basename (not a path with directory components)
// to avoid writing to a doubled directory path.
var pathbuf: bun.PathBuffer = undefined;
switch (bun.jsc.Node.fs.NodeFS.writeFileWithPathBuffer(
&pathbuf,
.{
.data = .{ .buffer = .{
.buffer = .{
.ptr = @constCast(sourcemap_bytes.ptr),
.len = @as(u32, @truncate(sourcemap_bytes.len)),
.byte_len = @as(u32, @truncate(sourcemap_bytes.len)),
},
} },
.encoding = .buffer,
.dirfd = .fromStdDir(root_dir),
.file = .{ .path = .{
.string = bun.PathString.init(map_basename),
} },
},
)) {
.err => |err| {
Output.err(err, "failed to write sourcemap file '{s}'", .{map_basename});
had_err = true;
},
.result => {},
}
}
}
}
const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms));
const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {
0...9 => 3,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, tempDir } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
describe("Bun.build compile with sourcemap", () => {
@@ -26,9 +26,9 @@ main();`,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const executablePath = result.outputs[0].path;
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// Run the compiled executable and capture the error
@@ -94,6 +94,167 @@ main();`,
expect(exitCode).not.toBe(0);
});
test("compile with sourcemap: external writes .map file to disk", async () => {
using dir = tempDir("build-compile-sourcemap-external-file", helperFiles);
const result = await Bun.build({
entrypoints: [join(String(dir), "app.js")],
compile: true,
sourcemap: "external",
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// The sourcemap output should appear in build result outputs
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBe(1);
// The .map file should exist next to the executable
const mapPath = sourcemapOutputs[0].path;
expect(mapPath).toEndWith(".map");
expect(await Bun.file(mapPath).exists()).toBe(true);
// Validate the sourcemap is valid JSON with expected fields
const mapContent = JSON.parse(await Bun.file(mapPath).text());
expect(mapContent.version).toBe(3);
expect(mapContent.sources).toBeArray();
expect(mapContent.sources.length).toBeGreaterThan(0);
expect(mapContent.mappings).toBeString();
});
test("compile without sourcemap does not write .map file", async () => {
using dir = tempDir("build-compile-no-sourcemap-file", {
"nosourcemap_entry.js": helperFiles["app.js"],
"helper.js": helperFiles["helper.js"],
});
const result = await Bun.build({
entrypoints: [join(String(dir), "nosourcemap_entry.js")],
compile: true,
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
// No .map file should exist next to the executable
expect(await Bun.file(`${executablePath}.map`).exists()).toBe(false);
// No sourcemap outputs should be in the result
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBe(0);
});
test("compile with splitting and external sourcemap writes multiple .map files", async () => {
using dir = tempDir("build-compile-sourcemap-splitting", {
"entry.js": `
const mod = await import("./lazy.js");
mod.greet();
`,
"lazy.js": `
export function greet() {
console.log("hello from lazy module");
}
`,
});
const result = await Bun.build({
entrypoints: [join(String(dir), "entry.js")],
compile: true,
splitting: true,
sourcemap: "external",
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// With splitting and a dynamic import, there should be at least 2 sourcemaps
// (one for the entry chunk, one for the lazy-loaded chunk)
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBeGreaterThanOrEqual(2);
// Each sourcemap should be a valid .map file on disk
const mapPaths = new Set<string>();
for (const sm of sourcemapOutputs) {
expect(sm.path).toEndWith(".map");
expect(await Bun.file(sm.path).exists()).toBe(true);
// Each map file should have a unique path (no overwrites)
expect(mapPaths.has(sm.path)).toBe(false);
mapPaths.add(sm.path);
// Validate the sourcemap is valid JSON
const mapContent = JSON.parse(await Bun.file(sm.path).text());
expect(mapContent.version).toBe(3);
expect(mapContent.mappings).toBeString();
}
// Run the compiled executable to ensure it works
await using proc = Bun.spawn({
cmd: [executablePath],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("hello from lazy module");
expect(exitCode).toBe(0);
});
test("compile with --outfile subdir/myapp writes .map next to executable", async () => {
using dir = tempDir("build-compile-sourcemap-outfile-subdir", helperFiles);
const subdirPath = join(String(dir), "subdir");
const exeSuffix = process.platform === "win32" ? ".exe" : "";
// Use CLI: bun build --compile --outfile subdir/myapp --sourcemap=external
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
join(subdirPath, "myapp"),
"--sourcemap=external",
],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// The executable should be at subdir/myapp (with .exe on Windows)
expect(await Bun.file(join(subdirPath, `myapp${exeSuffix}`)).exists()).toBe(true);
// The .map file should be in subdir/ (next to the executable)
const glob = new Bun.Glob("*.map");
const mapFiles = Array.from(glob.scanSync({ cwd: subdirPath }));
expect(mapFiles.length).toBe(1);
// Validate the sourcemap is valid JSON
const mapContent = JSON.parse(await Bun.file(join(subdirPath, mapFiles[0])).text());
expect(mapContent.version).toBe(3);
expect(mapContent.mappings).toBeString();
// Verify no .map was written into the doubled path subdir/subdir/
expect(await Bun.file(join(String(dir), "subdir", "subdir", "myapp.map")).exists()).toBe(false);
});
test("compile with multiple source files", async () => {
using dir = tempDir("build-compile-sourcemap-multiple-files", {
"utils.js": `export function utilError() {

View File

@@ -0,0 +1,153 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("basic echo", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.log('hello')"],
env: bunEnv,
});
expect(result.stdout.toString()).toBe("hello\n");
expect(result.exitCode).toBe(0);
expect(result.success).toBe(true);
expect(result.pid).toBeGreaterThan(0);
});
test("stderr is captured by default", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.error('err output')"],
env: bunEnv,
});
expect(result.stderr.toString()).toBe("err output\n");
expect(result.exitCode).toBe(0);
});
test("non-zero exit code", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "process.exit(42)"],
env: bunEnv,
});
expect(result.exitCode).toBe(42);
expect(result.success).toBe(false);
});
test("returns a promise that resolves", async () => {
const promise = Bun.spawnAndWait({
cmd: [bunExe(), "-e", "process.exit(0)"],
env: bunEnv,
});
expect(promise).toBeInstanceOf(Promise);
const result = await promise;
expect(result.exitCode).toBe(0);
});
test("does not block the event loop", async () => {
let timerFired = false;
const timerPromise = new Promise<void>(resolve => {
setTimeout(() => {
timerFired = true;
resolve();
}, 1);
});
// Sleep for 100ms in a child process - timer should fire during wait
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "await Bun.sleep(100)"],
env: bunEnv,
});
await timerPromise;
expect(result.exitCode).toBe(0);
expect(timerFired).toBe(true);
});
test("stdout and stderr are Buffers", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.log('out'); console.error('err')"],
env: bunEnv,
});
expect(Buffer.isBuffer(result.stdout)).toBe(true);
expect(Buffer.isBuffer(result.stderr)).toBe(true);
expect(result.stdout.toString()).toBe("out\n");
expect(result.stderr.toString()).toBe("err\n");
expect(result.exitCode).toBe(0);
});
test("resourceUsage is present", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", ""],
env: bunEnv,
});
expect(result.exitCode).toBe(0);
expect(result.resourceUsage).toBeDefined();
expect(typeof result.resourceUsage.maxRSS).toBe("number");
});
test("large output is buffered correctly", async () => {
const size = 1024 * 1024; // 1MB
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", `process.stdout.write(Buffer.alloc(${size}, 'x').toString())`],
env: bunEnv,
});
expect(result.stdout.length).toBe(size);
expect(result.exitCode).toBe(0);
});
test("signal code when killed", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "process.kill(process.pid, 'SIGTERM')"],
env: bunEnv,
});
// Process was killed by signal
expect(result.exitCode).not.toBe(0);
});
test("env option is forwarded", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.log(process.env.MY_TEST_VAR)"],
env: { ...bunEnv, MY_TEST_VAR: "hello_from_env" },
});
expect(result.stdout.toString().trim()).toBe("hello_from_env");
expect(result.exitCode).toBe(0);
});
test("cwd option is forwarded", async () => {
using dir = tempDir("spawnAndWait-cwd", {});
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.log(process.cwd())"],
env: bunEnv,
cwd: String(dir),
});
expect(result.stdout.toString().trim()).toBe(String(dir));
expect(result.exitCode).toBe(0);
});
test("invalid command throws", () => {
// spawnAndWait throws synchronously when the command is not found
expect(() => Bun.spawnAndWait(["this-command-does-not-exist-12345"])).toThrow();
});
test("array form works", async () => {
const result = await Bun.spawnAndWait([bunExe(), "-e", "console.log('array form')"], {
env: bunEnv,
});
expect(result.stdout.toString()).toBe("array form\n");
expect(result.exitCode).toBe(0);
});
test("object form with cmd works", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "console.log('object form')"],
env: bunEnv,
});
expect(result.stdout.toString()).toBe("object form\n");
expect(result.exitCode).toBe(0);
});
test("empty stdout", async () => {
const result = await Bun.spawnAndWait({
cmd: [bunExe(), "-e", "process.exit(0)"],
env: bunEnv,
stdout: "pipe",
});
expect(result.stdout.length).toBe(0);
expect(result.exitCode).toBe(0);
});