mirror of
https://github.com/oven-sh/bun
synced 2026-02-26 03:27:23 +01:00
Compare commits
6 Commits
claude/fix
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fddb931601 | ||
|
|
ead52a0285 | ||
|
|
b913b1cd67 | ||
|
|
32a89c4334 | ||
|
|
c643e0fad8 | ||
|
|
2222aa9f47 |
@@ -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!" />);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
48
packages/bun-types/bun.d.ts
vendored
48
packages/bun-types/bun.d.ts
vendored
@@ -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"` */
|
||||
|
||||
@@ -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") });
|
||||
|
||||
@@ -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 = {} };
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
macro(shrink) \
|
||||
macro(sleepSync) \
|
||||
macro(spawn) \
|
||||
macro(spawnAndWait) \
|
||||
macro(spawnSync) \
|
||||
macro(stringWidth) \
|
||||
macro(udpSocket) \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
153
test/js/bun/spawn/spawnAndWait.test.ts
Normal file
153
test/js/bun/spawn/spawnAndWait.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user