From b7d4b14b3d17eeeb6bc40348c618cdc270f21b71 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 18 Jun 2025 02:28:54 -0700 Subject: [PATCH 001/147] Fix BUN-D93 (#20468) --- src/bun.zig | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/bun.zig b/src/bun.zig index 5715dd301a..72e9fcf79b 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3708,24 +3708,16 @@ const StackOverflow = error{StackOverflow}; // We keep up to 4 path buffers alive per thread at a time. pub fn PathBufferPoolT(comptime T: type) type { return struct { - const Pool = ObjectPool(PathBuf, null, true, 4); - pub const PathBuf = struct { - bytes: T, - - pub fn deinit(this: *PathBuf) void { - var node: *Pool.Node = @alignCast(@fieldParentPtr("data", this)); - node.release(); - } - }; + const Pool = ObjectPool(T, null, true, 4); pub fn get() *T { // use a threadlocal allocator so mimalloc deletes it on thread deinit. - return &Pool.get(bun.threadlocalAllocator()).data.bytes; + return &Pool.get(bun.threadlocalAllocator()).data; } pub fn put(buffer: *T) void { - var path_buf: *PathBuf = @alignCast(@fieldParentPtr("bytes", buffer)); - path_buf.deinit(); + var node: *Pool.Node = @alignCast(@fieldParentPtr("data", buffer)); + node.release(); } pub fn deleteAll() void { From b9e72d0d2e894d6c60acb3b96dc69df473f9d643 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 19 Jun 2025 05:00:16 +1000 Subject: [PATCH 002/147] Make `bunx` work with `npm i -g bun` on windows (#20471) --- packages/bun-release/scripts/upload-npm.ts | 3 ++- packages/bun-release/src/fs.ts | 12 ++++++++++++ packages/bun-release/src/npm/install.ts | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index 9cd35783a8..063fd3e37a 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -72,6 +72,7 @@ async function buildRootModule(dryRun?: boolean) { }, }); write(join(cwd, "bin", "bun.exe"), ""); + write(join(cwd, "bin", "bunx.exe"), ""); write( join(cwd, "bin", "README.txt"), `The 'bun.exe' file is a placeholder for the binary file, which @@ -105,7 +106,7 @@ without *requiring* a postinstall script. ), bin: { bun: "bin/bun.exe", - bunx: "bin/bun.exe", + bunx: "bin/bunx.exe", }, os, cpu, diff --git a/packages/bun-release/src/fs.ts b/packages/bun-release/src/fs.ts index 66481b5b24..d70ccdd84f 100644 --- a/packages/bun-release/src/fs.ts +++ b/packages/bun-release/src/fs.ts @@ -157,3 +157,15 @@ export function exists(path: string): boolean { } return false; } + +export function link(path: string, newPath: string): void { + debug("link", path, newPath); + try { + fs.unlinkSync(newPath); + fs.linkSync(path, newPath); + return; + } catch (error) { + copy(path, newPath); + debug("fs.linkSync failed, reverting to copy", error); + } +} diff --git a/packages/bun-release/src/npm/install.ts b/packages/bun-release/src/npm/install.ts index f6e7d48437..165957c18e 100644 --- a/packages/bun-release/src/npm/install.ts +++ b/packages/bun-release/src/npm/install.ts @@ -1,7 +1,7 @@ import { unzipSync } from "zlib"; import { debug, error } from "../console"; import { fetch } from "../fetch"; -import { chmod, join, rename, rm, tmp, write } from "../fs"; +import { chmod, join, link, rename, rm, tmp, write } from "../fs"; import type { Platform } from "../platform"; import { abi, arch, os, supportedPlatforms } from "../platform"; import { spawn } from "../spawn"; @@ -125,6 +125,7 @@ export function optimizeBun(path: string): void { os === "win32" ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : "curl -fsSL https://bun.sh/install | bash"; try { rename(path, join(__dirname, "bin", "bun.exe")); + link(join(__dirname, "bin", "bun.exe"), join(__dirname, "bin", "bunx.exe")); return; } catch (error) { debug("optimizeBun failed", error); From 9811a2b53edcf201f9e9ca66b9fd650fa1bab624 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 19 Jun 2025 05:01:30 +1000 Subject: [PATCH 003/147] docs: minor fix to Bun.deflateSync (#20466) --- docs/api/utils.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/utils.md b/docs/api/utils.md index 979e406851..2d04163c72 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -582,11 +582,11 @@ Compresses a `Uint8Array` using zlib's DEFLATE algorithm. const buf = Buffer.from("hello".repeat(100)); const compressed = Bun.deflateSync(buf); -buf; // => Uint8Array(25) -compressed; // => Uint8Array(10) +buf; // => Buffer(500) +compressed; // => Uint8Array(12) ``` -The second argument supports the same set of configuration options as [`Bun.gzipSync`](#bungzipsync). +The second argument supports the same set of configuration options as [`Bun.gzipSync`](#bun-gzipsync). ## `Bun.inflateSync()` From aa37ecb7a506d107fe923e131d048dc196ec4bfd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 18 Jun 2025 12:01:40 -0700 Subject: [PATCH 004/147] Split up Timer.zig into more files (#20465) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- cmake/sources/ZigSources.txt | 4 + src/bun.js/api/Timer.zig | 928 +----------------- src/bun.js/api/Timer/EventLoopTimer.zig | 247 +++++ src/bun.js/api/Timer/ImmediateObject.zig | 104 ++ src/bun.js/api/Timer/TimeoutObject.zig | 134 +++ src/bun.js/api/Timer/TimerObjectInternals.zig | 487 +++++++++ 6 files changed, 980 insertions(+), 924 deletions(-) create mode 100644 src/bun.js/api/Timer/EventLoopTimer.zig create mode 100644 src/bun.js/api/Timer/ImmediateObject.zig create mode 100644 src/bun.js/api/Timer/TimeoutObject.zig create mode 100644 src/bun.js/api/Timer/TimerObjectInternals.zig diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 7fc059629f..7ffe24fafb 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -73,6 +73,10 @@ src/bun.js/api/server/StaticRoute.zig src/bun.js/api/server/WebSocketServerContext.zig src/bun.js/api/streams.classes.zig src/bun.js/api/Timer.zig +src/bun.js/api/Timer/EventLoopTimer.zig +src/bun.js/api/Timer/ImmediateObject.zig +src/bun.js/api/Timer/TimeoutObject.zig +src/bun.js/api/Timer/TimerObjectInternals.zig src/bun.js/api/TOMLObject.zig src/bun.js/api/UnsafeObject.zig src/bun.js/bindgen_test.zig diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index 7e21663a13..df132e86a9 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -5,13 +5,9 @@ const VirtualMachine = JSC.VirtualMachine; const JSValue = JSC.JSValue; const JSError = bun.JSError; const JSGlobalObject = JSC.JSGlobalObject; -const Debugger = JSC.Debugger; const Environment = bun.Environment; const uv = bun.windows.libuv; -const api = bun.api; -const StatWatcherScheduler = @import("../node/node_fs_stat_watcher.zig").StatWatcherScheduler; const Timer = @This(); -const DNSResolver = @import("./bun/dns_resolver.zig").DNSResolver; /// TimeoutMap is map of i32 to nullable Timeout structs /// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc. @@ -548,698 +544,11 @@ pub const All = struct { } }; -const uws = bun.uws; +pub const EventLoopTimer = @import("./Timer/EventLoopTimer.zig"); -pub const TimeoutObject = struct { - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; - - pub const js = JSC.Codegen.JSTimeout; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - ref_count: RefCount, - event_loop_timer: EventLoopTimer = .{ - .next = .{}, - .tag = .TimeoutObject, - }, - internals: TimerObjectInternals, - - pub fn init( - globalThis: *JSGlobalObject, - id: i32, - kind: Kind, - interval: u31, - callback: JSValue, - arguments: JSValue, - ) JSValue { - // internals are initialized by init() - const timeout = bun.new(TimeoutObject, .{ .ref_count = .init(), .internals = undefined }); - const js_value = timeout.toJS(globalThis); - defer js_value.ensureStillAlive(); - timeout.internals.init( - js_value, - globalThis, - id, - kind, - interval, - callback, - arguments, - ); - - if (globalThis.bunVM().isInspectorEnabled()) { - Debugger.didScheduleAsyncCall( - globalThis, - .DOMTimer, - ID.asyncID(.{ .id = id, .kind = kind.big() }), - kind != .setInterval, - ); - } - - return js_value; - } - - fn deinit(this: *TimeoutObject) void { - this.internals.deinit(); - bun.destroy(this); - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*TimeoutObject { - _ = callFrame; - return globalObject.throw("Timeout is not constructible", .{}); - } - - pub fn toPrimitive(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.toPrimitive(); - } - - pub fn doRef(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRef(globalThis, callFrame.this()); - } - - pub fn doUnref(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doUnref(globalThis, callFrame.this()); - } - - pub fn doRefresh(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRefresh(globalThis, callFrame.this()); - } - - pub fn hasRef(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.hasRef(); - } - - pub fn finalize(this: *TimeoutObject) void { - this.internals.finalize(); - } - - pub fn getDestroyed(this: *TimeoutObject, globalThis: *JSGlobalObject) JSValue { - _ = globalThis; - return .jsBoolean(this.internals.getDestroyed()); - } - - pub fn close(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) JSValue { - this.internals.cancel(globalThis.bunVM()); - return callFrame.this(); - } - - pub fn get_onTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.callbackGetCached(thisValue).?; - } - - pub fn set_onTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.callbackSetCached(thisValue, globalThis, value); - } - - pub fn get_idleTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.idleTimeoutGetCached(thisValue).?; - } - - pub fn set_idleTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.idleTimeoutSetCached(thisValue, globalThis, value); - } - - pub fn get_repeat(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.repeatGetCached(thisValue).?; - } - - pub fn set_repeat(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.repeatSetCached(thisValue, globalThis, value); - } - - pub fn dispose(this: *TimeoutObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.internals.cancel(globalThis.bunVM()); - return .js_undefined; - } -}; - -pub const ImmediateObject = struct { - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; - - pub const js = JSC.Codegen.JSImmediate; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - ref_count: RefCount, - event_loop_timer: EventLoopTimer = .{ - .next = .{}, - .tag = .ImmediateObject, - }, - internals: TimerObjectInternals, - - pub fn init( - globalThis: *JSGlobalObject, - id: i32, - callback: JSValue, - arguments: JSValue, - ) JSValue { - // internals are initialized by init() - const immediate = bun.new(ImmediateObject, .{ .ref_count = .init(), .internals = undefined }); - const js_value = immediate.toJS(globalThis); - defer js_value.ensureStillAlive(); - immediate.internals.init( - js_value, - globalThis, - id, - .setImmediate, - 0, - callback, - arguments, - ); - - if (globalThis.bunVM().isInspectorEnabled()) { - Debugger.didScheduleAsyncCall( - globalThis, - .DOMTimer, - ID.asyncID(.{ .id = id, .kind = .setImmediate }), - true, - ); - } - - return js_value; - } - - fn deinit(this: *ImmediateObject) void { - this.internals.deinit(); - bun.destroy(this); - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*ImmediateObject { - _ = callFrame; - return globalObject.throw("Immediate is not constructible", .{}); - } - - /// returns true if an exception was thrown - pub fn runImmediateTask(this: *ImmediateObject, vm: *VirtualMachine) bool { - return this.internals.runImmediateTask(vm); - } - - pub fn toPrimitive(this: *ImmediateObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.toPrimitive(); - } - - pub fn doRef(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRef(globalThis, callFrame.this()); - } - - pub fn doUnref(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doUnref(globalThis, callFrame.this()); - } - - pub fn hasRef(this: *ImmediateObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.hasRef(); - } - - pub fn finalize(this: *ImmediateObject) void { - this.internals.finalize(); - } - - pub fn getDestroyed(this: *ImmediateObject, globalThis: *JSGlobalObject) JSValue { - _ = globalThis; - return .jsBoolean(this.internals.getDestroyed()); - } - - pub fn dispose(this: *ImmediateObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.internals.cancel(globalThis.bunVM()); - return .js_undefined; - } -}; - -/// Data that TimerObject and ImmediateObject have in common -pub const TimerObjectInternals = struct { - /// Identifier for this timer that is exposed to JavaScript (by `+timer`) - id: i32 = -1, - interval: u31 = 0, - strong_this: JSC.Strong.Optional = .empty, - flags: Flags = .{}, - - const Flags = packed struct(u32) { - /// Whenever a timer is inserted into the heap (which happen on creation or refresh), the global - /// epoch is incremented and the new epoch is set on the timer. For timers created by - /// JavaScript, the epoch is used to break ties between timers scheduled for the same - /// millisecond. This ensures that if you set two timers for the same amount of time, and - /// refresh the first one, the first one will fire last. This mimics Node.js's behavior where - /// the refreshed timer will be inserted at the end of a list, which makes it fire later. - epoch: u25 = 0, - - kind: Kind = .setTimeout, - - // we do not allow the timer to be refreshed after we call clearInterval/clearTimeout - has_cleared_timer: bool = false, - is_keeping_event_loop_alive: bool = false, - - // if they never access the timer by integer, don't create a hashmap entry. - has_accessed_primitive: bool = false, - - has_js_ref: bool = true, - - /// Set to `true` only during execution of the JavaScript function so that `_destroyed` can be - /// false during the callback, even though the `state` will be `FIRED`. - in_callback: bool = false, - }; - - fn eventLoopTimer(this: *TimerObjectInternals) *EventLoopTimer { - switch (this.flags.kind) { - .setImmediate => { - const parent: *ImmediateObject = @fieldParentPtr("internals", this); - assert(parent.event_loop_timer.tag == .ImmediateObject); - return &parent.event_loop_timer; - }, - .setTimeout, .setInterval => { - const parent: *TimeoutObject = @fieldParentPtr("internals", this); - assert(parent.event_loop_timer.tag == .TimeoutObject); - return &parent.event_loop_timer; - }, - } - } - - fn ref(this: *TimerObjectInternals) void { - switch (this.flags.kind) { - .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).ref(), - .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).ref(), - } - } - - fn deref(this: *TimerObjectInternals) void { - switch (this.flags.kind) { - .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).deref(), - .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).deref(), - } - } - - extern "c" fn Bun__JSTimeout__call(globalObject: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue) bool; - - /// returns true if an exception was thrown - pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool { - if (this.flags.has_cleared_timer or - // unref'd setImmediate callbacks should only run if there are things keeping the event - // loop alive other than setImmediates - (!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates())) - { - this.deref(); - return false; - } - - const timer = this.strong_this.get() orelse { - if (Environment.isDebug) { - @panic("TimerObjectInternals.runImmediateTask: this_object is null"); - } - return false; - }; - const globalThis = vm.global; - this.strong_this.deinit(); - this.eventLoopTimer().state = .FIRED; - this.setEnableKeepingEventLoopAlive(vm, false); - - vm.eventLoop().enter(); - const callback = ImmediateObject.js.callbackGetCached(timer).?; - const arguments = ImmediateObject.js.argumentsGetCached(timer).?; - this.ref(); - const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm); - this.deref(); - - if (this.eventLoopTimer().state == .FIRED) { - this.deref(); - } - - vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown); - - return exception_thrown; - } - - pub fn asyncID(this: *const TimerObjectInternals) u64 { - return ID.asyncID(.{ .id = this.id, .kind = this.flags.kind.big() }); - } - - pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm { - const id = this.id; - const kind = this.flags.kind.big(); - const async_id: ID = .{ .id = id, .kind = kind }; - const has_been_cleared = this.eventLoopTimer().state == .CANCELLED or this.flags.has_cleared_timer or vm.scriptExecutionStatus() != .running; - - this.eventLoopTimer().state = .FIRED; - - const globalThis = vm.global; - const this_object = this.strong_this.get().?; - - const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) { - .setImmediate => .{ - ImmediateObject.js.callbackGetCached(this_object).?, - ImmediateObject.js.argumentsGetCached(this_object).?, - .js_undefined, - .js_undefined, - }, - .setTimeout, .setInterval => .{ - TimeoutObject.js.callbackGetCached(this_object).?, - TimeoutObject.js.argumentsGetCached(this_object).?, - TimeoutObject.js.idleTimeoutGetCached(this_object).?, - TimeoutObject.js.repeatGetCached(this_object).?, - }, - }; - - if (has_been_cleared or !callback.toBoolean()) { - if (vm.isInspectorEnabled()) { - Debugger.didCancelAsyncCall(globalThis, .DOMTimer, ID.asyncID(async_id)); - } - this.setEnableKeepingEventLoopAlive(vm, false); - this.flags.has_cleared_timer = true; - this.strong_this.deinit(); - this.deref(); - - return .disarm; - } - - var time_before_call: timespec = undefined; - - if (kind != .setInterval) { - this.strong_this.clearWithoutDeallocation(); - } else { - time_before_call = timespec.msFromNow(this.interval); - } - this_object.ensureStillAlive(); - - vm.eventLoop().enter(); - { - // Ensure it stays alive for this scope. - this.ref(); - defer this.deref(); - - _ = this.run(globalThis, this_object, callback, arguments, ID.asyncID(async_id), vm); - - switch (kind) { - .setTimeout, .setInterval => { - idle_timeout = TimeoutObject.js.idleTimeoutGetCached(this_object).?; - repeat = TimeoutObject.js.repeatGetCached(this_object).?; - }, - else => {}, - } - - const is_timer_done = is_timer_done: { - // Node doesn't drain microtasks after each timer callback. - if (kind == .setInterval) { - if (!this.shouldRescheduleTimer(repeat, idle_timeout)) { - break :is_timer_done true; - } - switch (this.eventLoopTimer().state) { - .FIRED => { - // If we didn't clear the setInterval, reschedule it starting from - vm.timer.update(this.eventLoopTimer(), &time_before_call); - - if (this.flags.has_js_ref) { - this.setEnableKeepingEventLoopAlive(vm, true); - } - - // The ref count doesn't change. It wasn't decremented. - }, - .ACTIVE => { - // The developer called timer.refresh() synchronously in the callback. - vm.timer.update(this.eventLoopTimer(), &time_before_call); - - // Balance out the ref count. - // the transition from "FIRED" -> "ACTIVE" caused it to increment. - this.deref(); - }, - else => { - break :is_timer_done true; - }, - } - } else { - if (kind == .setTimeout and !repeat.isNull()) { - if (idle_timeout.getNumber()) |num| { - if (num != -1) { - this.convertToInterval(globalThis, this_object, repeat); - break :is_timer_done false; - } - } - } - - if (this.eventLoopTimer().state == .FIRED) { - break :is_timer_done true; - } - } - - break :is_timer_done false; - }; - - if (is_timer_done) { - this.setEnableKeepingEventLoopAlive(vm, false); - // The timer will not be re-entered into the event loop at this point. - this.deref(); - } - } - vm.eventLoop().exit(); - - return .disarm; - } - - fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer: JSValue, repeat: JSValue) void { - bun.debugAssert(this.flags.kind == .setTimeout); - - const vm = global.bunVM(); - - const new_interval: u31 = if (repeat.getNumber()) |num| if (num < 1 or num > std.math.maxInt(u31)) 1 else @intFromFloat(num) else 1; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613 - TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat); - this.strong_this.set(global, timer); - this.flags.kind = .setInterval; - this.interval = new_interval; - this.reschedule(timer, vm); - } - - pub fn run(this: *TimerObjectInternals, globalThis: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, vm: *JSC.VirtualMachine) bool { - if (vm.isInspectorEnabled()) { - Debugger.willDispatchAsyncCall(globalThis, .DOMTimer, async_id); - } - - defer { - if (vm.isInspectorEnabled()) { - Debugger.didDispatchAsyncCall(globalThis, .DOMTimer, async_id); - } - } - - // Bun__JSTimeout__call handles exceptions. - this.flags.in_callback = true; - defer this.flags.in_callback = false; - return Bun__JSTimeout__call(globalThis, timer, callback, arguments); - } - - pub fn init( - this: *TimerObjectInternals, - timer: JSValue, - global: *JSGlobalObject, - id: i32, - kind: Kind, - interval: u31, - callback: JSValue, - arguments: JSValue, - ) void { - const vm = global.bunVM(); - this.* = .{ - .id = id, - .flags = .{ .kind = kind, .epoch = vm.timer.epoch }, - .interval = interval, - }; - - if (kind == .setImmediate) { - ImmediateObject.js.argumentsSetCached(timer, global, arguments); - ImmediateObject.js.callbackSetCached(timer, global, callback); - const parent: *ImmediateObject = @fieldParentPtr("internals", this); - vm.enqueueImmediateTask(parent); - this.setEnableKeepingEventLoopAlive(vm, true); - // ref'd by event loop - parent.ref(); - } else { - TimeoutObject.js.argumentsSetCached(timer, global, arguments); - TimeoutObject.js.callbackSetCached(timer, global, callback); - TimeoutObject.js.idleTimeoutSetCached(timer, global, JSC.jsNumber(interval)); - TimeoutObject.js.repeatSetCached(timer, global, if (kind == .setInterval) JSC.jsNumber(interval) else .null); - - // this increments the refcount - this.reschedule(timer, vm); - } - - this.strong_this.set(global, timer); - } - - pub fn doRef(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - this_value.ensureStillAlive(); - - const did_have_js_ref = this.flags.has_js_ref; - this.flags.has_js_ref = true; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L256 - // and - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L685-L687 - if (!did_have_js_ref and !this.flags.has_cleared_timer) { - this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), true); - } - - return this_value; - } - - pub fn doRefresh(this: *TimerObjectInternals, globalObject: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - // Immediates do not have a refresh function, and our binding generator should not let this - // function be reached even if you override the `this` value calling a Timeout object's - // `refresh` method - assert(this.flags.kind != .setImmediate); - - // setImmediate does not support refreshing and we do not support refreshing after cleanup - if (this.id == -1 or this.flags.kind == .setImmediate or this.flags.has_cleared_timer) { - return this_value; - } - - this.strong_this.set(globalObject, this_value); - this.reschedule(this_value, VirtualMachine.get()); - - return this_value; - } - - pub fn doUnref(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - this_value.ensureStillAlive(); - - const did_have_js_ref = this.flags.has_js_ref; - this.flags.has_js_ref = false; - - if (did_have_js_ref) { - this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), false); - } - - return this_value; - } - - pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void { - this.setEnableKeepingEventLoopAlive(vm, false); - this.flags.has_cleared_timer = true; - - if (this.flags.kind == .setImmediate) return; - - const was_active = this.eventLoopTimer().state == .ACTIVE; - - this.eventLoopTimer().state = .CANCELLED; - this.strong_this.deinit(); - - if (was_active) { - vm.timer.remove(this.eventLoopTimer()); - this.deref(); - } - } - - fn shouldRescheduleTimer(this: *TimerObjectInternals, repeat: JSValue, idle_timeout: JSValue) bool { - if (this.flags.kind == .setInterval and repeat.isNull()) return false; - if (idle_timeout.getNumber()) |num| { - if (num == -1) return false; - } - return true; - } - - pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine) void { - if (this.flags.kind == .setImmediate) return; - - const idle_timeout = TimeoutObject.js.idleTimeoutGetCached(timer).?; - const repeat = TimeoutObject.js.repeatGetCached(timer).?; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612 - if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return; - - const now = timespec.msFromNow(this.interval); - const was_active = this.eventLoopTimer().state == .ACTIVE; - if (was_active) { - vm.timer.remove(this.eventLoopTimer()); - } else { - this.ref(); - } - - vm.timer.update(this.eventLoopTimer(), &now); - this.flags.has_cleared_timer = false; - - if (this.flags.has_js_ref) { - this.setEnableKeepingEventLoopAlive(vm, true); - } - } - - fn setEnableKeepingEventLoopAlive(this: *TimerObjectInternals, vm: *VirtualMachine, enable: bool) void { - if (this.flags.is_keeping_event_loop_alive == enable) { - return; - } - this.flags.is_keeping_event_loop_alive = enable; - switch (this.flags.kind) { - .setTimeout, .setInterval => vm.timer.incrementTimerRef(if (enable) 1 else -1), - - // setImmediate has slightly different event loop logic - .setImmediate => vm.timer.incrementImmediateRef(if (enable) 1 else -1), - } - } - - pub fn hasRef(this: *TimerObjectInternals) JSValue { - return JSValue.jsBoolean(this.flags.is_keeping_event_loop_alive); - } - - pub fn toPrimitive(this: *TimerObjectInternals) bun.JSError!JSValue { - if (!this.flags.has_accessed_primitive) { - this.flags.has_accessed_primitive = true; - const vm = VirtualMachine.get(); - try vm.timer.maps.get(this.flags.kind).put(bun.default_allocator, this.id, this.eventLoopTimer()); - } - return JSValue.jsNumber(this.id); - } - - /// This is the getter for `_destroyed` on JS Timeout and Immediate objects - pub fn getDestroyed(this: *TimerObjectInternals) bool { - if (this.flags.has_cleared_timer) { - return true; - } - if (this.flags.in_callback) { - return false; - } - return switch (this.eventLoopTimer().state) { - .ACTIVE, .PENDING => false, - .FIRED, .CANCELLED => true, - }; - } - - pub fn finalize(this: *TimerObjectInternals) void { - this.strong_this.deinit(); - this.deref(); - } - - pub fn deinit(this: *TimerObjectInternals) void { - this.strong_this.deinit(); - const vm = VirtualMachine.get(); - const kind = this.flags.kind; - - if (this.eventLoopTimer().state == .ACTIVE) { - vm.timer.remove(this.eventLoopTimer()); - } - - if (this.flags.has_accessed_primitive) { - const map = vm.timer.maps.get(kind); - if (map.orderedRemove(this.id)) { - // If this array gets large, let's shrink it down - // Array keys are i32 - // Values are 1 ptr - // Therefore, 12 bytes per entry - // So if you created 21,000 timers and accessed them by ID, you'd be using 252KB - const allocated_bytes = map.capacity() * @sizeOf(TimeoutMap.Data); - const used_bytes = map.count() * @sizeOf(TimeoutMap.Data); - if (allocated_bytes - used_bytes > 256 * 1024) { - map.shrinkAndFree(bun.default_allocator, map.count() + 8); - } - } - } - - this.setEnableKeepingEventLoopAlive(vm, false); - switch (kind) { - .setImmediate => (@as(*ImmediateObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), - .setTimeout, .setInterval => (@as(*TimeoutObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), - } - } -}; +pub const TimeoutObject = @import("./Timer/TimeoutObject.zig"); +pub const ImmediateObject = @import("./Timer/ImmediateObject.zig"); +pub const TimerObjectInternals = @import("./Timer/TimerObjectInternals.zig"); pub const Kind = enum(u2) { setTimeout = 0, @@ -1275,235 +584,6 @@ pub const ID = extern struct { const assert = bun.assert; const heap = bun.io.heap; -pub const EventLoopTimer = struct { - /// The absolute time to fire this timer next. - next: timespec, - state: State = .PENDING, - tag: Tag, - /// Internal heap fields. - heap: heap.IntrusiveField(EventLoopTimer) = .{}, - - pub fn initPaused(tag: Tag) EventLoopTimer { - return .{ - .next = .{}, - .tag = tag, - }; - } - - pub fn less(_: void, a: *const EventLoopTimer, b: *const EventLoopTimer) bool { - const sec_order = std.math.order(a.next.sec, b.next.sec); - if (sec_order != .eq) return sec_order == .lt; - - // collapse sub-millisecond precision for JavaScript timers - const maybe_a_internals = a.jsTimerInternals(); - const maybe_b_internals = b.jsTimerInternals(); - var a_ns = a.next.nsec; - var b_ns = b.next.nsec; - if (maybe_a_internals != null) a_ns = std.time.ns_per_ms * @divTrunc(a_ns, std.time.ns_per_ms); - if (maybe_b_internals != null) b_ns = std.time.ns_per_ms * @divTrunc(b_ns, std.time.ns_per_ms); - - const order = std.math.order(a_ns, b_ns); - if (order == .eq) { - if (maybe_a_internals) |a_internals| { - if (maybe_b_internals) |b_internals| { - // We expect that the epoch will overflow sometimes. - // If it does, we would ideally like timers with an epoch from before the - // overflow to be sorted *before* timers with an epoch from after the overflow - // (even though their epoch will be numerically *larger*). - // - // Wrapping subtraction gives us a distance that is consistent even if one - // epoch has overflowed and the other hasn't. If the distance from a to b is - // small, it's likely that b is really newer than a, so we consider a less than - // b. If the distance from a to b is large (greater than half the u25 range), - // it's more likely that b is older than a so the true distance is from b to a. - return b_internals.flags.epoch -% a_internals.flags.epoch < std.math.maxInt(u25) / 2; - } - } - } - return order == .lt; - } - - pub const Tag = if (Environment.isWindows) enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - TestRunner, - StatWatcherScheduler, - UpgradedDuplex, - DNSResolver, - WindowsNamedPipe, - WTFTimer, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .TestRunner => JSC.Jest.TestRunner, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .DNSResolver => DNSResolver, - .WindowsNamedPipe => uws.WindowsNamedPipe, - .WTFTimer => WTFTimer, - .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, - .SubprocessTimeout => JSC.Subprocess, - .ValkeyConnectionReconnect => JSC.API.Valkey, - .ValkeyConnectionTimeout => JSC.API.Valkey, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - }; - } - } else enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - TestRunner, - StatWatcherScheduler, - UpgradedDuplex, - WTFTimer, - DNSResolver, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .TestRunner => JSC.Jest.TestRunner, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .WTFTimer => WTFTimer, - .DNSResolver => DNSResolver, - .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, - .ValkeyConnectionTimeout => JSC.API.Valkey, - .ValkeyConnectionReconnect => JSC.API.Valkey, - .SubprocessTimeout => JSC.Subprocess, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - }; - } - }; - - const TimerCallback = struct { - callback: *const fn (*TimerCallback) Arm, - ctx: *anyopaque, - event_loop_timer: EventLoopTimer, - }; - - pub const State = enum { - /// The timer is waiting to be enabled. - PENDING, - - /// The timer is active and will fire at the next time. - ACTIVE, - - /// The timer has been cancelled and will not fire. - CANCELLED, - - /// The timer has fired and the callback has been called. - FIRED, - }; - - /// If self was created by set{Immediate,Timeout,Interval}, get a pointer to the common data - /// for all those kinds of timers - fn jsTimerInternals(self: anytype) switch (@TypeOf(self)) { - *EventLoopTimer => ?*TimerObjectInternals, - *const EventLoopTimer => ?*const TimerObjectInternals, - else => |T| @compileError("wrong type " ++ @typeName(T) ++ " passed to jsTimerInternals"), - } { - switch (self.tag) { - inline .TimeoutObject, .ImmediateObject => |tag| { - const parent: switch (@TypeOf(self)) { - *EventLoopTimer => *tag.Type(), - *const EventLoopTimer => *const tag.Type(), - else => unreachable, - } = @fieldParentPtr("event_loop_timer", self); - return &parent.internals; - }, - else => return null, - } - } - - fn ns(self: *const EventLoopTimer) u64 { - return self.next.ns(); - } - - pub const Arm = union(enum) { - rearm: timespec, - disarm, - }; - - pub fn fire(this: *EventLoopTimer, now: *const timespec, vm: *VirtualMachine) Arm { - switch (this.tag) { - .PostgresSQLConnectionTimeout => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), - .PostgresSQLConnectionMaxLifetime => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(), - .ValkeyConnectionTimeout => return @as(*api.Valkey, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), - .ValkeyConnectionReconnect => return @as(*api.Valkey, @alignCast(@fieldParentPtr("reconnect_timer", this))).onReconnectTimer(), - .DevServerMemoryVisualizerTick => return bun.bake.DevServer.emitMemoryVisualizerMessageTimer(this, now), - .DevServerSweepSourceMaps => return bun.bake.DevServer.SourceMapStore.sweepWeakRefs(this, now), - inline else => |t| { - if (@FieldType(t.Type(), "event_loop_timer") != EventLoopTimer) { - @compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'"); - } - var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this)); - if (comptime t.Type() == TimeoutObject or t.Type() == ImmediateObject) { - return container.internals.fire(now, vm); - } - - if (comptime t.Type() == WTFTimer) { - return container.fire(now, vm); - } - - if (comptime t.Type() == StatWatcherScheduler) { - return container.timerCallback(); - } - if (comptime t.Type() == uws.UpgradedDuplex) { - return container.onTimeout(); - } - if (Environment.isWindows) { - if (comptime t.Type() == uws.WindowsNamedPipe) { - return container.onTimeout(); - } - } - - if (comptime t.Type() == JSC.Jest.TestRunner) { - container.onTestTimeout(now, vm); - return .disarm; - } - - if (comptime t.Type() == DNSResolver) { - return container.checkTimeouts(now, vm); - } - - if (comptime t.Type() == JSC.Subprocess) { - return container.timeoutCallback(); - } - - return container.callback(container); - }, - } - } - - pub fn deinit(_: *EventLoopTimer) void {} -}; - const timespec = bun.timespec; /// A timer created by WTF code and invoked by Bun's event loop diff --git a/src/bun.js/api/Timer/EventLoopTimer.zig b/src/bun.js/api/Timer/EventLoopTimer.zig new file mode 100644 index 0000000000..232949cb4a --- /dev/null +++ b/src/bun.js/api/Timer/EventLoopTimer.zig @@ -0,0 +1,247 @@ +const EventLoopTimer = @This(); + +/// The absolute time to fire this timer next. +next: timespec, +state: State = .PENDING, +tag: Tag, +/// Internal heap fields. +heap: bun.io.heap.IntrusiveField(EventLoopTimer) = .{}, + +pub fn initPaused(tag: Tag) EventLoopTimer { + return .{ + .next = .{}, + .tag = tag, + }; +} + +pub fn less(_: void, a: *const EventLoopTimer, b: *const EventLoopTimer) bool { + const sec_order = std.math.order(a.next.sec, b.next.sec); + if (sec_order != .eq) return sec_order == .lt; + + // collapse sub-millisecond precision for JavaScript timers + const maybe_a_internals = a.jsTimerInternals(); + const maybe_b_internals = b.jsTimerInternals(); + var a_ns = a.next.nsec; + var b_ns = b.next.nsec; + if (maybe_a_internals != null) a_ns = std.time.ns_per_ms * @divTrunc(a_ns, std.time.ns_per_ms); + if (maybe_b_internals != null) b_ns = std.time.ns_per_ms * @divTrunc(b_ns, std.time.ns_per_ms); + + const order = std.math.order(a_ns, b_ns); + if (order == .eq) { + if (maybe_a_internals) |a_internals| { + if (maybe_b_internals) |b_internals| { + // We expect that the epoch will overflow sometimes. + // If it does, we would ideally like timers with an epoch from before the + // overflow to be sorted *before* timers with an epoch from after the overflow + // (even though their epoch will be numerically *larger*). + // + // Wrapping subtraction gives us a distance that is consistent even if one + // epoch has overflowed and the other hasn't. If the distance from a to b is + // small, it's likely that b is really newer than a, so we consider a less than + // b. If the distance from a to b is large (greater than half the u25 range), + // it's more likely that b is older than a so the true distance is from b to a. + return b_internals.flags.epoch -% a_internals.flags.epoch < std.math.maxInt(u25) / 2; + } + } + } + return order == .lt; +} + +pub const Tag = if (Environment.isWindows) enum { + TimerCallback, + TimeoutObject, + ImmediateObject, + TestRunner, + StatWatcherScheduler, + UpgradedDuplex, + DNSResolver, + WindowsNamedPipe, + WTFTimer, + PostgresSQLConnectionTimeout, + PostgresSQLConnectionMaxLifetime, + ValkeyConnectionTimeout, + ValkeyConnectionReconnect, + SubprocessTimeout, + DevServerSweepSourceMaps, + DevServerMemoryVisualizerTick, + + pub fn Type(comptime T: Tag) type { + return switch (T) { + .TimerCallback => TimerCallback, + .TimeoutObject => TimeoutObject, + .ImmediateObject => ImmediateObject, + .TestRunner => JSC.Jest.TestRunner, + .StatWatcherScheduler => StatWatcherScheduler, + .UpgradedDuplex => uws.UpgradedDuplex, + .DNSResolver => DNSResolver, + .WindowsNamedPipe => uws.WindowsNamedPipe, + .WTFTimer => WTFTimer, + .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, + .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, + .SubprocessTimeout => JSC.Subprocess, + .ValkeyConnectionReconnect => JSC.API.Valkey, + .ValkeyConnectionTimeout => JSC.API.Valkey, + .DevServerSweepSourceMaps, + .DevServerMemoryVisualizerTick, + => bun.bake.DevServer, + }; + } +} else enum { + TimerCallback, + TimeoutObject, + ImmediateObject, + TestRunner, + StatWatcherScheduler, + UpgradedDuplex, + WTFTimer, + DNSResolver, + PostgresSQLConnectionTimeout, + PostgresSQLConnectionMaxLifetime, + ValkeyConnectionTimeout, + ValkeyConnectionReconnect, + SubprocessTimeout, + DevServerSweepSourceMaps, + DevServerMemoryVisualizerTick, + + pub fn Type(comptime T: Tag) type { + return switch (T) { + .TimerCallback => TimerCallback, + .TimeoutObject => TimeoutObject, + .ImmediateObject => ImmediateObject, + .TestRunner => JSC.Jest.TestRunner, + .StatWatcherScheduler => StatWatcherScheduler, + .UpgradedDuplex => uws.UpgradedDuplex, + .WTFTimer => WTFTimer, + .DNSResolver => DNSResolver, + .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, + .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, + .ValkeyConnectionTimeout => JSC.API.Valkey, + .ValkeyConnectionReconnect => JSC.API.Valkey, + .SubprocessTimeout => JSC.Subprocess, + .DevServerSweepSourceMaps, + .DevServerMemoryVisualizerTick, + => bun.bake.DevServer, + }; + } +}; + +const TimerCallback = struct { + callback: *const fn (*TimerCallback) Arm, + ctx: *anyopaque, + event_loop_timer: EventLoopTimer, +}; + +pub const State = enum { + /// The timer is waiting to be enabled. + PENDING, + + /// The timer is active and will fire at the next time. + ACTIVE, + + /// The timer has been cancelled and will not fire. + CANCELLED, + + /// The timer has fired and the callback has been called. + FIRED, +}; + +/// If self was created by set{Immediate,Timeout,Interval}, get a pointer to the common data +/// for all those kinds of timers +pub fn jsTimerInternals(self: anytype) switch (@TypeOf(self)) { + *EventLoopTimer => ?*TimerObjectInternals, + *const EventLoopTimer => ?*const TimerObjectInternals, + else => |T| @compileError("wrong type " ++ @typeName(T) ++ " passed to jsTimerInternals"), +} { + switch (self.tag) { + inline .TimeoutObject, .ImmediateObject => |tag| { + const parent: switch (@TypeOf(self)) { + *EventLoopTimer => *tag.Type(), + *const EventLoopTimer => *const tag.Type(), + else => unreachable, + } = @fieldParentPtr("event_loop_timer", self); + return &parent.internals; + }, + else => return null, + } +} + +fn ns(self: *const EventLoopTimer) u64 { + return self.next.ns(); +} + +pub const Arm = union(enum) { + rearm: timespec, + disarm, +}; + +pub fn fire(this: *EventLoopTimer, now: *const timespec, vm: *VirtualMachine) Arm { + switch (this.tag) { + .PostgresSQLConnectionTimeout => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), + .PostgresSQLConnectionMaxLifetime => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(), + .ValkeyConnectionTimeout => return @as(*api.Valkey, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), + .ValkeyConnectionReconnect => return @as(*api.Valkey, @alignCast(@fieldParentPtr("reconnect_timer", this))).onReconnectTimer(), + .DevServerMemoryVisualizerTick => return bun.bake.DevServer.emitMemoryVisualizerMessageTimer(this, now), + .DevServerSweepSourceMaps => return bun.bake.DevServer.SourceMapStore.sweepWeakRefs(this, now), + inline else => |t| { + if (@FieldType(t.Type(), "event_loop_timer") != EventLoopTimer) { + @compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'"); + } + var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this)); + if (comptime t.Type() == TimeoutObject or t.Type() == ImmediateObject) { + return container.internals.fire(now, vm); + } + + if (comptime t.Type() == WTFTimer) { + return container.fire(now, vm); + } + + if (comptime t.Type() == StatWatcherScheduler) { + return container.timerCallback(); + } + if (comptime t.Type() == uws.UpgradedDuplex) { + return container.onTimeout(); + } + if (Environment.isWindows) { + if (comptime t.Type() == uws.WindowsNamedPipe) { + return container.onTimeout(); + } + } + + if (comptime t.Type() == JSC.Jest.TestRunner) { + container.onTestTimeout(now, vm); + return .disarm; + } + + if (comptime t.Type() == DNSResolver) { + return container.checkTimeouts(now, vm); + } + + if (comptime t.Type() == JSC.Subprocess) { + return container.timeoutCallback(); + } + + return container.callback(container); + }, + } +} + +pub fn deinit(_: *EventLoopTimer) void {} + +const timespec = bun.timespec; + +/// A timer created by WTF code and invoked by Bun's event loop +const WTFTimer = @import("../../WTFTimer.zig"); +const VirtualMachine = JSC.VirtualMachine; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const TimeoutObject = @import("../Timer.zig").TimeoutObject; +const ImmediateObject = @import("../Timer.zig").ImmediateObject; +const StatWatcherScheduler = @import("../../node/node_fs_stat_watcher.zig").StatWatcherScheduler; +const DNSResolver = @import("../bun/dns_resolver.zig").DNSResolver; + +const bun = @import("bun"); +const std = @import("std"); +const Environment = bun.Environment; +const JSC = bun.JSC; + +const uws = bun.uws; +const api = JSC.API; diff --git a/src/bun.js/api/Timer/ImmediateObject.zig b/src/bun.js/api/Timer/ImmediateObject.zig new file mode 100644 index 0000000000..d695be5bc2 --- /dev/null +++ b/src/bun.js/api/Timer/ImmediateObject.zig @@ -0,0 +1,104 @@ +const ImmediateObject = @This(); + +const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +pub const js = JSC.Codegen.JSImmediate; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; + +ref_count: RefCount, +event_loop_timer: EventLoopTimer = .{ + .next = .{}, + .tag = .ImmediateObject, +}, +internals: TimerObjectInternals, + +pub fn init( + globalThis: *JSGlobalObject, + id: i32, + callback: JSValue, + arguments: JSValue, +) JSValue { + // internals are initialized by init() + const immediate = bun.new(ImmediateObject, .{ .ref_count = .init(), .internals = undefined }); + const js_value = immediate.toJS(globalThis); + defer js_value.ensureStillAlive(); + immediate.internals.init( + js_value, + globalThis, + id, + .setImmediate, + 0, + callback, + arguments, + ); + + if (globalThis.bunVM().isInspectorEnabled()) { + Debugger.didScheduleAsyncCall( + globalThis, + .DOMTimer, + ID.asyncID(.{ .id = id, .kind = .setImmediate }), + true, + ); + } + + return js_value; +} + +fn deinit(this: *ImmediateObject) void { + this.internals.deinit(); + bun.destroy(this); +} + +pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*ImmediateObject { + _ = callFrame; + return globalObject.throw("Immediate is not constructible", .{}); +} + +/// returns true if an exception was thrown +pub fn runImmediateTask(this: *ImmediateObject, vm: *VirtualMachine) bool { + return this.internals.runImmediateTask(vm); +} + +pub fn toPrimitive(this: *ImmediateObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.toPrimitive(); +} + +pub fn doRef(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRef(globalThis, callFrame.this()); +} + +pub fn doUnref(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doUnref(globalThis, callFrame.this()); +} + +pub fn hasRef(this: *ImmediateObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.hasRef(); +} + +pub fn finalize(this: *ImmediateObject) void { + this.internals.finalize(); +} + +pub fn getDestroyed(this: *ImmediateObject, globalThis: *JSGlobalObject) JSValue { + _ = globalThis; + return .jsBoolean(this.internals.getDestroyed()); +} + +pub fn dispose(this: *ImmediateObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.internals.cancel(globalThis.bunVM()); + return .js_undefined; +} + +const bun = @import("bun"); +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const Debugger = @import("../../Debugger.zig"); +const ID = @import("../Timer.zig").ID; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; diff --git a/src/bun.js/api/Timer/TimeoutObject.zig b/src/bun.js/api/Timer/TimeoutObject.zig new file mode 100644 index 0000000000..7e69cea0c6 --- /dev/null +++ b/src/bun.js/api/Timer/TimeoutObject.zig @@ -0,0 +1,134 @@ +const TimeoutObject = @This(); + +const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +pub const js = JSC.Codegen.JSTimeout; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; + +ref_count: RefCount, +event_loop_timer: EventLoopTimer = .{ + .next = .{}, + .tag = .TimeoutObject, +}, +internals: TimerObjectInternals, + +pub fn init( + globalThis: *JSGlobalObject, + id: i32, + kind: Kind, + interval: u31, + callback: JSValue, + arguments: JSValue, +) JSValue { + // internals are initialized by init() + const timeout = bun.new(TimeoutObject, .{ .ref_count = .init(), .internals = undefined }); + const js_value = timeout.toJS(globalThis); + defer js_value.ensureStillAlive(); + timeout.internals.init( + js_value, + globalThis, + id, + kind, + interval, + callback, + arguments, + ); + + if (globalThis.bunVM().isInspectorEnabled()) { + Debugger.didScheduleAsyncCall( + globalThis, + .DOMTimer, + ID.asyncID(.{ .id = id, .kind = kind.big() }), + kind != .setInterval, + ); + } + + return js_value; +} + +fn deinit(this: *TimeoutObject) void { + this.internals.deinit(); + bun.destroy(this); +} + +pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*TimeoutObject { + _ = callFrame; + return globalObject.throw("Timeout is not constructible", .{}); +} + +pub fn toPrimitive(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.toPrimitive(); +} + +pub fn doRef(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRef(globalThis, callFrame.this()); +} + +pub fn doUnref(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doUnref(globalThis, callFrame.this()); +} + +pub fn doRefresh(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRefresh(globalThis, callFrame.this()); +} + +pub fn hasRef(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.hasRef(); +} + +pub fn finalize(this: *TimeoutObject) void { + this.internals.finalize(); +} + +pub fn getDestroyed(this: *TimeoutObject, globalThis: *JSGlobalObject) JSValue { + _ = globalThis; + return .jsBoolean(this.internals.getDestroyed()); +} + +pub fn close(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) JSValue { + this.internals.cancel(globalThis.bunVM()); + return callFrame.this(); +} + +pub fn get_onTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.callbackGetCached(thisValue).?; +} + +pub fn set_onTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.callbackSetCached(thisValue, globalThis, value); +} + +pub fn get_idleTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.idleTimeoutGetCached(thisValue).?; +} + +pub fn set_idleTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.idleTimeoutSetCached(thisValue, globalThis, value); +} + +pub fn get_repeat(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.repeatGetCached(thisValue).?; +} + +pub fn set_repeat(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.repeatSetCached(thisValue, globalThis, value); +} + +pub fn dispose(this: *TimeoutObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.internals.cancel(globalThis.bunVM()); + return .js_undefined; +} + +const bun = @import("bun"); +const JSC = bun.JSC; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const Debugger = @import("../../Debugger.zig"); +const ID = @import("../Timer.zig").ID; +const Kind = @import("../Timer.zig").Kind; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; diff --git a/src/bun.js/api/Timer/TimerObjectInternals.zig b/src/bun.js/api/Timer/TimerObjectInternals.zig new file mode 100644 index 0000000000..cc3db48996 --- /dev/null +++ b/src/bun.js/api/Timer/TimerObjectInternals.zig @@ -0,0 +1,487 @@ +/// Data that TimerObject and ImmediateObject have in common +const TimerObjectInternals = @This(); + +/// Identifier for this timer that is exposed to JavaScript (by `+timer`) +id: i32 = -1, +interval: u31 = 0, +strong_this: JSC.Strong.Optional = .empty, +flags: Flags = .{}, + +const Flags = packed struct(u32) { + /// Whenever a timer is inserted into the heap (which happen on creation or refresh), the global + /// epoch is incremented and the new epoch is set on the timer. For timers created by + /// JavaScript, the epoch is used to break ties between timers scheduled for the same + /// millisecond. This ensures that if you set two timers for the same amount of time, and + /// refresh the first one, the first one will fire last. This mimics Node.js's behavior where + /// the refreshed timer will be inserted at the end of a list, which makes it fire later. + epoch: u25 = 0, + + kind: Kind = .setTimeout, + + // we do not allow the timer to be refreshed after we call clearInterval/clearTimeout + has_cleared_timer: bool = false, + is_keeping_event_loop_alive: bool = false, + + // if they never access the timer by integer, don't create a hashmap entry. + has_accessed_primitive: bool = false, + + has_js_ref: bool = true, + + /// Set to `true` only during execution of the JavaScript function so that `_destroyed` can be + /// false during the callback, even though the `state` will be `FIRED`. + in_callback: bool = false, +}; + +fn eventLoopTimer(this: *TimerObjectInternals) *EventLoopTimer { + switch (this.flags.kind) { + .setImmediate => { + const parent: *ImmediateObject = @fieldParentPtr("internals", this); + assert(parent.event_loop_timer.tag == .ImmediateObject); + return &parent.event_loop_timer; + }, + .setTimeout, .setInterval => { + const parent: *TimeoutObject = @fieldParentPtr("internals", this); + assert(parent.event_loop_timer.tag == .TimeoutObject); + return &parent.event_loop_timer; + }, + } +} + +fn ref(this: *TimerObjectInternals) void { + switch (this.flags.kind) { + .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).ref(), + .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).ref(), + } +} + +fn deref(this: *TimerObjectInternals) void { + switch (this.flags.kind) { + .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).deref(), + .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).deref(), + } +} + +extern "c" fn Bun__JSTimeout__call(globalObject: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue) bool; + +/// returns true if an exception was thrown +pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool { + if (this.flags.has_cleared_timer or + // unref'd setImmediate callbacks should only run if there are things keeping the event + // loop alive other than setImmediates + (!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates())) + { + this.deref(); + return false; + } + + const timer = this.strong_this.get() orelse { + if (Environment.isDebug) { + @panic("TimerObjectInternals.runImmediateTask: this_object is null"); + } + return false; + }; + const globalThis = vm.global; + this.strong_this.deinit(); + this.eventLoopTimer().state = .FIRED; + this.setEnableKeepingEventLoopAlive(vm, false); + + vm.eventLoop().enter(); + const callback = ImmediateObject.js.callbackGetCached(timer).?; + const arguments = ImmediateObject.js.argumentsGetCached(timer).?; + this.ref(); + const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm); + this.deref(); + + if (this.eventLoopTimer().state == .FIRED) { + this.deref(); + } + + vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown); + + return exception_thrown; +} + +pub fn asyncID(this: *const TimerObjectInternals) u64 { + return ID.asyncID(.{ .id = this.id, .kind = this.flags.kind.big() }); +} + +pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm { + const id = this.id; + const kind = this.flags.kind.big(); + const async_id: ID = .{ .id = id, .kind = kind }; + const has_been_cleared = this.eventLoopTimer().state == .CANCELLED or this.flags.has_cleared_timer or vm.scriptExecutionStatus() != .running; + + this.eventLoopTimer().state = .FIRED; + + const globalThis = vm.global; + const this_object = this.strong_this.get().?; + + const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) { + .setImmediate => .{ + ImmediateObject.js.callbackGetCached(this_object).?, + ImmediateObject.js.argumentsGetCached(this_object).?, + .js_undefined, + .js_undefined, + }, + .setTimeout, .setInterval => .{ + TimeoutObject.js.callbackGetCached(this_object).?, + TimeoutObject.js.argumentsGetCached(this_object).?, + TimeoutObject.js.idleTimeoutGetCached(this_object).?, + TimeoutObject.js.repeatGetCached(this_object).?, + }, + }; + + if (has_been_cleared or !callback.toBoolean()) { + if (vm.isInspectorEnabled()) { + Debugger.didCancelAsyncCall(globalThis, .DOMTimer, ID.asyncID(async_id)); + } + this.setEnableKeepingEventLoopAlive(vm, false); + this.flags.has_cleared_timer = true; + this.strong_this.deinit(); + this.deref(); + + return .disarm; + } + + var time_before_call: timespec = undefined; + + if (kind != .setInterval) { + this.strong_this.clearWithoutDeallocation(); + } else { + time_before_call = timespec.msFromNow(this.interval); + } + this_object.ensureStillAlive(); + + vm.eventLoop().enter(); + { + // Ensure it stays alive for this scope. + this.ref(); + defer this.deref(); + + _ = this.run(globalThis, this_object, callback, arguments, ID.asyncID(async_id), vm); + + switch (kind) { + .setTimeout, .setInterval => { + idle_timeout = TimeoutObject.js.idleTimeoutGetCached(this_object).?; + repeat = TimeoutObject.js.repeatGetCached(this_object).?; + }, + else => {}, + } + + const is_timer_done = is_timer_done: { + // Node doesn't drain microtasks after each timer callback. + if (kind == .setInterval) { + if (!this.shouldRescheduleTimer(repeat, idle_timeout)) { + break :is_timer_done true; + } + switch (this.eventLoopTimer().state) { + .FIRED => { + // If we didn't clear the setInterval, reschedule it starting from + vm.timer.update(this.eventLoopTimer(), &time_before_call); + + if (this.flags.has_js_ref) { + this.setEnableKeepingEventLoopAlive(vm, true); + } + + // The ref count doesn't change. It wasn't decremented. + }, + .ACTIVE => { + // The developer called timer.refresh() synchronously in the callback. + vm.timer.update(this.eventLoopTimer(), &time_before_call); + + // Balance out the ref count. + // the transition from "FIRED" -> "ACTIVE" caused it to increment. + this.deref(); + }, + else => { + break :is_timer_done true; + }, + } + } else { + if (kind == .setTimeout and !repeat.isNull()) { + if (idle_timeout.getNumber()) |num| { + if (num != -1) { + this.convertToInterval(globalThis, this_object, repeat); + break :is_timer_done false; + } + } + } + + if (this.eventLoopTimer().state == .FIRED) { + break :is_timer_done true; + } + } + + break :is_timer_done false; + }; + + if (is_timer_done) { + this.setEnableKeepingEventLoopAlive(vm, false); + // The timer will not be re-entered into the event loop at this point. + this.deref(); + } + } + vm.eventLoop().exit(); + + return .disarm; +} + +fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer: JSValue, repeat: JSValue) void { + bun.debugAssert(this.flags.kind == .setTimeout); + + const vm = global.bunVM(); + + const new_interval: u31 = if (repeat.getNumber()) |num| if (num < 1 or num > std.math.maxInt(u31)) 1 else @intFromFloat(num) else 1; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613 + TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat); + this.strong_this.set(global, timer); + this.flags.kind = .setInterval; + this.interval = new_interval; + this.reschedule(timer, vm); +} + +pub fn run(this: *TimerObjectInternals, globalThis: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, vm: *JSC.VirtualMachine) bool { + if (vm.isInspectorEnabled()) { + Debugger.willDispatchAsyncCall(globalThis, .DOMTimer, async_id); + } + + defer { + if (vm.isInspectorEnabled()) { + Debugger.didDispatchAsyncCall(globalThis, .DOMTimer, async_id); + } + } + + // Bun__JSTimeout__call handles exceptions. + this.flags.in_callback = true; + defer this.flags.in_callback = false; + return Bun__JSTimeout__call(globalThis, timer, callback, arguments); +} + +pub fn init( + this: *TimerObjectInternals, + timer: JSValue, + global: *JSGlobalObject, + id: i32, + kind: Kind, + interval: u31, + callback: JSValue, + arguments: JSValue, +) void { + const vm = global.bunVM(); + this.* = .{ + .id = id, + .flags = .{ .kind = kind, .epoch = vm.timer.epoch }, + .interval = interval, + }; + + if (kind == .setImmediate) { + ImmediateObject.js.argumentsSetCached(timer, global, arguments); + ImmediateObject.js.callbackSetCached(timer, global, callback); + const parent: *ImmediateObject = @fieldParentPtr("internals", this); + vm.enqueueImmediateTask(parent); + this.setEnableKeepingEventLoopAlive(vm, true); + // ref'd by event loop + parent.ref(); + } else { + TimeoutObject.js.argumentsSetCached(timer, global, arguments); + TimeoutObject.js.callbackSetCached(timer, global, callback); + TimeoutObject.js.idleTimeoutSetCached(timer, global, JSC.jsNumber(interval)); + TimeoutObject.js.repeatSetCached(timer, global, if (kind == .setInterval) JSC.jsNumber(interval) else .null); + + // this increments the refcount + this.reschedule(timer, vm); + } + + this.strong_this.set(global, timer); +} + +pub fn doRef(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + this_value.ensureStillAlive(); + + const did_have_js_ref = this.flags.has_js_ref; + this.flags.has_js_ref = true; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L256 + // and + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L685-L687 + if (!did_have_js_ref and !this.flags.has_cleared_timer) { + this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), true); + } + + return this_value; +} + +pub fn doRefresh(this: *TimerObjectInternals, globalObject: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + // Immediates do not have a refresh function, and our binding generator should not let this + // function be reached even if you override the `this` value calling a Timeout object's + // `refresh` method + assert(this.flags.kind != .setImmediate); + + // setImmediate does not support refreshing and we do not support refreshing after cleanup + if (this.id == -1 or this.flags.kind == .setImmediate or this.flags.has_cleared_timer) { + return this_value; + } + + this.strong_this.set(globalObject, this_value); + this.reschedule(this_value, VirtualMachine.get()); + + return this_value; +} + +pub fn doUnref(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + this_value.ensureStillAlive(); + + const did_have_js_ref = this.flags.has_js_ref; + this.flags.has_js_ref = false; + + if (did_have_js_ref) { + this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), false); + } + + return this_value; +} + +pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void { + this.setEnableKeepingEventLoopAlive(vm, false); + this.flags.has_cleared_timer = true; + + if (this.flags.kind == .setImmediate) return; + + const was_active = this.eventLoopTimer().state == .ACTIVE; + + this.eventLoopTimer().state = .CANCELLED; + this.strong_this.deinit(); + + if (was_active) { + vm.timer.remove(this.eventLoopTimer()); + this.deref(); + } +} + +fn shouldRescheduleTimer(this: *TimerObjectInternals, repeat: JSValue, idle_timeout: JSValue) bool { + if (this.flags.kind == .setInterval and repeat.isNull()) return false; + if (idle_timeout.getNumber()) |num| { + if (num == -1) return false; + } + return true; +} + +pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine) void { + if (this.flags.kind == .setImmediate) return; + + const idle_timeout = TimeoutObject.js.idleTimeoutGetCached(timer).?; + const repeat = TimeoutObject.js.repeatGetCached(timer).?; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612 + if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return; + + const now = timespec.msFromNow(this.interval); + const was_active = this.eventLoopTimer().state == .ACTIVE; + if (was_active) { + vm.timer.remove(this.eventLoopTimer()); + } else { + this.ref(); + } + + vm.timer.update(this.eventLoopTimer(), &now); + this.flags.has_cleared_timer = false; + + if (this.flags.has_js_ref) { + this.setEnableKeepingEventLoopAlive(vm, true); + } +} + +fn setEnableKeepingEventLoopAlive(this: *TimerObjectInternals, vm: *VirtualMachine, enable: bool) void { + if (this.flags.is_keeping_event_loop_alive == enable) { + return; + } + this.flags.is_keeping_event_loop_alive = enable; + switch (this.flags.kind) { + .setTimeout, .setInterval => vm.timer.incrementTimerRef(if (enable) 1 else -1), + + // setImmediate has slightly different event loop logic + .setImmediate => vm.timer.incrementImmediateRef(if (enable) 1 else -1), + } +} + +pub fn hasRef(this: *TimerObjectInternals) JSValue { + return JSValue.jsBoolean(this.flags.is_keeping_event_loop_alive); +} + +pub fn toPrimitive(this: *TimerObjectInternals) bun.JSError!JSValue { + if (!this.flags.has_accessed_primitive) { + this.flags.has_accessed_primitive = true; + const vm = VirtualMachine.get(); + try vm.timer.maps.get(this.flags.kind).put(bun.default_allocator, this.id, this.eventLoopTimer()); + } + return JSValue.jsNumber(this.id); +} + +/// This is the getter for `_destroyed` on JS Timeout and Immediate objects +pub fn getDestroyed(this: *TimerObjectInternals) bool { + if (this.flags.has_cleared_timer) { + return true; + } + if (this.flags.in_callback) { + return false; + } + return switch (this.eventLoopTimer().state) { + .ACTIVE, .PENDING => false, + .FIRED, .CANCELLED => true, + }; +} + +pub fn finalize(this: *TimerObjectInternals) void { + this.strong_this.deinit(); + this.deref(); +} + +pub fn deinit(this: *TimerObjectInternals) void { + this.strong_this.deinit(); + const vm = VirtualMachine.get(); + const kind = this.flags.kind; + + if (this.eventLoopTimer().state == .ACTIVE) { + vm.timer.remove(this.eventLoopTimer()); + } + + if (this.flags.has_accessed_primitive) { + const map = vm.timer.maps.get(kind); + if (map.orderedRemove(this.id)) { + // If this array gets large, let's shrink it down + // Array keys are i32 + // Values are 1 ptr + // Therefore, 12 bytes per entry + // So if you created 21,000 timers and accessed them by ID, you'd be using 252KB + const allocated_bytes = map.capacity() * @sizeOf(TimeoutMap.Data); + const used_bytes = map.count() * @sizeOf(TimeoutMap.Data); + if (allocated_bytes - used_bytes > 256 * 1024) { + map.shrinkAndFree(bun.default_allocator, map.count() + 8); + } + } + } + + this.setEnableKeepingEventLoopAlive(vm, false); + switch (kind) { + .setImmediate => (@as(*ImmediateObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), + .setTimeout, .setInterval => (@as(*TimeoutObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), + } +} + +const bun = @import("bun"); +const std = @import("std"); +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const TimeoutObject = @import("../Timer.zig").TimeoutObject; +const ImmediateObject = @import("../Timer.zig").ImmediateObject; +const Debugger = @import("../../Debugger.zig"); +const timespec = bun.timespec; +const Environment = bun.Environment; +const ID = @import("../Timer.zig").ID; +const TimeoutMap = @import("../Timer.zig").TimeoutMap; +const Kind = @import("../Timer.zig").Kind; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const assert = bun.assert; From 346e97dde2b4db4df837bec0ff9615c969d73d1b Mon Sep 17 00:00:00 2001 From: 190n Date: Wed, 18 Jun 2025 23:08:19 -0700 Subject: [PATCH 005/147] fix bugs found by exception scope verification (#20285) Co-authored-by: 190n <7763597+190n@users.noreply.github.com> --- cmake/sources/CxxSources.txt | 1 + cmake/sources/ZigSources.txt | 1 + package.json | 2 +- scripts/runner.node.mjs | 46 +- src/bake/bake.zig | 30 +- src/bun.js/ConsoleObject.zig | 152 +- src/bun.js/ModuleLoader.zig | 28 +- src/bun.js/VirtualMachine.zig | 86 +- src/bun.js/api/BunObject.zig | 14 +- src/bun.js/api/JSBundler.zig | 24 +- src/bun.js/api/JSTranspiler.zig | 23 +- src/bun.js/api/Timer/TimerObjectInternals.zig | 2 +- src/bun.js/api/bun/dns_resolver.zig | 10 +- src/bun.js/api/bun/h2_frame_parser.zig | 12 +- src/bun.js/api/bun/subprocess.zig | 12 +- src/bun.js/api/bun/udp_socket.zig | 6 +- src/bun.js/api/ffi.zig | 24 +- src/bun.js/api/filesystem_router.zig | 6 +- src/bun.js/api/html_rewriter.zig | 2 +- src/bun.js/api/server/SSLConfig.zig | 12 +- src/bun.js/api/server/ServerConfig.zig | 6 +- src/bun.js/bindings/AnyPromise.zig | 6 +- src/bun.js/bindings/BunObject.cpp | 1 + src/bun.js/bindings/BunProcess.cpp | 1 - src/bun.js/bindings/CatchScope.zig | 173 + src/bun.js/bindings/CatchScopeBinding.cpp | 57 + src/bun.js/bindings/ErrorCode.cpp | 11 +- src/bun.js/bindings/Errorable.zig | 5 +- src/bun.js/bindings/ImportMetaObject.cpp | 10 +- src/bun.js/bindings/JSArray.zig | 6 +- src/bun.js/bindings/JSArrayIterator.zig | 9 +- src/bun.js/bindings/JSBuffer.cpp | 25 +- src/bun.js/bindings/JSCommonJSModule.cpp | 29 +- .../bindings/JSDOMExceptionHandling.cpp | 4 +- .../bindings/JSEnvironmentVariableMap.cpp | 4 +- src/bun.js/bindings/JSGlobalObject.zig | 1 + src/bun.js/bindings/JSModuleLoader.zig | 6 +- src/bun.js/bindings/JSObject.zig | 14 +- src/bun.js/bindings/JSPromise.zig | 13 +- src/bun.js/bindings/JSPropertyIterator.zig | 8 +- src/bun.js/bindings/JSValue.zig | 290 +- src/bun.js/bindings/ModuleLoader.cpp | 40 +- src/bun.js/bindings/NodeHTTP.cpp | 1 + src/bun.js/bindings/NodeModuleModule.zig | 17 +- src/bun.js/bindings/NodeTimerObject.cpp | 2 +- src/bun.js/bindings/ObjectBindings.cpp | 4 +- src/bun.js/bindings/ProcessBindingTTYWrap.cpp | 2 + src/bun.js/bindings/ZigErrorType.zig | 3 +- src/bun.js/bindings/ZigGlobalObject.cpp | 38 +- src/bun.js/bindings/bindings.cpp | 305 +- src/bun.js/bindings/headers-handwritten.h | 2 +- src/bun.js/bindings/headers.h | 11 - src/bun.js/bindings/helpers.h | 2 +- .../bindings/node/http/JSConnectionsList.cpp | 4 + src/bun.js/event_loop.zig | 24 +- src/bun.js/event_loop/Task.zig | 14 +- src/bun.js/ipc.zig | 14 +- src/bun.js/jsc.zig | 5 +- src/bun.js/jsc/host_fn.zig | 87 +- src/bun.js/modules/BunJSCModule.h | 3 + src/bun.js/modules/NodeBufferModule.h | 3 + src/bun.js/node/node_os.zig | 20 +- src/bun.js/node/node_process.zig | 8 +- src/bun.js/node/types.zig | 4 +- src/bun.js/node/util/parse_args.zig | 46 +- src/bun.js/node/util/validators.zig | 8 +- src/bun.js/test/expect.zig | 212 +- src/bun.js/test/jest.zig | 25 +- src/bun.js/test/pretty_format.zig | 78 +- src/bun.js/virtual_machine_exports.zig | 3 +- src/bun.js/webcore/Blob.zig | 14 +- src/bun.js/webcore/Body.zig | 2 +- src/bun.js/webcore/blob/read_file.zig | 2 +- src/bun.js/webcore/fetch.zig | 4 +- src/bun.js/webcore/streams.zig | 2 +- src/bun.zig | 9 + src/cli/test_command.zig | 2 +- src/codegen/bindgen.ts | 11 +- src/codegen/generate-classes.ts | 8 +- src/codegen/generate-js2native.ts | 2 +- src/codegen/generate-node-errors.ts | 1 + src/csrf.zig | 4 +- src/css/css_internals.zig | 4 +- src/css/values/color_js.zig | 16 +- src/deps/c_ares.zig | 4 +- src/install/install.zig | 4 +- src/install/npm.zig | 8 +- src/js_ast.zig | 10 +- src/napi/napi.zig | 12 +- src/shell/ParsedShellScript.zig | 2 +- src/shell/shell.zig | 16 +- src/sql/postgres.zig | 40 +- src/sql/postgres/postgres_types.zig | 5 +- src/string.zig | 24 +- src/transpiler.zig | 8 +- src/valkey/js_valkey_functions.zig | 14 +- test/internal/ban-words.test.ts | 4 + test/no-validate-exceptions.txt | 2807 +++++++++++++++++ 98 files changed, 4263 insertions(+), 933 deletions(-) create mode 100644 src/bun.js/bindings/CatchScope.zig create mode 100644 src/bun.js/bindings/CatchScopeBinding.cpp create mode 100644 test/no-validate-exceptions.txt diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index c5e07b9814..3f04817879 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -28,6 +28,7 @@ src/bun.js/bindings/BunWorkerGlobalScope.cpp src/bun.js/bindings/c-bindings.cpp src/bun.js/bindings/CallSite.cpp src/bun.js/bindings/CallSitePrototype.cpp +src/bun.js/bindings/CatchScopeBinding.cpp src/bun.js/bindings/CodeCoverage.cpp src/bun.js/bindings/ConsoleObject.cpp src/bun.js/bindings/Cookie.cpp diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 7ffe24fafb..f8e597503c 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -85,6 +85,7 @@ src/bun.js/bindings/AnyPromise.zig src/bun.js/bindings/bun-simdutf.zig src/bun.js/bindings/CachedBytecode.zig src/bun.js/bindings/CallFrame.zig +src/bun.js/bindings/CatchScope.zig src/bun.js/bindings/codegen.zig src/bun.js/bindings/CommonAbortReason.zig src/bun.js/bindings/CommonStrings.zig diff --git a/package.json b/package.json index 764e813718..06acf99b10 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "build": "bun run build:debug", - "watch": "bun run zig build check --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib -Doverride-no-export-cpp-apis=true", + "watch": "bun run zig build check --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", "watch-windows": "bun run zig build check-windows --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", "bd:v": "(bun run --silent build:debug &> /tmp/bun.debug.build.log || (cat /tmp/bun.debug.build.log && rm -rf /tmp/bun.debug.build.log && exit 1)) && rm -f /tmp/bun.debug.build.log && ./build/debug/bun-debug", "bd": "BUN_DEBUG_QUIET_LOGS=1 bun bd:v", diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 70c7cd5def..2aaaa179d6 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -230,6 +230,27 @@ function getTestExpectations() { return expectations; } +/** + * Returns whether we should validate exception checks running the given test + * @param {string} test + * @returns {boolean} + */ +const shouldValidateExceptions = (() => { + let skipArray; + return test => { + if (!skipArray) { + const path = join(cwd, "test/no-validate-exceptions.txt"); + if (!existsSync(path)) { + skipArray = []; + } + skipArray = readFileSync(path, "utf-8") + .split("\n") + .filter(line => !line.startsWith("#")); + } + return !(skipArray.includes(test) || skipArray.includes("test/" + test)); + }; +})(); + /** * @param {string} testPath * @returns {string[]} @@ -416,16 +437,20 @@ async function runTests() { const runWithBunTest = title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test"); const subcommand = runWithBunTest ? "test" : "run"; + const env = { + FORCE_COLOR: "0", + NO_COLOR: "1", + BUN_DEBUG_QUIET_LOGS: "1", + }; + if (basename(execPath).includes("asan") && shouldValidateExceptions(testPath)) { + env.BUN_JSC_validateExceptionChecks = "1"; + } await runTest(title, async () => { const { ok, error, stdout } = await spawnBun(execPath, { cwd: cwd, args: [subcommand, "--config=" + join(import.meta.dirname, "../bunfig.node-test.toml"), absoluteTestPath], timeout: getNodeParallelTestTimeout(title), - env: { - FORCE_COLOR: "0", - NO_COLOR: "1", - BUN_DEBUG_QUIET_LOGS: "1", - }, + env, stdout: chunk => pipeTestStdout(process.stdout, chunk), stderr: chunk => pipeTestStdout(process.stderr, chunk), }); @@ -953,13 +978,18 @@ async function spawnBunTest(execPath, testPath, options = { cwd }) { testArgs.push(absPath); + const env = { + GITHUB_ACTIONS: "true", // always true so annotations are parsed + }; + if (basename(execPath).includes("asan") && shouldValidateExceptions(relative(cwd, absPath))) { + env.BUN_JSC_validateExceptionChecks = "1"; + } + const { ok, error, stdout } = await spawnBun(execPath, { args: isReallyTest ? testArgs : [...args, absPath], cwd: options["cwd"], timeout: isReallyTest ? timeout : 30_000, - env: { - GITHUB_ACTIONS: "true", // always true so annotations are parsed - }, + env, stdout: chunk => pipeTestStdout(process.stdout, chunk), stderr: chunk => pipeTestStdout(process.stderr, chunk), }); diff --git a/src/bake/bake.zig b/src/bake/bake.zig index 39a665251b..a03eca25f0 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -104,13 +104,13 @@ pub const SplitBundlerOptions = struct { .ssr = .{}, }; - pub fn parsePluginArray(opts: *SplitBundlerOptions, plugin_array: JSValue, global: *JSC.JSGlobalObject) !void { + pub fn parsePluginArray(opts: *SplitBundlerOptions, plugin_array: JSValue, global: *JSC.JSGlobalObject) bun.JSError!void { const plugin = opts.plugin orelse Plugin.create(global, .bun); opts.plugin = plugin; const empty_object = JSValue.createEmptyObject(global, 0); - var iter = plugin_array.arrayIterator(global); - while (iter.next()) |plugin_config| { + var iter = try plugin_array.arrayIterator(global); + while (try iter.next()) |plugin_config| { if (!plugin_config.isObject()) { return global.throwInvalidArguments("Expected plugin to be an object", .{}); } @@ -359,7 +359,7 @@ pub const Framework = struct { refs: *StringRefList, bundler_options: *SplitBundlerOptions, arena: Allocator, - ) !Framework { + ) bun.JSError!Framework { if (opts.isString()) { const str = try opts.toBunString(global); defer str.deref(); @@ -446,13 +446,13 @@ pub const Framework = struct { const array = try opts.getArray(global, "builtInModules") orelse break :built_in_modules .{}; - const len = array.getLength(global); + const len = try array.getLength(global); var files: bun.StringArrayHashMapUnmanaged(BuiltInModule) = .{}; try files.ensureTotalCapacity(arena, len); - var it = array.arrayIterator(global); + var it = try array.arrayIterator(global); var i: usize = 0; - while (it.next()) |file| : (i += 1) { + while (try it.next()) |file| : (i += 1) { if (!file.isObject()) { return global.throwInvalidArguments("'builtInModules[{d}]' is not an object", .{i}); } @@ -477,16 +477,16 @@ pub const Framework = struct { const array: JSValue = try opts.getArray(global, "fileSystemRouterTypes") orelse { return global.throwInvalidArguments("Missing 'framework.fileSystemRouterTypes'", .{}); }; - const len = array.getLength(global); + const len = try array.getLength(global); if (len > 256) { return global.throwInvalidArguments("Framework can only define up to 256 file-system router types", .{}); } const file_system_router_types = try arena.alloc(FileSystemRouterType, len); - var it = array.arrayIterator(global); + var it = try array.arrayIterator(global); var i: usize = 0; errdefer for (file_system_router_types[0..i]) |*fsr| fsr.style.deinit(); - while (it.next()) |fsr_opts| : (i += 1) { + while (try it.next()) |fsr_opts| : (i += 1) { const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse { return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i}); }; @@ -511,10 +511,10 @@ pub const Framework = struct { break :exts &.{}; } } else if (exts_js.isArray()) { - var it_2 = exts_js.arrayIterator(global); + var it_2 = try exts_js.arrayIterator(global); var i_2: usize = 0; - const extensions = try arena.alloc([]const u8, exts_js.getLength(global)); - while (it_2.next()) |array_item| : (i_2 += 1) { + const extensions = try arena.alloc([]const u8, try exts_js.getLength(global)); + while (try it_2.next()) |array_item| : (i_2 += 1) { const slice = refs.track(try array_item.toSlice(global, arena)); if (bun.strings.eqlComptime(slice, "*")) return global.throwInvalidArguments("'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array.", .{}); @@ -536,10 +536,10 @@ pub const Framework = struct { const ignore_dirs: []const []const u8 = if (try fsr_opts.get(global, "ignoreDirs")) |exts_js| exts: { if (exts_js.isArray()) { - var it_2 = array.arrayIterator(global); + var it_2 = try array.arrayIterator(global); var i_2: usize = 0; const dirs = try arena.alloc([]const u8, len); - while (it_2.next()) |array_item| : (i_2 += 1) { + while (try it_2.next()) |array_item| : (i_2 += 1) { dirs[i_2] = refs.track(try array_item.toSlice(global, arena)); } break :exts dirs; diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 70fa6654e7..90891e020b 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -185,7 +185,7 @@ fn messageWithTypeAndLevel_( var tabular_data = vals[0]; if (tabular_data.isObject()) { const properties: JSValue = if (len >= 2 and vals[1].jsType().isArray()) vals[1] else .js_undefined; - var table_printer = TablePrinter.init( + var table_printer = try TablePrinter.init( global, level, tabular_data, @@ -277,13 +277,13 @@ pub const TablePrinter = struct { level: MessageLevel, tabular_data: JSValue, properties: JSValue, - ) TablePrinter { + ) bun.JSError!TablePrinter { return TablePrinter{ .level = level, .globalObject = globalObject, .tabular_data = tabular_data, .properties = properties, - .is_iterable = tabular_data.isIterable(globalObject), + .is_iterable = try tabular_data.isIterable(globalObject), .jstype = tabular_data.jsType(), .value_formatter = ConsoleObject.Formatter{ .remaining_values = &[_]JSValue{}, @@ -320,11 +320,11 @@ pub const TablePrinter = struct { }; /// Compute how much horizontal space will take a JSValue when printed - fn getWidthForValue(this: *TablePrinter, value: JSValue) u32 { + fn getWidthForValue(this: *TablePrinter, value: JSValue) bun.JSError!u32 { var width: usize = 0; var value_formatter = this.value_formatter; - const tag = ConsoleObject.Formatter.Tag.get(value, this.globalObject); + const tag = try ConsoleObject.Formatter.Tag.get(value, this.globalObject); value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); value_formatter.format( tag, @@ -343,7 +343,7 @@ pub const TablePrinter = struct { } /// Update the sizes of the columns for the values of a given row, and create any additional columns as needed - fn updateColumnsForRow(this: *TablePrinter, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue) !void { + fn updateColumnsForRow(this: *TablePrinter, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue) bun.JSError!void { // update size of "(index)" column const row_key_len: u32 = switch (row_key) { .str => |value| @intCast(value.visibleWidthExcludeANSIColors(false)), @@ -353,10 +353,10 @@ pub const TablePrinter = struct { // special handling for Map: column with idx=1 is "Keys" if (this.jstype.isMap()) { - const entry_key = row_value.getIndex(this.globalObject, 0); - const entry_value = row_value.getIndex(this.globalObject, 1); - columns.items[1].width = @max(columns.items[1].width, this.getWidthForValue(entry_key)); - this.values_col_width = @max(this.values_col_width orelse 0, this.getWidthForValue(entry_value)); + const entry_key = try row_value.getIndex(this.globalObject, 0); + const entry_value = try row_value.getIndex(this.globalObject, 1); + columns.items[1].width = @max(columns.items[1].width, try this.getWidthForValue(entry_key)); + this.values_col_width = @max(this.values_col_width orelse 0, try this.getWidthForValue(entry_value)); return; } @@ -366,8 +366,8 @@ pub const TablePrinter = struct { // - otherwise: iterate the object properties, and create the columns on-demand if (!this.properties.isUndefined()) { for (columns.items[1..]) |*column| { - if (row_value.getOwn(this.globalObject, column.name)) |value| { - column.width = @max(column.width, this.getWidthForValue(value)); + if (try row_value.getOwn(this.globalObject, column.name)) |value| { + column.width = @max(column.width, try this.getWidthForValue(value)); } } } else { @@ -399,12 +399,12 @@ pub const TablePrinter = struct { break :brk &columns.items[columns.items.len - 1]; }; - column.width = @max(column.width, this.getWidthForValue(value)); + column.width = @max(column.width, try this.getWidthForValue(value)); } } } else if (this.properties.isUndefined()) { // not object -> the value will go to the special "Values" column - this.values_col_width = @max(this.values_col_width orelse 1, this.getWidthForValue(row_value)); + this.values_col_width = @max(this.values_col_width orelse 1, try this.getWidthForValue(row_value)); } } @@ -452,24 +452,24 @@ pub const TablePrinter = struct { var value = JSValue.zero; if (col_idx == 1 and this.jstype.isMap()) { // is the "Keys" column, when iterating a Map? - value = row_value.getIndex(this.globalObject, 0); + value = try row_value.getIndex(this.globalObject, 0); } else if (col_idx == this.values_col_idx) { // is the "Values" column? if (this.jstype.isMap()) { - value = row_value.getIndex(this.globalObject, 1); + value = try row_value.getIndex(this.globalObject, 1); } else if (!row_value.isObject()) { value = row_value; } } else if (row_value.isObject()) { - value = row_value.getOwn(this.globalObject, col.name) orelse JSValue.zero; + value = try row_value.getOwn(this.globalObject, col.name) orelse JSValue.zero; } if (value == .zero) { try writer.writeByteNTimes(' ', col.width + (PADDING * 2)); } else { - const len: u32 = this.getWidthForValue(value); + const len: u32 = try this.getWidthForValue(value); const needed = col.width -| len; try writer.writeByteNTimes(' ', PADDING); - const tag = ConsoleObject.Formatter.Tag.get(value, this.globalObject); + const tag = try ConsoleObject.Formatter.Tag.get(value, this.globalObject); var value_formatter = this.value_formatter; value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); @@ -532,8 +532,8 @@ pub const TablePrinter = struct { // if the "properties" arg was provided, pre-populate the columns if (!this.properties.isUndefined()) { - var properties_iter = JSC.JSArrayIterator.init(this.properties, globalObject); - while (properties_iter.next()) |value| { + var properties_iter = try JSC.JSArrayIterator.init(this.properties, globalObject); + while (try properties_iter.next()) |value| { try columns.append(.{ .name = try value.toBunString(globalObject), }); @@ -838,7 +838,7 @@ pub fn format2( .error_display_level = options.error_display_level, }; defer fmt.deinit(); - const tag = ConsoleObject.Formatter.Tag.get(vals[0], global); + const tag = try ConsoleObject.Formatter.Tag.get(vals[0], global); fmt.writeIndent(Writer, writer) catch return; if (tag.tag == .String) { @@ -937,7 +937,7 @@ pub fn format2( } any = true; - tag = ConsoleObject.Formatter.Tag.get(this_value, global); + tag = try ConsoleObject.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .{ .StringPossiblyFormatted = {} }; } @@ -959,7 +959,7 @@ pub fn format2( _ = writer.write(" ") catch 0; } any = true; - tag = ConsoleObject.Formatter.Tag.get(this_value, global); + tag = try ConsoleObject.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .{ .StringPossiblyFormatted = {} }; } @@ -1046,7 +1046,7 @@ pub const Formatter = struct { self.formatter.remaining_values = &[_]JSValue{}; } try self.formatter.format( - Tag.get(self.value, self.formatter.globalThis), + try Tag.get(self.value, self.formatter.globalThis), @TypeOf(writer), writer, self.value, @@ -1181,7 +1181,7 @@ pub const Formatter = struct { cell: JSValue.JSType = JSValue.JSType.Cell, }; - pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { + pub fn get(value: JSValue, globalThis: *JSGlobalObject) bun.JSError!Result { return getAdvanced(value, globalThis, .{ .hide_global = false }); } @@ -1191,12 +1191,12 @@ pub const Formatter = struct { disable_inspect_custom: bool = false, }; - pub fn getAdvanced(value: JSValue, globalThis: *JSGlobalObject, opts: Options) Result { - switch (@intFromEnum(value)) { - 0, 0xa => return Result{ + pub fn getAdvanced(value: JSValue, globalThis: *JSGlobalObject, opts: Options) bun.JSError!Result { + switch (value) { + .zero, .js_undefined => return Result{ .tag = .{ .Undefined = {} }, }, - 0x2 => return Result{ + .null => return Result{ .tag = .{ .Null = {} }, }, else => {}, @@ -1294,16 +1294,16 @@ pub const Formatter = struct { // Is this a react element? if (js_type.isObject() and js_type != .ProxyObject) { - if (value.getOwnTruthy(globalThis, "$$typeof")) |typeof_symbol| { + if (try value.getOwnTruthy(globalThis, "$$typeof")) |typeof_symbol| { // React 18 and below var react_element_legacy = ZigString.init("react.element"); // For React 19 - https://github.com/oven-sh/bun/issues/17223 var react_element_transitional = ZigString.init("react.transitional.element"); var react_fragment = ZigString.init("react.fragment"); - if (JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_element_legacy), globalThis) or - JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_element_transitional), globalThis) or - JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_fragment), globalThis)) + if (try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_element_legacy), globalThis) or + try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_element_transitional), globalThis) or + try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_fragment), globalThis)) { return .{ .tag = .{ .JSX = {} }, .cell = js_type }; } @@ -1613,7 +1613,7 @@ pub const Formatter = struct { // > implementation-specific, potentially-interactive representation // > of an object judged to be maximally useful and informative. } - try this.format(Tag.get(next_value, global), Writer, writer_, next_value, global, enable_ansi_colors); + try this.format(try Tag.get(next_value, global), Writer, writer_, next_value, global, enable_ansi_colors); }, .c => { @@ -1766,8 +1766,8 @@ pub const Formatter = struct { this.writer.writeAll(" ") catch unreachable; } if (!is_iterator) { - const key = nextValue.getIndex(globalObject, 0); - const value = nextValue.getIndex(globalObject, 1); + const key = nextValue.getIndex(globalObject, 0) catch return; + const value = nextValue.getIndex(globalObject, 1) catch return; if (!single_line) { this.formatter.writeIndent(Writer, this.writer) catch unreachable; @@ -1775,7 +1775,7 @@ pub const Formatter = struct { const key_tag = Tag.getAdvanced(key, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, - }); + }) catch return; this.formatter.format( key_tag, @@ -1789,7 +1789,7 @@ pub const Formatter = struct { const value_tag = Tag.getAdvanced(value, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, - }); + }) catch return; this.formatter.format( value_tag, Writer, @@ -1806,7 +1806,7 @@ pub const Formatter = struct { const tag = Tag.getAdvanced(nextValue, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, - }); + }) catch return; this.formatter.format( tag, Writer, @@ -1847,7 +1847,7 @@ pub const Formatter = struct { const key_tag = Tag.getAdvanced(nextValue, globalObject, .{ .hide_global = true, .disable_inspect_custom = this.formatter.disable_inspect_custom, - }); + }) catch return; this.formatter.format( key_tag, Writer, @@ -1924,7 +1924,7 @@ pub const Formatter = struct { const tag = Tag.getAdvanced(value, globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, - }); + }) catch return; if (tag.cell.isHidden()) return; if (ctx.i == 0) { @@ -2270,7 +2270,7 @@ pub const Formatter = struct { if (result.isString()) { writer.print("{}", .{result.fmtString(this.globalThis)}); } else { - try this.format(ConsoleObject.Formatter.Tag.get(result, this.globalThis), Writer, writer_, result, this.globalThis, enable_ansi_colors); + try this.format(try ConsoleObject.Formatter.Tag.get(result, this.globalThis), Writer, writer_, result, this.globalThis, enable_ansi_colors); } }, .Symbol => { @@ -2369,7 +2369,7 @@ pub const Formatter = struct { } }, .Array => { - const len = value.getLength(this.globalThis); + const len = try value.getLength(this.globalThis); // TODO: DerivedArray does not get passed along in JSType, and it's not clear why. // if (jsType == .DerivedArray) { @@ -2404,7 +2404,7 @@ pub const Formatter = struct { first: { const element = value.getDirectIndex(this.globalThis, 0); - const tag = Tag.getAdvanced(element, this.globalThis, .{ + const tag = try Tag.getAdvanced(element, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -2487,7 +2487,7 @@ pub const Formatter = struct { writer.space(); } - const tag = Tag.getAdvanced(element, this.globalThis, .{ + const tag = try Tag.getAdvanced(element, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -2534,10 +2534,7 @@ pub const Formatter = struct { .parent = value, .i = i, }; - value.forEachPropertyNonIndexed(this.globalThis, &iter, Iterator.forEach); - if (this.globalThis.hasException()) { - return error.JSError; - } + try value.forEachPropertyNonIndexed(this.globalThis, &iter, Iterator.forEach); if (this.failed) return; } } @@ -2571,7 +2568,7 @@ pub const Formatter = struct { s3client.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(bun.webcore.FetchHeaders) != null) { - if (value.get_unsafe(this.globalThis, "toJSON")) |toJSONFunction| { + if (try value.get(this.globalThis, "toJSON")) |toJSONFunction| { this.addForNewLine("Headers ".len); writer.writeAll(comptime Output.prettyFmt("Headers ", enable_ansi_colors)); const prev_quote_keys = this.quote_keys; @@ -2589,7 +2586,7 @@ pub const Formatter = struct { ); } } else if (value.as(JSC.DOMFormData) != null) { - if (value.get_unsafe(this.globalThis, "toJSON")) |toJSONFunction| { + if (try value.get(this.globalThis, "toJSON")) |toJSONFunction| { const prev_quote_keys = this.quote_keys; this.quote_keys = true; defer this.quote_keys = prev_quote_keys; @@ -2705,7 +2702,7 @@ pub const Formatter = struct { writer.writeAll(comptime Output.prettyFmt("" ++ fmt ++ "", enable_ansi_colors)); }, .Map => { - const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length_value = try value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; @@ -2812,7 +2809,7 @@ pub const Formatter = struct { writer.writeAll("}"); }, .Set => { - const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length_value = try value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; @@ -2855,7 +2852,7 @@ pub const Formatter = struct { writer.writeAll("}"); }, .toJSON => { - if (value.get_unsafe(this.globalThis, "toJSON")) |func| brk: { + if (try value.get(this.globalThis, "toJSON")) |func| brk: { const result = func.call(this.globalThis, value, &.{}) catch { this.globalThis.clearException(); break :brk; @@ -2863,7 +2860,7 @@ pub const Formatter = struct { const prev_quote_keys = this.quote_keys; this.quote_keys = true; defer this.quote_keys = prev_quote_keys; - const tag = ConsoleObject.Formatter.Tag.get(result, this.globalThis); + const tag = try ConsoleObject.Formatter.Tag.get(result, this.globalThis); try this.format(tag, Writer, writer_, result, this.globalThis, enable_ansi_colors); return; } @@ -2896,7 +2893,7 @@ pub const Formatter = struct { }, .Event => { const event_type_value: JSValue = brk: { - const value_ = value.get_unsafe(this.globalThis, "type") orelse break :brk .js_undefined; + const value_ = try value.get(this.globalThis, "type") orelse break :brk .js_undefined; if (value_.isString()) { break :brk value_; } @@ -2949,7 +2946,7 @@ pub const Formatter = struct { .{}, ); - const tag = Tag.getAdvanced(message_value, this.globalThis, .{ + const tag = try Tag.getAdvanced(message_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -2973,7 +2970,7 @@ pub const Formatter = struct { .{}, ); const data: JSValue = (try value.fastGet(this.globalThis, .data)) orelse .js_undefined; - const tag = Tag.getAdvanced(data, this.globalThis, .{ + const tag = try Tag.getAdvanced(data, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -2995,7 +2992,7 @@ pub const Formatter = struct { .{}, ); - const tag = Tag.getAdvanced(error_value, this.globalThis, .{ + const tag = try Tag.getAdvanced(error_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -3029,8 +3026,8 @@ pub const Formatter = struct { defer if (tag_name_slice.isAllocated()) tag_name_slice.deinit(); - if (value.get_unsafe(this.globalThis, "type")) |type_value| { - const _tag = Tag.getAdvanced(type_value, this.globalThis, .{ + if (try value.get(this.globalThis, "type")) |type_value| { + const _tag = try Tag.getAdvanced(type_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -3062,7 +3059,7 @@ pub const Formatter = struct { writer.writeAll(tag_name_slice.slice()); if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); - if (value.get_unsafe(this.globalThis, "key")) |key_value| { + if (try value.get(this.globalThis, "key")) |key_value| { if (!key_value.isUndefinedOrNull()) { if (needs_space) writer.writeAll(" key=") @@ -3073,7 +3070,7 @@ pub const Formatter = struct { this.quote_strings = true; defer this.quote_strings = old_quote_strings; - try this.format(Tag.getAdvanced(key_value, this.globalThis, .{ + try this.format(try Tag.getAdvanced(key_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); @@ -3082,7 +3079,7 @@ pub const Formatter = struct { } } - if (value.get_unsafe(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| { const prev_quote_strings = this.quote_strings; defer this.quote_strings = prev_quote_strings; this.quote_strings = true; @@ -3095,7 +3092,7 @@ pub const Formatter = struct { }).init(this.globalThis, props_obj); defer props_iter.deinit(); - const children_prop = props.get_unsafe(this.globalThis, "children"); + const children_prop = try props.get(this.globalThis, "children"); if (props_iter.len > 0) { { this.indent += 1; @@ -3107,7 +3104,7 @@ pub const Formatter = struct { continue; const property_value = props_iter.value; - const tag = Tag.getAdvanced(property_value, this.globalThis, .{ + const tag = try Tag.getAdvanced(property_value, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }); @@ -3156,7 +3153,7 @@ pub const Formatter = struct { } if (children_prop) |children| { - const tag = Tag.get(children, this.globalThis); + const tag = try Tag.get(children, this.globalThis); const print_children = switch (tag.tag) { .String, .JSX, .Array => true, @@ -3191,14 +3188,14 @@ pub const Formatter = struct { this.indent += 1; this.writeIndent(Writer, writer_) catch unreachable; defer this.indent -|= 1; - try this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); + try this.format(try Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); } writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; }, .Array => { - const length = children.getLength(this.globalThis); + const length = try children.getLength(this.globalThis); if (length == 0) break :print_children; writer.writeAll(">\n"); @@ -3213,8 +3210,8 @@ pub const Formatter = struct { var j: usize = 0; while (j < length) : (j += 1) { - const child = children.getIndex(this.globalThis, @as(u32, @intCast(j))); - try this.format(Tag.getAdvanced(child, this.globalThis, .{ + const child = try children.getIndex(this.globalThis, @as(u32, @intCast(j))); + try this.format(try Tag.getAdvanced(child, this.globalThis, .{ .hide_global = true, .disable_inspect_custom = this.disable_inspect_custom, }), Writer, writer_, child, this.globalThis, enable_ansi_colors); @@ -3304,14 +3301,11 @@ pub const Formatter = struct { }); return; } else if (this.ordered_properties) { - value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); + try value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); } else { - value.forEachProperty(this.globalThis, &iter, Iterator.forEach); + try value.forEachProperty(this.globalThis, &iter, Iterator.forEach); } - if (this.globalThis.hasException()) { - return error.JSError; - } if (this.failed) return; if (iter.i == 0) { @@ -3464,7 +3458,7 @@ pub const Formatter = struct { } // TODO: if (options.showProxy), print like `Proxy { target: ..., handlers: ... }` // this is default off so it is not used. - try this.format(ConsoleObject.Formatter.Tag.get(target, this.globalThis), Writer, writer_, target, this.globalThis, enable_ansi_colors); + try this.format(try ConsoleObject.Formatter.Tag.get(target, this.globalThis), Writer, writer_, target, this.globalThis, enable_ansi_colors); }, } } @@ -3654,7 +3648,7 @@ pub fn timeLog( var writer = console.error_writer.writer(); const Writer = @TypeOf(writer); for (args[0..args_len]) |arg| { - const tag = ConsoleObject.Formatter.Tag.get(arg, global); + const tag = ConsoleObject.Formatter.Tag.get(arg, global) catch return; _ = writer.write(" ") catch 0; if (Output.enable_ansi_colors_stderr) { fmt.format(tag, Writer, writer, arg, global, true) catch {}; // TODO: diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index d190e053ea..5154d9c3e5 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -429,7 +429,7 @@ pub const AsyncModule = struct { errorable = JSC.ErrorableResolvedSource.ok(this.resumeLoadingModule(&log) catch |err| { switch (err) { error.JSError => { - errorable = .err(error.JSError, this.globalThis.takeError(error.JSError).asVoid()); + errorable = .err(error.JSError, this.globalThis.takeError(error.JSError)); break :outer; }, else => { @@ -468,13 +468,16 @@ pub const AsyncModule = struct { specifier_: bun.String, referrer_: bun.String, log: *logger.Log, - ) void { + ) bun.JSExecutionTerminated!void { JSC.markBinding(@src()); var specifier = specifier_; var referrer = referrer_; + var scope: JSC.CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); defer { specifier.deref(); referrer.deref(); + scope.deinit(); } var errorable: JSC.ErrorableResolvedSource = undefined; @@ -487,7 +490,7 @@ pub const AsyncModule = struct { } if (e == error.JSError) { - errorable = JSC.ErrorableResolvedSource.err(error.JSError, globalThis.takeError(error.JSError).asVoid()); + errorable = JSC.ErrorableResolvedSource.err(error.JSError, globalThis.takeError(error.JSError)); } else { VirtualMachine.processFetchLog( globalThis, @@ -512,6 +515,7 @@ pub const AsyncModule = struct { &specifier, &referrer, ); + try scope.assertNoExceptionExceptTermination(); } pub fn resolveError(this: *AsyncModule, vm: *VirtualMachine, import_record_id: u32, result: PackageResolveError) !void { @@ -1616,7 +1620,7 @@ pub export fn Bun__transpileFile( var virtual_source_to_use: ?logger.Source = null; var blob_to_deinit: ?JSC.WebCore.Blob = null; var lr = options.getLoaderAndVirtualSource(_specifier.slice(), jsc_vm, &virtual_source_to_use, &blob_to_deinit, type_attribute_str) catch { - ret.* = JSC.ErrorableResolvedSource.err(error.JSErrorObject, globalObject.ERR(.MODULE_NOT_FOUND, "Blob not found", .{}).toJS().asVoid()); + ret.* = JSC.ErrorableResolvedSource.err(error.JSErrorObject, globalObject.ERR(.MODULE_NOT_FOUND, "Blob not found", .{}).toJS()); return null; }; defer if (blob_to_deinit) |*blob| blob.deinit(); @@ -1815,7 +1819,7 @@ pub export fn Bun__transpileFile( }, error.PluginError => return null, error.JSError => { - ret.* = JSC.ErrorableResolvedSource.err(error.JSError, globalObject.takeError(error.JSError).asVoid()); + ret.* = JSC.ErrorableResolvedSource.err(error.JSError, globalObject.takeError(error.JSError)); return null; }, else => { @@ -1991,7 +1995,7 @@ export fn Bun__transpileVirtualModule( switch (err) { error.PluginError => return true, error.JSError => { - ret.* = JSC.ErrorableResolvedSource.err(error.JSError, globalObject.takeError(error.JSError).asVoid()); + ret.* = JSC.ErrorableResolvedSource.err(error.JSError, globalObject.takeError(error.JSError)); return true; }, else => { @@ -2140,12 +2144,12 @@ pub const RuntimeTranspilerStore = struct { } // This is run at the top of the event loop on the JS thread. - pub fn drain(this: *RuntimeTranspilerStore) void { + pub fn drain(this: *RuntimeTranspilerStore) bun.JSExecutionTerminated!void { var batch = this.queue.popBatch(); var iter = batch.iterator(); if (iter.next()) |job| { // we run just one job first to see if there are more - job.runFromJSThread(); + try job.runFromJSThread(); } else { return; } @@ -2155,8 +2159,8 @@ pub const RuntimeTranspilerStore = struct { const jsc_vm = vm.jsc; while (iter.next()) |job| { // if there are more, we need to drain the microtasks from the previous run - event_loop.drainMicrotasksWithGlobal(global, jsc_vm); - job.runFromJSThread(); + try event_loop.drainMicrotasksWithGlobal(global, jsc_vm); + try job.runFromJSThread(); } // immediately after this is called, the microtasks will be drained again. @@ -2263,7 +2267,7 @@ pub const RuntimeTranspilerStore = struct { this.vm.eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&this.vm.transpiler_store)); } - pub fn runFromJSThread(this: *TranspilerJob) void { + pub fn runFromJSThread(this: *TranspilerJob) bun.JSExecutionTerminated!void { var vm = this.vm; const promise = this.promise.swap(); const globalThis = this.globalThis; @@ -2296,7 +2300,7 @@ pub const RuntimeTranspilerStore = struct { _ = vm.transpiler_store.store.put(this); - ModuleLoader.AsyncModule.fulfill(globalThis, promise, &resolved_source, parse_error, specifier, referrer, &log); + try ModuleLoader.AsyncModule.fulfill(globalThis, promise, &resolved_source, parse_error, specifier, referrer, &log); } pub fn schedule(this: *TranspilerJob) void { diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 10e2a063de..ac4a53f840 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -535,60 +535,80 @@ fn wrapUnhandledRejectionErrorForUncaughtException(globalObject: *JSGlobalObject return globalObject.ERR(.UNHANDLED_REJECTION, msg, .{"undefined"}).toJS(); } -pub fn unhandledRejection(this: *JSC.VirtualMachine, globalObject: *JSGlobalObject, reason: JSValue, promise: JSValue) bool { +pub fn unhandledRejection(this: *JSC.VirtualMachine, globalObject: *JSGlobalObject, reason: JSValue, promise: JSValue) void { if (this.isShuttingDown()) { Output.debugWarn("unhandledRejection during shutdown.", .{}); - return true; + return; } if (isBunTest) { this.unhandled_error_counter += 1; this.onUnhandledRejection(this, globalObject, reason); - return true; + return; } switch (this.unhandledRejectionsMode()) { .bun => { - if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return true; + if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return; // continue to default handler }, .none => { - defer this.eventLoop().drainMicrotasks(); - if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return true; - return true; // ignore the unhandled rejection + defer this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; + if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return; + return; // ignore the unhandled rejection }, .warn => { - defer this.eventLoop().drainMicrotasks(); + defer this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; _ = Bun__handleUnhandledRejection(globalObject, reason, promise); Bun__promises__emitUnhandledRejectionWarning(globalObject, reason, promise); - return true; + return; }, .warn_with_error_code => { - defer this.eventLoop().drainMicrotasks(); - if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return true; + defer this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; + if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return; Bun__promises__emitUnhandledRejectionWarning(globalObject, reason, promise); this.exit_handler.exit_code = 1; - return true; + return; }, .strict => { - defer this.eventLoop().drainMicrotasks(); + defer this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; const wrapped_reason = wrapUnhandledRejectionErrorForUncaughtException(globalObject, reason); _ = this.uncaughtException(globalObject, wrapped_reason, true); - if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return true; + if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return; Bun__promises__emitUnhandledRejectionWarning(globalObject, reason, promise); - return true; + return; }, .throw => { - defer this.eventLoop().drainMicrotasks(); - if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) return true; + if (Bun__handleUnhandledRejection(globalObject, reason, promise) > 0) { + this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; + return; + } const wrapped_reason = wrapUnhandledRejectionErrorForUncaughtException(globalObject, reason); - if (this.uncaughtException(globalObject, wrapped_reason, true)) return true; + if (this.uncaughtException(globalObject, wrapped_reason, true)) { + this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => {}, // we are returning anyway + }; + return; + } // continue to default handler + this.eventLoop().drainMicrotasks() catch |e| switch (e) { + error.JSExecutionTerminated => return, + }; }, } this.unhandled_error_counter += 1; this.onUnhandledRejection(this, globalObject, reason); - return false; + return; } pub fn handledPromise(this: *JSC.VirtualMachine, globalObject: *JSGlobalObject, promise: JSValue) bool { @@ -636,7 +656,7 @@ pub fn uncaughtException(this: *JSC.VirtualMachine, globalObject: *JSGlobalObjec pub fn handlePendingInternalPromiseRejection(this: *JSC.VirtualMachine) void { var promise = this.pending_internal_promise.?; if (promise.status(this.global.vm()) == .rejected and !promise.isHandled(this.global.vm())) { - _ = this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue()); + this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue()); promise.setHandled(this.global.vm()); } } @@ -1660,7 +1680,7 @@ pub fn resolveMaybeNeedsTrailingSlash( printed, ), }; - res.* = ErrorableString.err(error.NameTooLong, (try bun.api.ResolveMessage.create(global, VirtualMachine.get().allocator, msg, source_utf8.slice())).asVoid()); + res.* = ErrorableString.err(error.NameTooLong, (try bun.api.ResolveMessage.create(global, VirtualMachine.get().allocator, msg, source_utf8.slice()))); return; } @@ -1750,7 +1770,7 @@ pub fn resolveMaybeNeedsTrailingSlash( }; { - res.* = ErrorableString.err(err, (try bun.api.ResolveMessage.create(global, VirtualMachine.get().allocator, msg, source_utf8.slice())).asVoid()); + res.* = ErrorableString.err(err, (try bun.api.ResolveMessage.create(global, VirtualMachine.get().allocator, msg, source_utf8.slice()))); } return; @@ -1772,7 +1792,8 @@ pub export fn Bun__drainMicrotasksFromJS(globalObject: *JSGlobalObject, callfram } pub fn drainMicrotasks(this: *VirtualMachine) void { - this.eventLoop().drainMicrotasks(); + // TODO: properly propagate exception upwards + this.eventLoop().drainMicrotasks() catch {}; } pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, referrer: bun.String, log: *logger.Log, ret: *ErrorableResolvedSource, err: anyerror) void { @@ -1794,7 +1815,7 @@ pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, refer }; }; { - ret.* = ErrorableResolvedSource.err(err, (bun.api.BuildMessage.create(globalThis, globalThis.allocator(), msg) catch |e| globalThis.takeException(e)).asVoid()); + ret.* = ErrorableResolvedSource.err(err, (bun.api.BuildMessage.create(globalThis, globalThis.allocator(), msg) catch |e| globalThis.takeException(e))); } return; }, @@ -1802,13 +1823,13 @@ pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, refer 1 => { const msg = log.msgs.items[0]; ret.* = ErrorableResolvedSource.err(err, switch (msg.metadata) { - .build => (bun.api.BuildMessage.create(globalThis, globalThis.allocator(), msg) catch |e| globalThis.takeException(e)).asVoid(), + .build => (bun.api.BuildMessage.create(globalThis, globalThis.allocator(), msg) catch |e| globalThis.takeException(e)), .resolve => (bun.api.ResolveMessage.create( globalThis, globalThis.allocator(), msg, referrer.toUTF8(bun.default_allocator).slice(), - ) catch |e| globalThis.takeException(e)).asVoid(), + ) catch |e| globalThis.takeException(e)), }); return; }, @@ -1841,7 +1862,7 @@ pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, refer specifier, }) catch unreachable, ), - ).asVoid(), + ), ); }, } @@ -1910,8 +1931,7 @@ pub noinline fn runErrorHandler(this: *VirtualMachine, result: JSValue, exceptio const writer = buffered_writer.writer(); - if (result.isException(this.global.vm())) { - const exception = @as(*Exception, @ptrCast(result.asVoid())); + if (result.asException(this.jsc)) |exception| { this.printException( exception, exception_list, @@ -1987,7 +2007,7 @@ fn loadPreloads(this: *VirtualMachine) !?*JSInternalPromise { return error.ModuleNotFound; }, }; - var promise = JSModuleLoader.import(this.global, &String.fromBytes(result.path().?.text)); + var promise = try JSModuleLoader.import(this.global, &String.fromBytes(result.path().?.text)); this.pending_internal_promise = promise; JSValue.fromCell(promise).protect(); @@ -3091,7 +3111,7 @@ fn printErrorInstance( } formatter.format( - JSC.Formatter.Tag.getAdvanced( + try JSC.Formatter.Tag.getAdvanced( value, this.global, .{ .disable_inspect_custom = true, .hide_global = true }, @@ -3132,7 +3152,7 @@ fn printErrorInstance( // "cause" is not enumerable, so the above loop won't see it. if (!saw_cause) { - if (error_instance.getOwn(this.global, "cause")) |cause| { + if (try error_instance.getOwn(this.global, "cause")) |cause| { if (cause.jsType() == .ErrorInstance) { cause.protect(); try errors_to_append.append(cause); @@ -3141,7 +3161,7 @@ fn printErrorInstance( } } else if (mode == .js and error_instance != .zero) { // If you do reportError([1,2,3]] we should still show something at least. - const tag = JSC.Formatter.Tag.getAdvanced( + const tag = try JSC.Formatter.Tag.getAdvanced( error_instance, this.global, .{ .disable_inspect_custom = true, .hide_global = true }, diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 22901fde80..a84db98bc3 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -95,7 +95,7 @@ pub const BunObject = struct { fn toJSGetter(comptime getter: anytype) LazyPropertyCallback { return struct { pub fn callback(this: *JSC.JSGlobalObject, object: *JSC.JSObject) callconv(JSC.conv) JSValue { - return bun.jsc.toJSHostValue(this, getter(this, object)); + return bun.jsc.toJSHostCall(this, @src(), getter, .{ this, object }); } }.callback; } @@ -381,7 +381,7 @@ pub fn inspectTable(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) const writer = buffered_writer.writer(); const Writer = @TypeOf(writer); const properties: JSValue = if (arguments[1].jsType().isArray()) arguments[1] else .js_undefined; - var table_printer = ConsoleObject.TablePrinter.init( + var table_printer = try ConsoleObject.TablePrinter.init( globalThis, .Log, value, @@ -847,7 +847,7 @@ fn doResolveWithArgs(ctx: *JSC.JSGlobalObject, specifier: bun.String, from: bun. ); if (!errorable.success) { - return ctx.throwValue(bun.cast(JSC.C.JSValueRef, errorable.result.err.ptr.?).?.value()); + return ctx.throwValue(errorable.result.err.value); } if (query_string.len > 0) { @@ -905,7 +905,7 @@ export fn Bun__resolveSync(global: *JSGlobalObject, specifier: JSValue, source: const source_str = source.toBunString(global) catch return .zero; defer source_str.deref(); - return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source_str, is_esm, true, is_user_require_resolve)); + return JSC.toJSHostCall(global, @src(), doResolveWithArgs, .{ global, specifier_str, source_str, is_esm, true, is_user_require_resolve }); } export fn Bun__resolveSyncWithPaths( @@ -934,12 +934,12 @@ export fn Bun__resolveSyncWithPaths( bun_vm.transpiler.resolver.custom_dir_paths = paths; defer bun_vm.transpiler.resolver.custom_dir_paths = null; - return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source_str, is_esm, true, is_user_require_resolve)); + return JSC.toJSHostCall(global, @src(), doResolveWithArgs, .{ global, specifier_str, source_str, is_esm, true, is_user_require_resolve }); } export fn Bun__resolveSyncWithStrings(global: *JSGlobalObject, specifier: *bun.String, source: *bun.String, is_esm: bool) JSC.JSValue { Output.scoped(.importMetaResolve, false)("source: {s}, specifier: {s}", .{ source.*, specifier.* }); - return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier.*, source.*, is_esm, true, false)); + return JSC.toJSHostCall(global, @src(), doResolveWithArgs, .{ global, specifier.*, source.*, is_esm, true, false }); } export fn Bun__resolveSyncWithSource(global: *JSGlobalObject, specifier: JSValue, source: *bun.String, is_esm: bool, is_user_require_resolve: bool) JSC.JSValue { @@ -948,7 +948,7 @@ export fn Bun__resolveSyncWithSource(global: *JSGlobalObject, specifier: JSValue if (specifier_str.length() == 0) { return global.ERR(.INVALID_ARG_VALUE, "The argument 'id' must be a non-empty string. Received ''", .{}).throw() catch .zero; } - return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source.*, is_esm, true, is_user_require_resolve)); + return JSC.toJSHostCall(global, @src(), doResolveWithArgs, .{ global, specifier_str, source.*, is_esm, true, is_user_require_resolve }); } pub fn indexOfLine(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 66da89ea28..7ffa49672d 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -86,11 +86,11 @@ pub const JSBundler = struct { // Plugins must be resolved first as they are allowed to mutate the config JSValue if (try config.getArray(globalThis, "plugins")) |array| { - const length = array.getLength(globalThis); - var iter = array.arrayIterator(globalThis); + const length = try array.getLength(globalThis); + var iter = try array.arrayIterator(globalThis); var onstart_promise_array: JSValue = .js_undefined; var i: usize = 0; - while (iter.next()) |plugin| : (i += 1) { + while (try iter.next()) |plugin| : (i += 1) { if (!plugin.isObject()) { return globalThis.throwInvalidArguments("Expected plugin to be an object", .{}); } @@ -267,8 +267,8 @@ pub const JSBundler = struct { } if (try config.getArray(globalThis, "entrypoints") orelse try config.getArray(globalThis, "entryPoints")) |entry_points| { - var iter = entry_points.arrayIterator(globalThis); - while (iter.next()) |entry_point| { + var iter = try entry_points.arrayIterator(globalThis); + while (try iter.next()) |entry_point| { var slice = try entry_point.toSliceOrNull(globalThis); defer slice.deinit(); try this.entry_points.insert(slice.slice()); @@ -291,8 +291,8 @@ pub const JSBundler = struct { defer slice.deinit(); try this.conditions.insert(slice.slice()); } else if (conditions_value.jsType().isArray()) { - var iter = conditions_value.arrayIterator(globalThis); - while (iter.next()) |entry_point| { + var iter = try conditions_value.arrayIterator(globalThis); + while (try iter.next()) |entry_point| { var slice = try entry_point.toSliceOrNull(globalThis); defer slice.deinit(); try this.conditions.insert(slice.slice()); @@ -332,8 +332,8 @@ pub const JSBundler = struct { } if (try config.getOwnArray(globalThis, "external")) |externals| { - var iter = externals.arrayIterator(globalThis); - while (iter.next()) |entry_point| { + var iter = try externals.arrayIterator(globalThis); + while (try iter.next()) |entry_point| { var slice = try entry_point.toSliceOrNull(globalThis); defer slice.deinit(); try this.external.insert(slice.slice()); @@ -341,8 +341,8 @@ pub const JSBundler = struct { } if (try config.getOwnArray(globalThis, "drop")) |drops| { - var iter = drops.arrayIterator(globalThis); - while (iter.next()) |entry| { + var iter = try drops.arrayIterator(globalThis); + while (try iter.next()) |entry| { var slice = try entry.toSliceOrNull(globalThis); defer slice.deinit(); try this.drop.insert(slice.slice()); @@ -782,7 +782,7 @@ pub const JSBundler = struct { } export fn JSBundlerPlugin__onDefer(load: *Load, global: *JSC.JSGlobalObject) JSValue { - return JSC.toJSHostValue(global, load.onDefer(global)); + return JSC.toJSHostCall(global, @src(), Load.onDefer, .{ load, global }); } fn onDefer(this: *Load, globalObject: *JSC.JSGlobalObject) bun.JSError!JSValue { if (this.called_defer) { diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index b9ec44b4d4..b9a7dbdcd0 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -6,7 +6,6 @@ const JSC = bun.JSC; const Transpiler = bun.transpiler; const options = @import("../../options.zig"); const ZigString = JSC.ZigString; -const JSObject = JSC.JSObject; const JSValue = bun.JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; const strings = bun.strings; @@ -354,13 +353,13 @@ fn transformOptionsFromJSC(globalObject: *JSC.JSGlobalObject, temp_allocator: st single_external[0] = std.fmt.allocPrint(allocator, "{}", .{external}) catch unreachable; transpiler.transform.external = single_external; } else if (toplevel_type.isArray()) { - const count = external.getLength(globalThis); + const count = try external.getLength(globalThis); if (count == 0) break :external; var externals = allocator.alloc(string, count) catch unreachable; - var iter = external.arrayIterator(globalThis); + var iter = try external.arrayIterator(globalThis); var i: usize = 0; - while (iter.next()) |entry| { + while (try iter.next()) |entry| { if (!entry.jsType().isStringLike()) { return globalObject.throwInvalidArguments("external must be a string or string[]", .{}); } @@ -550,13 +549,13 @@ fn transformOptionsFromJSC(globalObject: *JSC.JSGlobalObject, temp_allocator: st var total_name_buf_len: u32 = 0; var string_count: u32 = 0; - const iter = JSC.JSArrayIterator.init(eliminate, globalThis); + const iter = try JSC.JSArrayIterator.init(eliminate, globalThis); { var length_iter = iter; - while (length_iter.next()) |value| { + while (try length_iter.next()) |value| { if (value.isString()) { - const length = @as(u32, @truncate(value.getLength(globalThis))); - string_count += @as(u32, @intFromBool(length > 0)); + const length: u32 = @truncate(try value.getLength(globalThis)); + string_count += @intFromBool(length > 0); total_name_buf_len += length; } } @@ -567,7 +566,7 @@ fn transformOptionsFromJSC(globalObject: *JSC.JSGlobalObject, temp_allocator: st try replacements.ensureUnusedCapacity(bun.default_allocator, string_count); { var length_iter = iter; - while (length_iter.next()) |value| { + while (try length_iter.next()) |value| { if (!value.isString()) continue; const str = try value.getZigString(globalThis); if (str.len == 0) continue; @@ -624,10 +623,10 @@ fn transformOptionsFromJSC(globalObject: *JSC.JSGlobalObject, temp_allocator: st continue; } - if (value.isObject() and value.getLength(globalObject) == 2) { - const replacementValue = JSC.JSObject.getIndex(value, globalThis, 1); + if (value.isObject() and try value.getLength(globalObject) == 2) { + const replacementValue = try value.getIndex(globalThis, 1); if (try exportReplacementValue(replacementValue, globalThis)) |to_replace| { - const replacementKey = JSC.JSObject.getIndex(value, globalThis, 0); + const replacementKey = try value.getIndex(globalThis, 0); var slice = (try (try replacementKey.toSlice(globalThis, bun.default_allocator)).cloneIfNeeded(bun.default_allocator)); const replacement_name = slice.slice(); diff --git a/src/bun.js/api/Timer/TimerObjectInternals.zig b/src/bun.js/api/Timer/TimerObjectInternals.zig index cc3db48996..60e3c88cba 100644 --- a/src/bun.js/api/Timer/TimerObjectInternals.zig +++ b/src/bun.js/api/Timer/TimerObjectInternals.zig @@ -96,7 +96,7 @@ pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool { this.deref(); } - vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown); + vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown) catch return true; return exception_thrown; } diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index f7bfc86f29..4fe7437c67 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -3303,7 +3303,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("setServers", "servers", "array"); } - var triplesIterator = argument.arrayIterator(globalThis); + var triplesIterator = try argument.arrayIterator(globalThis); if (triplesIterator.len == 0) { const r = c_ares.ares_set_servers_ports(channel, null); @@ -3321,19 +3321,19 @@ pub const DNSResolver = struct { var i: u32 = 0; - while (triplesIterator.next()) |triple| : (i += 1) { + while (try triplesIterator.next()) |triple| : (i += 1) { if (!triple.isArray()) { return globalThis.throwInvalidArgumentType("setServers", "triple", "array"); } - const family = JSValue.getIndex(triple, globalThis, 0).toInt32(); - const port = JSValue.getIndex(triple, globalThis, 2).toInt32(); + const family = (try triple.getIndex(globalThis, 0)).toInt32(); + const port = (try triple.getIndex(globalThis, 2)).toInt32(); if (family != 4 and family != 6) { return globalThis.throwInvalidArguments("Invalid address family", .{}); } - const addressString = try JSValue.getIndex(triple, globalThis, 1).toBunString(globalThis); + const addressString = try (try triple.getIndex(globalThis, 1)).toBunString(globalThis); defer addressString.deref(); const addressSlice = try addressString.toOwnedSlice(allocator); diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 5e6e4305d7..9009855bae 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -2878,9 +2878,9 @@ pub const H2FrameParser = struct { var stream = std.io.fixedBufferStream(&buffer); const writer = stream.writer(); stream.seekTo(FrameHeader.byteSize) catch {}; - var value_iter = origin_arg.arrayIterator(globalObject); + var value_iter = try origin_arg.arrayIterator(globalObject); - while (value_iter.next()) |item| { + while (try value_iter.next()) |item| { if (!item.isString()) { return globalObject.throwInvalidArguments("Expected origin to be a string or an array of strings", .{}); } @@ -3446,7 +3446,7 @@ pub const H2FrameParser = struct { if (js_value.jsType().isArray()) { // https://github.com/oven-sh/bun/issues/8940 - var value_iter = js_value.arrayIterator(globalObject); + var value_iter = try js_value.arrayIterator(globalObject); if (SingleValueHeaders.indexOf(validated_name)) |idx| { if (value_iter.len > 1 or single_value_headers[idx]) { @@ -3456,7 +3456,7 @@ pub const H2FrameParser = struct { single_value_headers[idx] = true; } - while (value_iter.next()) |item| { + while (try value_iter.next()) |item| { if (item.isEmptyOrUndefinedOrNull()) { const exception = globalObject.toTypeError(.HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{validated_name}); return globalObject.throwValue(exception); @@ -3889,7 +3889,7 @@ pub const H2FrameParser = struct { if (js_value.jsType().isArray()) { log("array header {s}", .{name}); // https://github.com/oven-sh/bun/issues/8940 - var value_iter = js_value.arrayIterator(globalObject); + var value_iter = try js_value.arrayIterator(globalObject); if (SingleValueHeaders.indexOf(validated_name)) |idx| { if (value_iter.len > 1 or single_value_headers[idx]) { @@ -3902,7 +3902,7 @@ pub const H2FrameParser = struct { single_value_headers[idx] = true; } - while (value_iter.next()) |item| { + while (try value_iter.next()) |item| { if (item.isEmptyOrUndefinedOrNull()) { if (!globalObject.hasException()) { return globalObject.ERR(.HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{validated_name}).throw(); diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index fcb4307457..0e25964198 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1827,7 +1827,7 @@ fn getArgv0(globalThis: *JSC.JSGlobalObject, PATH: []const u8, cwd: []const u8, } fn getArgv(globalThis: *JSC.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { - var cmds_array = args.arrayIterator(globalThis); + var cmds_array = try args.arrayIterator(globalThis); // + 1 for argv0 // + 1 for null terminator argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); @@ -1840,12 +1840,12 @@ fn getArgv(globalThis: *JSC.JSGlobalObject, args: JSValue, PATH: []const u8, cwd return globalThis.throwInvalidArguments("cmd must not be empty", .{}); } - const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, cmds_array.next().?, allocator); + const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator); argv0.* = argv0_result.argv0.ptr; argv.appendAssumeCapacity(argv0_result.arg0.ptr); - while (cmds_array.next()) |value| { + while (try cmds_array.next()) |value| { const arg = try value.toBunString(globalThis); defer arg.deref(); @@ -2031,16 +2031,16 @@ pub fn spawnMaybeSync( if (try args.get(globalThis, "stdio")) |stdio_val| { if (!stdio_val.isEmptyOrUndefinedOrNull()) { if (stdio_val.jsType().isArray()) { - var stdio_iter = stdio_val.arrayIterator(globalThis); + var stdio_iter = try stdio_val.arrayIterator(globalThis); var i: u31 = 0; - while (stdio_iter.next()) |value| : (i += 1) { + while (try stdio_iter.next()) |value| : (i += 1) { try stdio[i].extract(globalThis, i, value); if (i == 2) break; } i += 1; - while (stdio_iter.next()) |value| : (i += 1) { + while (try stdio_iter.next()) |value| : (i += 1) { var new_item: Stdio = undefined; try new_item.extract(globalThis, i, value); diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index ca7f196f4f..d6ae1e3765 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -608,7 +608,7 @@ pub const UDPSocket = struct { return globalThis.throwInvalidArgumentType("sendMany", "first argument", "array"); } - const array_len = arg.getLength(globalThis); + const array_len = try arg.getLength(globalThis); if (this.connect_info == null and array_len % 3 != 0) { return globalThis.throwInvalidArguments("Expected 3 arguments for each packet", .{}); } @@ -624,11 +624,11 @@ pub const UDPSocket = struct { var addr_ptrs = alloc.alloc(?*const anyopaque, len) catch bun.outOfMemory(); var addrs = alloc.alloc(std.posix.sockaddr.storage, len) catch bun.outOfMemory(); - var iter = arg.arrayIterator(globalThis); + var iter = try arg.arrayIterator(globalThis); var i: u16 = 0; var port: JSValue = .zero; - while (iter.next()) |val| : (i += 1) { + while (try iter.next()) |val| : (i += 1) { if (i >= array_len) { return globalThis.throwInvalidArguments("Mismatch between array length property and number of items", .{}); } diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index ff6e0075ad..1297c2cdfe 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -538,10 +538,10 @@ pub const FFI = struct { } pub fn fromJSArray(globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, comptime property: []const u8) bun.JSError!StringArray { - var iter = value.arrayIterator(globalThis); + var iter = try value.arrayIterator(globalThis); var items = std.ArrayList([:0]const u8).init(bun.default_allocator); - while (iter.next()) |val| { + while (try iter.next()) |val| { if (!val.isString()) { for (items.items) |item| { bun.default_allocator.free(@constCast(item)); @@ -595,7 +595,7 @@ pub const FFI = struct { } } - const symbols_object: JSValue = object.getOwn(globalThis, "symbols") orelse .js_undefined; + const symbols_object: JSValue = try object.getOwn(globalThis, "symbols") orelse .js_undefined; if (!globalThis.hasException() and (symbols_object == .zero or !symbols_object.isObject())) { return globalThis.throwInvalidArgumentTypeValue("symbols", "object", symbols_object); } @@ -615,7 +615,7 @@ pub const FFI = struct { return globalThis.throw("Expected at least one exported symbol", .{}); } - if (object.getOwn(globalThis, "library")) |library_value| { + if (try object.getOwn(globalThis, "library")) |library_value| { compile_c.libraries = try StringArray.fromJS(globalThis, library_value, "library"); } @@ -625,13 +625,13 @@ pub const FFI = struct { if (try object.getTruthy(globalThis, "flags")) |flags_value| { if (flags_value.isArray()) { - var iter = flags_value.arrayIterator(globalThis); + var iter = try flags_value.arrayIterator(globalThis); var flags = std.ArrayList(u8).init(allocator); defer flags.deinit(); flags.appendSlice(CompileC.default_tcc_options) catch bun.outOfMemory(); - while (iter.next()) |value| { + while (try iter.next()) |value| { if (!value.isString()) { return globalThis.throwInvalidArgumentTypeValue("flags", "array of strings", value); } @@ -698,11 +698,11 @@ pub const FFI = struct { return error.JSError; } - if (object.getOwn(globalThis, "source")) |source_value| { + if (try object.getOwn(globalThis, "source")) |source_value| { if (source_value.isArray()) { compile_c.source = .{ .files = .{} }; - var iter = source_value.arrayIterator(globalThis); - while (iter.next()) |value| { + var iter = try source_value.arrayIterator(globalThis); + while (try iter.next()) |value| { if (!value.isString()) { return globalThis.throwInvalidArgumentTypeValue("source", "array of strings", value); } @@ -1249,15 +1249,15 @@ pub const FFI = struct { var abi_types = std.ArrayListUnmanaged(ABIType){}; - if (value.getOwn(global, "args")) |args| { + if (try value.getOwn(global, "args")) |args| { if (args.isEmptyOrUndefinedOrNull() or !args.jsType().isArray()) { return ZigString.static("Expected an object with \"args\" as an array").toErrorInstance(global); } - var array = args.arrayIterator(global); + var array = try args.arrayIterator(global); try abi_types.ensureTotalCapacityPrecise(allocator, array.len); - while (array.next()) |val| { + while (try array.next()) |val| { if (val.isEmptyOrUndefinedOrNull()) { abi_types.clearAndFree(allocator); return ZigString.static("param must be a string (type name) or number").toErrorInstance(global); diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig index a0e4894a62..05521e5283 100644 --- a/src/bun.js/api/filesystem_router.zig +++ b/src/bun.js/api/filesystem_router.zig @@ -99,16 +99,16 @@ pub const FileSystemRouter = struct { return globalThis.throwInvalidArguments("Expected fileExtensions to be an Array", .{}); } - var iter = file_extensions.arrayIterator(globalThis); + var iter = try file_extensions.arrayIterator(globalThis); extensions.ensureTotalCapacityPrecise(iter.len) catch unreachable; - while (iter.next()) |val| { + while (try iter.next()) |val| { if (!val.isString()) { origin_str.deinit(); arena.deinit(); globalThis.allocator().destroy(arena); return globalThis.throwInvalidArguments("Expected fileExtensions to be an Array of strings", .{}); } - if (val.getLength(globalThis) == 0) continue; + if (try val.getLength(globalThis) == 0) continue; extensions.appendAssumeCapacity(((try val.toSlice(globalThis, allocator)).clone(allocator) catch unreachable).slice()[1..]); } } diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index c5ef322882..67fd3d3b8c 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -923,7 +923,7 @@ fn HandlerCallback( this.global.bunVM().waitForPromise(promise); const fail = promise.status(this.global.vm()) == .rejected; if (fail) { - _ = this.global.bunVM().unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue()); + this.global.bunVM().unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue()); } return fail; } diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index 5ed7eecbdf..bbe7cd5b29 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -252,13 +252,13 @@ pub fn fromJS(vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, obj: JSC.JSV if (try obj.getTruthy(global, "key")) |js_obj| { if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); + const count = try js_obj.getLength(global); if (count > 0) { const native_array = try bun.default_allocator.alloc([*c]const u8, count); var valid_count: u32 = 0; for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); + const item = try js_obj.getIndex(global, @intCast(i)); if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { defer sb.deinit(); const sliced = sb.slice(); @@ -360,13 +360,13 @@ pub fn fromJS(vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, obj: JSC.JSV if (try obj.getTruthy(global, "cert")) |js_obj| { if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); + const count = try js_obj.getLength(global); if (count > 0) { const native_array = try bun.default_allocator.alloc([*c]const u8, count); var valid_count: u32 = 0; for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); + const item = try js_obj.getIndex(global, @intCast(i)); if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { defer sb.deinit(); const sliced = sb.slice(); @@ -469,13 +469,13 @@ pub fn fromJS(vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, obj: JSC.JSV if (try obj.getTruthy(global, "ca")) |js_obj| { if (js_obj.jsType().isArray()) { - const count = js_obj.getLength(global); + const count = try js_obj.getLength(global); if (count > 0) { const native_array = try bun.default_allocator.alloc([*c]const u8, count); var valid_count: u32 = 0; for (0..count) |i| { - const item = js_obj.getIndex(global, @intCast(i)); + const item = try js_obj.getIndex(global, @intCast(i)); if (try JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { defer sb.deinit(); const sliced = sb.slice(); diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 88a3effc7b..964b08b3c9 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -579,7 +579,7 @@ pub fn fromJS( }; var found = false; inline for (methods) |method| { - if (value.getOwn(global, @tagName(method))) |function| { + if (try value.getOwn(global, @tagName(method))) |function| { if (!found) { try validateRouteName(global, path); } @@ -911,11 +911,11 @@ pub fn fromJS( if (tls.isFalsey()) { args.ssl_config = null; } else if (tls.jsType().isArray()) { - var value_iter = tls.arrayIterator(global); + var value_iter = try tls.arrayIterator(global); if (value_iter.len == 1) { return global.throwInvalidArguments("tls option expects at least 1 tls object", .{}); } - while (value_iter.next()) |item| { + while (try value_iter.next()) |item| { var ssl_config = try SSLConfig.fromJS(vm, global, item) orelse { if (global.hasException()) { return error.JSError; diff --git a/src/bun.js/bindings/AnyPromise.zig b/src/bun.js/bindings/AnyPromise.zig index 4dcce82733..9d12999dee 100644 --- a/src/bun.js/bindings/AnyPromise.zig +++ b/src/bun.js/bindings/AnyPromise.zig @@ -76,11 +76,15 @@ pub const AnyPromise = union(enum) { args: Args, pub fn call(wrap_: *@This(), global: *JSC.JSGlobalObject) callconv(.c) JSC.JSValue { - return JSC.toJSHostValue(global, @call(.auto, Fn, wrap_.args)); + return JSC.toJSHostCall(global, @src(), Fn, wrap_.args); } }; + var scope: JSC.CatchScope = undefined; + scope.init(globalObject, @src(), .enabled); + defer scope.deinit(); var ctx = Wrapper{ .args = args }; JSC__AnyPromise__wrap(globalObject, this.asValue(), &ctx, @ptrCast(&Wrapper.call)); + bun.debugAssert(!scope.hasException()); // TODO: properly propagate exception upwards } }; diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 8f24d751b3..82a4e54728 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -359,6 +359,7 @@ static JSValue constructBunShell(VM& vm, JSObject* bunObject) auto* bunShell = shell.getObject(); auto ShellError = bunShell->get(globalObject, JSC::Identifier::fromString(vm, "ShellError"_s)); + RETURN_IF_EXCEPTION(scope, {}); if (!ShellError.isObject()) [[unlikely]] { throwTypeError(globalObject, scope, "Internal error: BunShell.ShellError is not an object"_s); return {}; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 01e538bb30..5e313628ff 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -2062,7 +2062,6 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb JSC_DEFINE_HOST_FUNCTION(Process_functionGetReport, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); // TODO: node:vm return JSValue::encode(constructReportObjectComplete(vm, jsCast(globalObject), String())); } diff --git a/src/bun.js/bindings/CatchScope.zig b/src/bun.js/bindings/CatchScope.zig new file mode 100644 index 0000000000..bcd98f5480 --- /dev/null +++ b/src/bun.js/bindings/CatchScope.zig @@ -0,0 +1,173 @@ +//! Binding for JSC::CatchScope. This should be used rarely, only at translation boundaries between +//! JSC's exception checking and Zig's. Make sure not to move it after creation. For instance: +//! +//! ```zig +//! // Declare a CatchScope surrounding the call that may throw an exception +//! var scope: CatchScope = undefined; +//! scope.init(global, @src(), .assertions_only); +//! defer scope.deinit(); +//! +//! const value = external_call(vm, foo, bar, baz); +//! // Calling hasException() suffices to prove that we checked for an exception. +//! // This function's caller does not need to use a CatchScope or ThrowScope +//! // because it can use Zig error unions. +//! if (Environment.allow_assert) assert((value == .zero) == scope.hasException()); +//! return if (value == .zero) error.JSError else value; +//! ``` + +const CatchScope = @This(); + +/// TODO determine size and alignment automatically +const size = 56; +const alignment = 8; + +bytes: [size]u8 align(alignment), +global: *jsc.JSGlobalObject, +/// Pointer to `bytes`, set by `init()`, used to assert that the location did not change +location: if (Environment.allow_assert) *u8 else void, +enabled: bool, + +pub const Enable = enum { + /// You are using the CatchScope to check for exceptions. + enabled, + /// You have another way to detect exceptions and are only using the CatchScope to prove that + /// exceptions are checked. + /// + /// This CatchScope will only do anything when assertions are enabled. Otherwise, init and + /// deinit do nothing and it always reports there is no exception. + assertions_only, +}; + +pub fn init( + self: *CatchScope, + global: *jsc.JSGlobalObject, + src: std.builtin.SourceLocation, + /// If not enabled, the scope does nothing (it never has an exception). + /// If you need to do something different when there is an exception, leave enabled. + /// If you are only using the scope to prove you handle exceptions correctly, you can pass + /// `Environment.allow_assert` as `enabled`. + enable_condition: Enable, +) void { + const enabled = switch (enable_condition) { + .enabled => true, + .assertions_only => Environment.allow_assert, + }; + if (enabled) { + CatchScope__construct( + &self.bytes, + global, + src.fn_name, + src.file, + src.line, + @sizeOf(@TypeOf(self.bytes)), + @typeInfo(CatchScope).@"struct".fields[0].alignment, + ); + } + self.* = .{ + .bytes = self.bytes, + .global = global, + .location = if (Environment.allow_assert) &self.bytes[0], + .enabled = enabled, + }; +} + +/// Generate a useful message including where the exception was thrown. +/// Only intended to be called when there is a pending exception. +fn assertionFailure(self: *CatchScope, proof: *jsc.Exception) noreturn { + _ = proof; + if (Environment.allow_assert) bun.assert(self.location == &self.bytes[0]); + CatchScope__assertNoException(&self.bytes); + @panic("assertionFailure called without a pending exception"); +} + +pub fn hasException(self: *CatchScope) bool { + return self.exception() != null; +} + +/// Get the thrown exception if it exists (like scope.exception() in C++) +pub fn exception(self: *CatchScope) ?*jsc.Exception { + if (Environment.allow_assert) bun.assert(self.location == &self.bytes[0]); + if (!self.enabled) return null; + return CatchScope__pureException(&self.bytes); +} + +/// Get the thrown exception if it exists, or if an unhandled trap causes an exception to be thrown +pub fn exceptionIncludingTraps(self: *CatchScope) ?*jsc.Exception { + if (Environment.allow_assert) bun.assert(self.location == &self.bytes[0]); + if (!self.enabled) return null; + return CatchScope__exceptionIncludingTraps(&self.bytes); +} + +/// Intended for use with `try`. Returns if there is already a pending exception or if traps cause +/// an exception to be thrown (this is the same as how RETURN_IF_EXCEPTION behaves in C++) +pub fn returnIfException(self: *CatchScope) bun.JSError!void { + if (self.exceptionIncludingTraps() != null) return error.JSError; +} + +/// Asserts there has not been any exception thrown. +pub fn assertNoException(self: *CatchScope) void { + if (Environment.allow_assert) { + if (self.exception()) |e| self.assertionFailure(e); + } +} + +/// Asserts that there is or is not an exception according to the value of `should_have_exception`. +/// Prefer over `assert(scope.hasException() == ...)` because if there is an unexpected exception, +/// this function prints a trace of where it was thrown. +pub fn assertExceptionPresenceMatches(self: *CatchScope, should_have_exception: bool) void { + if (Environment.allow_assert) { + // paranoid; will only fail if you manually changed enabled to false + bun.assert(self.enabled); + if (should_have_exception) { + bun.assertf(self.hasException(), "Expected an exception to be thrown", .{}); + } else { + self.assertNoException(); + } + } +} + +/// If no exception, returns. +/// If termination exception, returns JSExecutionTerminated (so you can `try`) +/// If non-termination exception, assertion failure. +pub fn assertNoExceptionExceptTermination(self: *CatchScope) bun.JSExecutionTerminated!void { + bun.assert(self.enabled); + return if (self.exception()) |e| + if (jsc.JSValue.fromCell(e).isTerminationException(self.global.vm())) + error.JSExecutionTerminated + else if (Environment.allow_assert) + self.assertionFailure(e) + else + // we got an exception other than the termination one, but we can't assert. + // treat this like the termination exception so we still bail out + error.JSExecutionTerminated + else {}; +} + +pub fn deinit(self: *CatchScope) void { + if (Environment.allow_assert) bun.assert(self.location == &self.bytes[0]); + if (!self.enabled) return; + CatchScope__destruct(&self.bytes); + self.bytes = undefined; +} + +extern fn CatchScope__construct( + ptr: *align(alignment) [size]u8, + global: *jsc.JSGlobalObject, + function: [*:0]const u8, + file: [*:0]const u8, + line: c_uint, + size: usize, + alignment: usize, +) void; +/// only returns exceptions that have already been thrown. does not check traps +extern fn CatchScope__pureException(ptr: *align(alignment) [size]u8) ?*jsc.Exception; +/// returns if an exception was already thrown, or if a trap (like another thread requesting +/// termination) causes an exception to be thrown +extern fn CatchScope__exceptionIncludingTraps(ptr: *align(alignment) [size]u8) ?*jsc.Exception; +extern fn CatchScope__assertNoException(ptr: *align(alignment) [size]u8) void; +extern fn CatchScope__destruct(ptr: *align(alignment) [size]u8) void; + +const std = @import("std"); +const bun = @import("bun"); +const jsc = bun.jsc; +const Environment = bun.Environment; diff --git a/src/bun.js/bindings/CatchScopeBinding.cpp b/src/bun.js/bindings/CatchScopeBinding.cpp new file mode 100644 index 0000000000..3efe668735 --- /dev/null +++ b/src/bun.js/bindings/CatchScopeBinding.cpp @@ -0,0 +1,57 @@ +#include + +using JSC::CatchScope; + +extern "C" void CatchScope__construct( + void* ptr, + JSC::JSGlobalObject* globalObject, + const char* function, + const char* file, + unsigned line, + size_t size, + size_t alignment) +{ + // validate that Zig is correct about what the size and alignment should be + ASSERT(size >= sizeof(CatchScope)); + ASSERT(alignment >= alignof(CatchScope)); + ASSERT((uintptr_t)ptr % alignment == 0); + +#if ENABLE(EXCEPTION_SCOPE_VERIFICATION) + new (ptr) JSC::CatchScope(JSC::getVM(globalObject), + JSC::ExceptionEventLocation { currentStackPointer(), function, file, line }); +#else + (void)function; + (void)file; + (void)line; + new (ptr) JSC::CatchScope(JSC::getVM(globalObject)); +#endif +} + +extern "C" JSC::Exception* CatchScope__pureException(void* ptr) +{ + ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); + return static_cast(ptr)->exception(); +} + +extern "C" JSC::Exception* CatchScope__exceptionIncludingTraps(void* ptr) +{ + ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); + auto* scope = static_cast(ptr); + // this is different than `return scope->exception()` because `RETURN_IF_EXCEPTION` also checks + // if there are traps that should throw an exception (like a termination request from another + // thread) + RETURN_IF_EXCEPTION(*scope, scope->exception()); + return nullptr; +} + +extern "C" void CatchScope__destruct(void* ptr) +{ + ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); + static_cast(ptr)->~CatchScope(); +} + +extern "C" void CatchScope__assertNoException(void* ptr) +{ + ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); + static_cast(ptr)->assertNoException(); +} diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 168a98f3e1..78d62a50bf 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -189,6 +189,7 @@ static Structure* createErrorStructure(JSC::VM& vm, JSGlobalObject* globalObject JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options) { + auto scope = DECLARE_CATCH_SCOPE(vm); auto* cache = errorCache(globalObject); const auto& data = errors[static_cast(code)]; if (!cache->internalField(static_cast(code))) { @@ -197,7 +198,15 @@ JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, E } auto* structure = jsCast(cache->internalField(static_cast(code)).get()); - return JSC::ErrorInstance::create(globalObject, structure, message, options, nullptr, JSC::RuntimeType::TypeNothing, data.type, true); + auto* created_error = JSC::ErrorInstance::create(globalObject, structure, message, options, nullptr, JSC::RuntimeType::TypeNothing, data.type, true); + if (auto* thrown_exception = scope.exception()) [[unlikely]] { + scope.clearException(); + // TODO investigate what can throw here and whether it will throw non-objects + // (this is better than before where we would have returned nullptr from createError if any + // exception were thrown by ErrorInstance::create) + return jsCast(thrown_exception->value()); + } + return created_error; } JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, const String& message) diff --git a/src/bun.js/bindings/Errorable.zig b/src/bun.js/bindings/Errorable.zig index 2e27290c40..6bcbe25d48 100644 --- a/src/bun.js/bindings/Errorable.zig +++ b/src/bun.js/bindings/Errorable.zig @@ -1,3 +1,4 @@ +const bun = @import("bun"); const ZigErrorType = @import("ZigErrorType.zig").ZigErrorType; const ErrorCode = @import("ErrorCode.zig").ErrorCode; @@ -27,12 +28,12 @@ pub fn Errorable(comptime Type: type) type { return @This(){ .result = .{ .value = val }, .success = true }; } - pub fn err(code: anyerror, ptr: *anyopaque) @This() { + pub fn err(code: anyerror, err_value: bun.jsc.JSValue) @This() { return @This(){ .result = .{ .err = .{ .code = ErrorCode.from(code), - .ptr = ptr, + .value = err_value, }, }, .success = false, diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 814126bd1e..f670fc76f1 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -541,7 +541,8 @@ JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_require, (JSGlobalObject * glo if (!thisObject) [[unlikely]] return JSValue::encode(jsUndefined()); - return JSValue::encode(thisObject->requireProperty.getInitializedOnMainThread(thisObject)); + auto* nullable = thisObject->requireProperty.getInitializedOnMainThread(thisObject); + return JSValue::encode(nullable ? nullable : jsUndefined()); } // https://github.com/oven-sh/bun/issues/11754#issuecomment-2452626172 @@ -651,6 +652,7 @@ void ImportMetaObject::finishCreation(VM& vm) ASSERT(inherits(info())); this->requireProperty.initLater([](const JSC::LazyProperty::Initializer& init) { + auto scope = DECLARE_THROW_SCOPE(init.vm); ImportMetaObject* meta = jsCast(init.owner); WTF::URL url = isAbsolutePath(meta->url) ? WTF::URL::fileURLWithFileSystemPath(meta->url) : WTF::URL(meta->url); @@ -666,8 +668,10 @@ void ImportMetaObject::finishCreation(VM& vm) path = meta->url; } - JSFunction* value = jsCast(Bun::JSCommonJSModule::createBoundRequireFunction(init.vm, meta->globalObject(), path)); - init.set(value); + auto* object = Bun::JSCommonJSModule::createBoundRequireFunction(init.vm, meta->globalObject(), path); + RETURN_IF_EXCEPTION(scope, ); + ASSERT(object); + init.set(jsCast(object)); }); this->urlProperty.initLater([](const JSC::LazyProperty::Initializer& init) { ImportMetaObject* meta = jsCast(init.owner); diff --git a/src/bun.js/bindings/JSArray.zig b/src/bun.js/bindings/JSArray.zig index 1f5d23c385..62662ae309 100644 --- a/src/bun.js/bindings/JSArray.zig +++ b/src/bun.js/bindings/JSArray.zig @@ -9,16 +9,16 @@ pub const JSArray = opaque { extern fn JSArray__constructArray(*JSGlobalObject, [*]const JSValue, usize) JSValue; pub fn create(global: *JSGlobalObject, items: []const JSValue) bun.JSError!JSValue { - return bun.jsc.fromJSHostValue(JSArray__constructArray(global, items.ptr, items.len)); + return bun.jsc.fromJSHostCall(global, @src(), JSArray__constructArray, .{ global, items.ptr, items.len }); } extern fn JSArray__constructEmptyArray(*JSGlobalObject, usize) JSValue; pub fn createEmpty(global: *JSGlobalObject, len: usize) bun.JSError!JSValue { - return bun.jsc.fromJSHostValue(JSArray__constructEmptyArray(global, len)); + return bun.jsc.fromJSHostCall(global, @src(), JSArray__constructEmptyArray, .{ global, len }); } - pub fn iterator(array: *JSArray, global: *JSGlobalObject) JSArrayIterator { + pub fn iterator(array: *JSArray, global: *JSGlobalObject) bun.JSError!JSArrayIterator { return JSValue.fromCell(array).arrayIterator(global); } }; diff --git a/src/bun.js/bindings/JSArrayIterator.zig b/src/bun.js/bindings/JSArrayIterator.zig index 315171fe63..6ff2beb691 100644 --- a/src/bun.js/bindings/JSArrayIterator.zig +++ b/src/bun.js/bindings/JSArrayIterator.zig @@ -10,21 +10,20 @@ pub const JSArrayIterator = struct { array: JSValue, global: *JSGlobalObject, - pub fn init(value: JSValue, global: *JSGlobalObject) JSArrayIterator { + pub fn init(value: JSValue, global: *JSGlobalObject) bun.JSError!JSArrayIterator { return .{ .array = value, .global = global, - .len = @as(u32, @truncate(value.getLength(global))), + .len = @truncate(try value.getLength(global)), }; } - // TODO: this can throw - pub fn next(this: *JSArrayIterator) ?JSValue { + pub fn next(this: *JSArrayIterator) bun.JSError!?JSValue { if (!(this.i < this.len)) { return null; } const i = this.i; this.i += 1; - return JSObject.getIndex(this.array, this.global, i); + return try JSObject.getIndex(this.array, this.global, i); } }; diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index e32794b288..5971a32a20 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -219,21 +219,15 @@ std::optional byteLength(JSC::JSString* str, JSC::JSGlobalObject* lexica static JSUint8Array* allocBuffer(JSC::JSGlobalObject* lexicalGlobalObject, size_t byteLength) { -#if ASSERT_ENABLED auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); -#endif auto* globalObject = defaultGlobalObject(lexicalGlobalObject); auto* subclassStructure = globalObject->JSBufferSubclassStructure(); auto* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, byteLength); -#if ASSERT_ENABLED - if (!uint8Array) [[unlikely]] { - // it should have thrown an exception already - ASSERT(throwScope.exception()); - } -#endif + // it should have thrown an exception already + ASSERT(!!throwScope.exception() == !uint8Array); return uint8Array; } @@ -241,19 +235,13 @@ static JSUint8Array* allocBuffer(JSC::JSGlobalObject* lexicalGlobalObject, size_ static JSUint8Array* allocBufferUnsafe(JSC::JSGlobalObject* lexicalGlobalObject, size_t byteLength) { -#if ASSERT_ENABLED auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); -#endif auto* result = createUninitializedBuffer(lexicalGlobalObject, byteLength); -#if ASSERT_ENABLED - if (!result) [[unlikely]] { - // it should have thrown an exception already - ASSERT(throwScope.exception()); - } -#endif + // it should have thrown an exception already + ASSERT(!!throwScope.exception() == !result); return result; } @@ -566,6 +554,7 @@ JSC::EncodedJSValue constructFromEncoding(JSGlobalObject* lexicalGlobalObject, W } } } + RETURN_IF_EXCEPTION(scope, {}); JSC::JSValue decoded = JSC::JSValue::decode(result); if (!result) [[unlikely]] { @@ -606,7 +595,7 @@ static JSC::EncodedJSValue constructBufferFromStringAndEncoding(JSC::JSGlobalObj } if (str->length() == 0) - return constructBufferEmpty(lexicalGlobalObject); + RELEASE_AND_RETURN(scope, constructBufferEmpty(lexicalGlobalObject)); JSC::EncodedJSValue result = constructFromEncoding(lexicalGlobalObject, view, encoding); @@ -626,7 +615,7 @@ static JSC::EncodedJSValue jsBufferConstructorFunction_allocBody(JSC::JSGlobalOb RETURN_IF_EXCEPTION(scope, {}); if (length == 0) { - return JSValue::encode(createEmptyBuffer(lexicalGlobalObject)); + RELEASE_AND_RETURN(scope, JSValue::encode(createEmptyBuffer(lexicalGlobalObject))); } // fill argument if (callFrame->argumentCount() > 1) [[unlikely]] { diff --git a/src/bun.js/bindings/JSCommonJSModule.cpp b/src/bun.js/bindings/JSCommonJSModule.cpp index 4739efe53c..4c4a0468ae 100644 --- a/src/bun.js/bindings/JSCommonJSModule.cpp +++ b/src/bun.js/bindings/JSCommonJSModule.cpp @@ -148,7 +148,9 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj // Using same approach as node, `arguments` in the entry point isn't defined // https://github.com/nodejs/node/blob/592c6907bfe1922f36240e9df076be1864c3d1bd/lib/internal/process/execution.js#L92 - globalObject->putDirect(vm, builtinNames(vm).exportsPublicName(), moduleObject->exportsObject(), 0); + auto exports = moduleObject->exportsObject(); + RETURN_IF_EXCEPTION(scope, {}); + globalObject->putDirect(vm, builtinNames(vm).exportsPublicName(), exports, 0); globalObject->putDirect(vm, builtinNames(vm).requirePublicName(), requireFunction, 0); globalObject->putDirect(vm, Identifier::fromString(vm, "module"_s), moduleObject, 0); globalObject->putDirect(vm, Identifier::fromString(vm, "__filename"_s), filename, 0); @@ -183,7 +185,9 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj RETURN_IF_EXCEPTION(scope, false); MarkedArgumentBuffer args; - args.append(moduleObject->exportsObject()); // exports + auto exports = moduleObject->exportsObject(); + RETURN_IF_EXCEPTION(scope, false); + args.append(exports); // exports args.append(requireFunction); // require args.append(moduleObject); // module args.append(filename); // filename @@ -1126,7 +1130,9 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, Vector& exportNames, JSC::MarkedArgumentBuffer& exportValues) { + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); auto result = this->exportsObject(); + RETURN_IF_EXCEPTION(scope, ); populateESMExports(globalObject, result, exportNames, exportValues, this->ignoreESModuleAnnotation); } @@ -1305,7 +1311,9 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireNativeModule, (JSGlobalObject * lexica res.success = false; memset(&res.result, 0, sizeof res.result); BunString specifierStr = Bun::toString(specifier); - if (auto result = fetchBuiltinModuleWithoutResolution(globalObject, &specifierStr, &res)) { + auto result = fetchBuiltinModuleWithoutResolution(globalObject, &specifierStr, &res); + RETURN_IF_EXCEPTION(throwScope, {}); + if (result) { if (res.success) return JSC::JSValue::encode(result); } @@ -1411,10 +1419,13 @@ std::optional createCommonJSModule( ResolvedSource& source, bool isBuiltIn) { + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); JSCommonJSModule* moduleObject = nullptr; WTF::String sourceURL = source.source_url.toWTFString(); JSValue entry = globalObject->requireMap()->get(globalObject, requireMapKey); + RETURN_IF_EXCEPTION(scope, {}); bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; SourceOrigin sourceOrigin; @@ -1423,12 +1434,12 @@ std::optional createCommonJSModule( } if (!moduleObject) { - VM& vm = JSC::getVM(globalObject); size_t index = sourceURL.reverseFind(PLATFORM_SEP, sourceURL.length()); JSString* dirname; JSString* filename = requireMapKey; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, requireMapKey, 0, index); + RETURN_IF_EXCEPTION(scope, {}); } else { dirname = jsEmptyString(vm); } @@ -1458,6 +1469,7 @@ std::optional createCommonJSModule( JSC::constructEmptyObject(globalObject, globalObject->objectPrototype()), 0); requireMap->set(globalObject, filename, moduleObject); + RETURN_IF_EXCEPTION(scope, {}); } else { sourceOrigin = Zig::toSourceOrigin(sourceURL, isBuiltIn); } @@ -1472,14 +1484,15 @@ std::optional createCommonJSModule( JSC::MarkedArgumentBuffer& exportValues) -> void { auto* globalObject = jsCast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); JSValue keyValue = identifierToJSValue(vm, moduleKey); JSValue entry = globalObject->requireMap()->get(globalObject, keyValue); + RETURN_IF_EXCEPTION(scope, {}); if (entry) { if (auto* moduleObject = jsDynamicCast(entry)) { if (!moduleObject->hasEvaluated) { - auto scope = DECLARE_THROW_SCOPE(vm); evaluateCommonJSModuleOnce( vm, globalObject, @@ -1492,6 +1505,7 @@ std::optional createCommonJSModule( // On error, remove the module from the require map // so that it can be re-evaluated on the next require. globalObject->requireMap()->remove(globalObject, moduleObject->filename()); + RETURN_IF_EXCEPTION(scope, {}); scope.throwException(globalObject, exception); return; @@ -1499,6 +1513,7 @@ std::optional createCommonJSModule( } moduleObject->toSyntheticSource(globalObject, moduleKey, exportNames, exportValues); + RETURN_IF_EXCEPTION(scope, {}); } } else { // require map was cleared of the entry @@ -1513,12 +1528,14 @@ JSObject* JSCommonJSModule::createBoundRequireFunction(VM& vm, JSGlobalObject* l ASSERT(!pathString.startsWith("file://"_s)); auto* globalObject = jsCast(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); JSString* filename = JSC::jsStringWithCache(vm, pathString); auto index = pathString.reverseFind(PLATFORM_SEP, pathString.length()); JSString* dirname; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, filename, 0, index); + RETURN_IF_EXCEPTION(scope, nullptr); } else { dirname = jsEmptyString(vm); } @@ -1533,12 +1550,14 @@ JSObject* JSCommonJSModule::createBoundRequireFunction(VM& vm, JSGlobalObject* l globalObject->requireFunctionUnbound(), moduleObject, ArgList(), 1, globalObject->commonStrings().requireString(globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); JSFunction* resolveFunction = JSC::JSBoundFunction::create(vm, globalObject, globalObject->requireResolveFunctionUnbound(), moduleObject->filename(), ArgList(), 1, globalObject->commonStrings().resolveString(globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); requireFunction->putDirect(vm, vm.propertyNames->resolve, resolveFunction, 0); diff --git a/src/bun.js/bindings/JSDOMExceptionHandling.cpp b/src/bun.js/bindings/JSDOMExceptionHandling.cpp index f2c83e14e7..de78fc5d29 100644 --- a/src/bun.js/bindings/JSDOMExceptionHandling.cpp +++ b/src/bun.js/bindings/JSDOMExceptionHandling.cpp @@ -212,7 +212,9 @@ JSValue createDOMException(JSGlobalObject& lexicalGlobalObject, Exception&& exce void propagateExceptionSlowPath(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& throwScope, Exception&& exception) { throwScope.assertNoExceptionExceptTermination(); - throwException(&lexicalGlobalObject, throwScope, createDOMException(lexicalGlobalObject, WTFMove(exception))); + auto jsException = createDOMException(lexicalGlobalObject, WTFMove(exception)); + RETURN_IF_EXCEPTION(throwScope, ); + throwException(&lexicalGlobalObject, throwScope, jsException); } static EncodedJSValue throwTypeError(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, const String& errorMessage) diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp index 6f08fa2a2a..d3f1baafb9 100644 --- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp +++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp @@ -78,7 +78,9 @@ JSC_DEFINE_CUSTOM_GETTER(jsTimeZoneEnvironmentVariableGetter, (JSGlobalObject * ZigString name = toZigString(propertyName.publicName()); ZigString value = { nullptr, 0 }; - if (auto hasExistingValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().dataPrivateName())) { + auto hasExistingValue = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().dataPrivateName()); + RETURN_IF_EXCEPTION(scope, {}); + if (hasExistingValue) { return JSValue::encode(hasExistingValue); } diff --git a/src/bun.js/bindings/JSGlobalObject.zig b/src/bun.js/bindings/JSGlobalObject.zig index 723b633af8..85637512e9 100644 --- a/src/bun.js/bindings/JSGlobalObject.zig +++ b/src/bun.js/bindings/JSGlobalObject.zig @@ -490,6 +490,7 @@ pub const JSGlobalObject = opaque { return JSC__JSGlobalObject__generateHeapSnapshot(this); } + // DEPRECATED - use CatchScope to check for exceptions and signal exceptions by returning JSError pub fn hasException(this: *JSGlobalObject) bool { return JSGlobalObject__hasException(this); } diff --git a/src/bun.js/bindings/JSModuleLoader.zig b/src/bun.js/bindings/JSModuleLoader.zig index bebeaacd9a..8692a6b449 100644 --- a/src/bun.js/bindings/JSModuleLoader.zig +++ b/src/bun.js/bindings/JSModuleLoader.zig @@ -46,8 +46,8 @@ pub const JSModuleLoader = opaque { return JSC__JSModuleLoader__loadAndEvaluateModule(globalObject, module_name); } - extern fn JSModuleLoader__import(*JSGlobalObject, *const bun.String) *JSInternalPromise; - pub fn import(globalObject: *JSGlobalObject, module_name: *const bun.String) *JSInternalPromise { - return JSModuleLoader__import(globalObject, module_name); + extern fn JSModuleLoader__import(*JSGlobalObject, *const bun.String) ?*JSInternalPromise; + pub fn import(globalObject: *JSGlobalObject, module_name: *const bun.String) bun.JSError!*JSInternalPromise { + return JSModuleLoader__import(globalObject, module_name) orelse error.JSError; } }; diff --git a/src/bun.js/bindings/JSObject.zig b/src/bun.js/bindings/JSObject.zig index 748b5322e4..188ae137ee 100644 --- a/src/bun.js/bindings/JSObject.zig +++ b/src/bun.js/bindings/JSObject.zig @@ -144,8 +144,18 @@ pub const JSObject = opaque { return JSC__JSObject__create(global, length, creator, Type.call); } - pub fn getIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSValue { - return JSC__JSObject__getIndex(this, globalThis, i); + pub fn getIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSError!JSValue { + // we don't use fromJSHostCall, because it will assert that if there is an exception + // then the JSValue is zero. the function this ends up calling can return undefined + // with an exception: + // https://github.com/oven-sh/WebKit/blob/397dafc9721b8f8046f9448abb6dbc14efe096d3/Source/JavaScriptCore/runtime/JSObjectInlines.h#L112 + var scope: JSC.CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); + const value = JSC__JSObject__getIndex(this, globalThis, i); + try scope.returnIfException(); + bun.assert(value != .zero); + return value; } pub fn putRecord(this: *JSObject, global: *JSGlobalObject, key: *ZigString, values: []ZigString) void { diff --git a/src/bun.js/bindings/JSPromise.zig b/src/bun.js/bindings/JSPromise.zig index 5cb0765641..631e29e6bc 100644 --- a/src/bun.js/bindings/JSPromise.zig +++ b/src/bun.js/bindings/JSPromise.zig @@ -189,12 +189,17 @@ pub const JSPromise = opaque { args: Args, pub fn call(this: *@This(), g: *JSC.JSGlobalObject) callconv(.c) JSC.JSValue { - return JSC.toJSHostValue(g, @call(.auto, Fn, this.args)); + return JSC.toJSHostCall(g, @src(), Fn, this.args); } }; + var scope: JSC.CatchScope = undefined; + scope.init(globalObject, @src(), .enabled); + defer scope.deinit(); var ctx = Wrapper{ .args = args }; - return JSC__JSPromise__wrap(globalObject, &ctx, @ptrCast(&Wrapper.call)); + const promise = JSC__JSPromise__wrap(globalObject, &ctx, @ptrCast(&Wrapper.call)); + bun.debugAssert(!scope.hasException()); // TODO: properly propagate exception upwards + return promise; } pub fn wrapValue(globalObject: *JSGlobalObject, value: JSValue) JSValue { @@ -257,6 +262,9 @@ pub const JSPromise = opaque { /// The value can be another Promise /// If you want to create a new Promise that is already resolved, see JSPromise.resolvedPromiseValue pub fn resolve(this: *JSPromise, globalThis: *JSGlobalObject, value: JSValue) void { + var scope: JSC.CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); if (comptime bun.Environment.isDebug) { const loop = JSC.VirtualMachine.get().eventLoop(); loop.debug.js_call_count_outside_tick_queue += @as(usize, @intFromBool(!loop.debug.is_inside_tick_queue)); @@ -266,6 +274,7 @@ pub const JSPromise = opaque { } JSC__JSPromise__resolve(this, globalThis, value); + bun.debugAssert(!scope.hasException()); // TODO: properly propagate exception upwards } pub fn reject(this: *JSPromise, globalThis: *JSGlobalObject, value: JSError!JSValue) void { diff --git a/src/bun.js/bindings/JSPropertyIterator.zig b/src/bun.js/bindings/JSPropertyIterator.zig index 226585e337..401d47d0df 100644 --- a/src/bun.js/bindings/JSPropertyIterator.zig +++ b/src/bun.js/bindings/JSPropertyIterator.zig @@ -121,10 +121,11 @@ const JSPropertyIteratorImpl = opaque { own_properties_only: bool, only_non_index_properties: bool, ) bun.JSError!?*JSPropertyIteratorImpl { + var scope: JSC.CatchScope = undefined; + scope.init(globalObject, @src(), .enabled); + defer scope.deinit(); const iter = Bun__JSPropertyIterator__create(globalObject, object.toJS(), count, own_properties_only, only_non_index_properties); - if (globalObject.hasException()) { - return error.JSError; - } + try scope.returnIfException(); return iter; } @@ -134,6 +135,7 @@ const JSPropertyIteratorImpl = opaque { pub const getName = Bun__JSPropertyIterator__getName; pub const getLongestPropertyName = Bun__JSPropertyIterator__getLongestPropertyName; + /// may return null without an exception extern "c" fn Bun__JSPropertyIterator__create(globalObject: *JSC.JSGlobalObject, encodedValue: JSC.JSValue, count: *usize, own_properties_only: bool, only_non_index_properties: bool) ?*JSPropertyIteratorImpl; extern "c" fn Bun__JSPropertyIterator__getNameAndValue(iter: *JSPropertyIteratorImpl, globalObject: *JSC.JSGlobalObject, object: *JSC.JSObject, propertyName: *bun.String, i: usize) JSC.JSValue; extern "c" fn Bun__JSPropertyIterator__getNameAndValueNonObservable(iter: *JSPropertyIteratorImpl, globalObject: *JSC.JSGlobalObject, object: *JSC.JSObject, propertyName: *bun.String, i: usize) JSC.JSValue; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 218f07800c..3a89ee7770 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -58,7 +58,7 @@ pub const JSValue = enum(i64) { return JSC__JSValue__coerceToInt64(this, globalThis); } - pub fn getIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSValue { + pub fn getIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSError!JSValue { return JSC.JSObject.getIndex(this, globalThis, i); } @@ -91,8 +91,12 @@ pub const JSValue = enum(i64) { globalThis: *JSC.JSGlobalObject, ctx: ?*anyopaque, callback: PropertyIteratorFn, - ) void { + ) JSError!void { + var scope: CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); JSC__JSValue__forEachPropertyNonIndexed(this, globalThis, ctx, callback); + try scope.returnIfException(); } pub fn forEachProperty( @@ -100,17 +104,25 @@ pub const JSValue = enum(i64) { globalThis: *JSC.JSGlobalObject, ctx: ?*anyopaque, callback: PropertyIteratorFn, - ) void { + ) JSError!void { + var scope: CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); JSC__JSValue__forEachProperty(this, globalThis, ctx, callback); + try scope.returnIfException(); } pub fn forEachPropertyOrdered( this: JSValue, - globalObject: *JSC.JSGlobalObject, + globalThis: *JSC.JSGlobalObject, ctx: ?*anyopaque, callback: PropertyIteratorFn, - ) void { - JSC__JSValue__forEachPropertyOrdered(this, globalObject, ctx, callback); + ) JSError!void { + var scope: CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); + JSC__JSValue__forEachPropertyOrdered(this, globalThis, ctx, callback); + try scope.returnIfException(); } extern fn JSC__JSValue__coerceToDouble(this: JSValue, globalObject: *JSC.JSGlobalObject) f64; @@ -255,13 +267,13 @@ pub const JSValue = enum(i64) { return this.call(globalThis, globalThis.toJSValue(), args); } - pub extern "c" fn Bun__JSValue__call( + extern "c" fn Bun__JSValue__call( ctx: *JSGlobalObject, object: JSValue, thisObject: JSValue, argumentCount: usize, arguments: [*]const JSValue, - ) JSValue.MaybeException; + ) JSValue; pub fn call(function: JSValue, global: *JSGlobalObject, thisValue: JSC.JSValue, args: []const JSC.JSValue) bun.JSError!JSC.JSValue { JSC.markBinding(@src()); @@ -277,13 +289,13 @@ pub const JSValue = enum(i64) { // this can be an async context so it's fine if it's not callable. } - return Bun__JSValue__call( + return fromJSHostCall(global, @src(), Bun__JSValue__call, .{ global, function, thisValue, args.len, args.ptr, - ).unwrap(); + }); } extern fn Bun__Process__queueNextTick1(*JSGlobalObject, func: JSValue, JSValue) void; @@ -337,7 +349,7 @@ pub const JSValue = enum(i64) { extern fn JSC__JSValue__createEmptyArray(global: *JSGlobalObject, len: usize) JSValue; pub fn createEmptyArray(global: *JSGlobalObject, len: usize) bun.JSError!JSValue { - return bun.jsc.fromJSHostValue(JSC__JSValue__createEmptyArray(global, len)); + return fromJSHostCall(global, @src(), JSC__JSValue__createEmptyArray, .{ global, len }); } extern fn JSC__JSValue__putRecord(value: JSValue, global: *JSGlobalObject, key: *ZigString, values_array: [*]ZigString, values_len: usize) void; @@ -758,24 +770,24 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__keys(globalThis: *JSGlobalObject, value: JSValue) JSValue; - pub fn keys(value: JSValue, globalThis: *JSGlobalObject) JSValue { - return JSC__JSValue__keys( + pub fn keys(value: JSValue, globalThis: *JSGlobalObject) JSError!JSValue { + return fromJSHostCall(globalThis, @src(), JSC__JSValue__keys, .{ globalThis, value, - ); + }); } extern fn JSC__JSValue__values(globalThis: *JSGlobalObject, value: JSValue) JSValue; /// This is `Object.values`. /// `value` is assumed to be not empty, undefined, or null. - pub fn values(value: JSValue, globalThis: *JSGlobalObject) JSValue { + pub fn values(value: JSValue, globalThis: *JSGlobalObject) JSError!JSValue { if (comptime bun.Environment.allow_assert) { bun.assert(!value.isEmptyOrUndefinedOrNull()); } - return JSC__JSValue__values( + return fromJSHostCall(globalThis, @src(), JSC__JSValue__values, .{ globalThis, value, - ); + }); } extern "c" fn JSC__JSValue__hasOwnPropertyValue(JSValue, *JSGlobalObject, JSValue) bool; @@ -783,9 +795,16 @@ pub const JSValue = enum(i64) { /// Returns true if the object has the property, false otherwise /// /// If the object is not an object, it will crash. **You must check if the object is an object before calling this function.** - pub const hasOwnPropertyValue = JSC__JSValue__hasOwnPropertyValue; + pub fn hasOwnPropertyValue(this: JSValue, global: *JSGlobalObject, key: JSValue) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + const result = JSC__JSValue__hasOwnPropertyValue(this, global, key); + try scope.returnIfException(); + return result; + } - pub inline fn arrayIterator(this: JSValue, global: *JSGlobalObject) JSArrayIterator { + pub inline fn arrayIterator(this: JSValue, global: *JSGlobalObject) JSError!JSArrayIterator { return JSArrayIterator.init(this, global); } @@ -1044,7 +1063,7 @@ pub const JSValue = enum(i64) { pub inline fn isFunction(this: JSValue) bool { return this.isCell() and this.jsType().isFunction(); } - pub fn isObjectEmpty(this: JSValue, globalObject: *JSGlobalObject) bool { + pub fn isObjectEmpty(this: JSValue, globalObject: *JSGlobalObject) JSError!bool { const type_of_value = this.jsType(); // https://github.com/jestjs/jest/blob/main/packages/jest-get-type/src/index.ts#L26 // Map and Set are not considered as object in jest-extended @@ -1052,7 +1071,7 @@ pub const JSValue = enum(i64) { return false; } - return this.jsType().isObject() and keys(this, globalObject).getLength(globalObject) == 0; + return this.jsType().isObject() and try (try this.keys(globalObject)).getLength(globalObject) == 0; } extern fn JSC__JSValue__isClass(this: JSValue, global: *JSGlobalObject) bool; @@ -1118,6 +1137,14 @@ pub const JSValue = enum(i64) { return JSC__JSValue__isException(this, vm); } + /// Cast to an Exception pointer, or null if not an Exception + pub fn asException(this: JSValue, vm: *VM) ?*JSC.Exception { + return if (this.isException(vm)) + this.uncheckedPtrCast(JSC.Exception) + else + null; + } + extern fn JSC__JSValue__isTerminationException(this: JSValue, vm: *VM) bool; pub fn isTerminationException(this: JSValue, vm: *VM) bool { return JSC__JSValue__isTerminationException(this, vm); @@ -1129,9 +1156,12 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__toZigString(this: JSValue, out: *ZigString, global: *JSGlobalObject) void; - pub fn toZigString(this: JSValue, out: *ZigString, global: *JSGlobalObject) error{JSError}!void { + pub fn toZigString(this: JSValue, out: *ZigString, global: *JSGlobalObject) JSError!void { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); JSC__JSValue__toZigString(this, out, global); - if (global.hasException()) return error.JSError; + try scope.returnIfException(); } /// Increments the reference count, you must call `.deref()` or it will leak memory. @@ -1232,7 +1262,12 @@ pub const JSValue = enum(i64) { extern fn JSC__JSValue__toStringOrNull(this: JSValue, globalThis: *JSGlobalObject) ?*JSString; // Calls JSValue::toStringOrNull. Returns error on exception. pub fn toJSString(this: JSValue, globalThis: *JSGlobalObject) bun.JSError!*JSString { - return JSC__JSValue__toStringOrNull(this, globalThis) orelse return error.JSError; + var scope: CatchScope = undefined; + scope.init(globalThis, @src(), .assertions_only); + defer scope.deinit(); + const maybe_string = JSC__JSValue__toStringOrNull(this, globalThis); + scope.assertExceptionPresenceMatches(maybe_string == null); + return maybe_string orelse error.JSError; } /// Call `toString()` on the JSValue and clone the result. @@ -1363,8 +1398,13 @@ pub const JSValue = enum(i64) { if (bun.Environment.isDebug) bun.assert(this.isObject()); - return switch (JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name))) { - .zero => error.JSError, + return switch (try fromJSHostCall( + global, + @src(), + JSC__JSValue__fastGet, + .{ this, global, @intFromEnum(builtin_name) }, + )) { + .zero => unreachable, // handled by fromJSHostCall .js_undefined, .property_does_not_exist_on_object => null, else => |val| val, }; @@ -1398,8 +1438,13 @@ pub const JSValue = enum(i64) { extern fn JSC__JSValue__getIfPropertyExistsImpl(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) JSValue; extern fn JSC__JSValue__getPropertyValue(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) JSValue; extern fn JSC__JSValue__getIfPropertyExistsFromPath(this: JSValue, global: *JSGlobalObject, path: JSValue) JSValue; - pub fn getIfPropertyExistsFromPath(this: JSValue, global: *JSGlobalObject, path: JSValue) JSValue { - return JSC__JSValue__getIfPropertyExistsFromPath(this, global, path); + pub fn getIfPropertyExistsFromPath(this: JSValue, global: *JSGlobalObject, path: JSValue) JSError!JSValue { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + const result = JSC__JSValue__getIfPropertyExistsFromPath(this, global, path); + try scope.returnIfException(); + return result; } extern fn JSC__JSValue__getSymbolDescription(this: JSValue, global: *JSGlobalObject, str: *ZigString) void; @@ -1418,16 +1463,24 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue___then(this: JSValue, global: *JSGlobalObject, ctx: JSValue, resolve: *const JSC.JSHostFn, reject: *const JSC.JSHostFn) void; - pub fn _then(this: JSValue, global: *JSGlobalObject, ctx: JSValue, resolve: JSC.JSHostFnZig, reject: JSC.JSHostFnZig) void { + fn _then(this: JSValue, global: *JSGlobalObject, ctx: JSValue, resolve: JSC.JSHostFnZig, reject: JSC.JSHostFnZig) void { return JSC__JSValue___then(this, global, ctx, toJSHostFunction(resolve), toJSHostFunction(reject)); } pub fn _then2(this: JSValue, global: *JSGlobalObject, ctx: JSValue, resolve: *const JSC.JSHostFn, reject: *const JSC.JSHostFn) void { - return JSC__JSValue___then(this, global, ctx, resolve, reject); + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + JSC__JSValue___then(this, global, ctx, resolve, reject); + bun.debugAssert(!scope.hasException()); // TODO: properly propagate exception upwards } pub fn then(this: JSValue, global: *JSGlobalObject, ctx: ?*anyopaque, resolve: JSC.JSHostFnZig, reject: JSC.JSHostFnZig) void { - return this._then(global, JSValue.fromPtrAddress(@intFromPtr(ctx)), resolve, reject); + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + this._then(global, JSValue.fromPtrAddress(@intFromPtr(ctx)), resolve, reject); + bun.debugAssert(!scope.hasException()); // TODO: properly propagate exception upwards } pub fn getDescription(this: JSValue, global: *JSGlobalObject) ZigString { @@ -1436,23 +1489,6 @@ pub const JSValue = enum(i64) { return zig_str; } - /// Equivalent to `obj.property` in JavaScript. - /// Reminder: `undefined` is a value! - /// - /// Prefer `get` in new code, as this function is incapable of returning an exception - pub fn get_unsafe(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { - if (comptime bun.Environment.isDebug) { - if (BuiltinName.has(property)) { - Output.debugWarn("get(\"{s}\") called. Please use fastGet(.{s}) instead!", .{ property, property }); - } - } - - return switch (JSC__JSValue__getIfPropertyExistsImpl(this, global, property.ptr, @intCast(property.len))) { - .js_undefined, .zero, .property_does_not_exist_on_object => null, - else => |val| val, - }; - } - /// Equivalent to `target[property]`. Calls userland getters/proxies. Can /// throw. Null indicates the property does not exist. JavaScript undefined /// and JavaScript null can exist as a property and is different than zig @@ -1466,7 +1502,7 @@ pub const JSValue = enum(i64) { /// Cannot handle property names that are numeric indexes. (For this use `getPropertyValue` instead.) /// pub inline fn get(target: JSValue, global: *JSGlobalObject, property: anytype) JSError!?JSValue { - if (bun.Environment.isDebug) bun.assert(target.isObject()); + bun.debugAssert(target.isObject()); const property_slice: []const u8 = property; // must be a slice! // This call requires `get` to be `inline` @@ -1476,8 +1512,13 @@ pub const JSValue = enum(i64) { } } - return switch (JSC__JSValue__getIfPropertyExistsImpl(target, global, property_slice.ptr, @intCast(property_slice.len))) { - .zero => error.JSError, + return switch (try fromJSHostCall(global, @src(), JSC__JSValue__getIfPropertyExistsImpl, .{ + target, + global, + property_slice.ptr, + @intCast(property_slice.len), + })) { + .zero => unreachable, // handled by fromJSHostCall .property_does_not_exist_on_object => null, // TODO: see bug described in ObjectBindings.cpp @@ -1512,10 +1553,17 @@ pub const JSValue = enum(i64) { extern fn JSC__JSValue__getOwn(value: JSValue, globalObject: *JSGlobalObject, propertyName: *const bun.String) JSValue; /// Get *own* property value (i.e. does not resolve property in the prototype chain) - pub fn getOwn(this: JSValue, global: *JSGlobalObject, property_name: anytype) ?JSValue { + pub fn getOwn(this: JSValue, global: *JSGlobalObject, property_name: anytype) bun.JSError!?JSValue { var property_name_str = bun.String.init(property_name); + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const value = JSC__JSValue__getOwn(this, global, &property_name_str); - return if (@intFromEnum(value) != 0) value else return null; + try scope.returnIfException(); + return if (value == .zero) + null + else + value; } extern fn JSC__JSValue__getOwnByValue(value: JSValue, globalObject: *JSGlobalObject, propertyValue: JSValue) JSValue; @@ -1525,8 +1573,8 @@ pub const JSValue = enum(i64) { return if (@intFromEnum(value) != 0) value else return null; } - pub fn getOwnTruthy(this: JSValue, global: *JSGlobalObject, property_name: anytype) ?JSValue { - if (getOwn(this, global, property_name)) |prop| { + pub fn getOwnTruthy(this: JSValue, global: *JSGlobalObject, property_name: anytype) bun.JSError!?JSValue { + if (try getOwn(this, global, property_name)) |prop| { if (prop.isUndefined()) return null; return prop; } @@ -1610,6 +1658,9 @@ pub const JSValue = enum(i64) { /// - .js_undefined /// - an empty string pub fn getStringish(this: JSValue, global: *JSGlobalObject, property: []const u8) bun.JSError!?bun.String { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const prop = try get(this, global, property) orelse return null; if (prop.isNull() or prop == .false) { return null; @@ -1619,14 +1670,12 @@ pub const JSValue = enum(i64) { } const str = try prop.toBunString(global); - if (global.hasException()) { - str.deref(); - return error.JSError; - } - if (str.isEmpty()) { - return null; - } - return str; + errdefer str.deref(); + try scope.returnIfException(); + return if (str.isEmpty()) + null + else + str; } pub fn toEnumFromMap( @@ -1715,7 +1764,7 @@ pub const JSValue = enum(i64) { return globalThis.throwInvalidArguments(property_name ++ " must be an array", .{}); } - if (prop.getLength(globalThis) == 0) { + if (try prop.getLength(globalThis) == 0) { return null; } @@ -1731,7 +1780,7 @@ pub const JSValue = enum(i64) { } pub fn getOwnArray(this: JSValue, globalThis: *JSGlobalObject, comptime property_name: []const u8) JSError!?JSValue { - if (getOwnTruthy(this, globalThis, property_name)) |prop| { + if (try getOwnTruthy(this, globalThis, property_name)) |prop| { return coerceToArray(prop, globalThis, property_name); } @@ -1739,7 +1788,7 @@ pub const JSValue = enum(i64) { } pub fn getOwnObject(this: JSValue, globalThis: *JSGlobalObject, comptime property_name: []const u8) JSError!?*JSC.JSObject { - if (getOwnTruthy(this, globalThis, property_name)) |prop| { + if (try getOwnTruthy(this, globalThis, property_name)) |prop| { const obj = prop.getObject() orelse { return globalThis.throwInvalidArguments(property_name ++ " must be an object", .{}); }; @@ -1867,46 +1916,66 @@ pub const JSValue = enum(i64) { /// /// This algorithm differs from the IsStrictlyEqual Algorithm by treating all NaN values as equivalent and by differentiating +0𝔽 from -0𝔽. /// https://tc39.es/ecma262/#sec-samevalue - pub fn isSameValue(this: JSValue, other: JSValue, global: *JSGlobalObject) bool { - return @intFromEnum(this) == @intFromEnum(other) or JSC__JSValue__isSameValue(this, other, global); + /// + /// This can throw because it resolves rope strings + pub fn isSameValue(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool { + if (@intFromEnum(this) == @intFromEnum(other)) return true; + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + const same = JSC__JSValue__isSameValue(this, other, global); + try scope.returnIfException(); + return same; } extern fn JSC__JSValue__deepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool; pub fn deepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const result = JSC__JSValue__deepEquals(this, other, global); - if (global.hasException()) return error.JSError; + try scope.returnIfException(); return result; } extern fn JSC__JSValue__jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool; /// same as `JSValue.deepEquals`, but with jest asymmetric matchers enabled pub fn jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const result = JSC__JSValue__jestDeepEquals(this, other, global); - if (global.hasException()) return error.JSError; + try scope.returnIfException(); return result; } extern fn JSC__JSValue__strictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool; pub fn strictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const result = JSC__JSValue__strictDeepEquals(this, other, global); - if (global.hasException()) return error.JSError; + try scope.returnIfException(); return result; } extern fn JSC__JSValue__jestStrictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool; /// same as `JSValue.strictDeepEquals`, but with jest asymmetric matchers enabled pub fn jestStrictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); const result = JSC__JSValue__jestStrictDeepEquals(this, other, global); - if (global.hasException()) return error.JSError; + try scope.returnIfException(); return result; } - extern fn JSC__JSValue__deepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool; - /// NOTE: can throw. Check for exceptions. - pub fn deepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool { - return JSC__JSValue__deepMatch(this, subset, global, replace_props_with_asymmetric_matchers); - } extern fn JSC__JSValue__jestDeepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool; /// same as `JSValue.deepMatch`, but with jest asymmetric matchers enabled - pub fn jestDeepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool { - return JSC__JSValue__jestDeepMatch(this, subset, global, replace_props_with_asymmetric_matchers); + pub fn jestDeepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) JSError!bool { + var scope: CatchScope = undefined; + scope.init(global, @src(), .enabled); + defer scope.deinit(); + const result = JSC__JSValue__jestDeepMatch(this, subset, global, replace_props_with_asymmetric_matchers); + try scope.returnIfException(); + return result; } pub const DiffMethod = enum(u8) { @@ -2107,42 +2176,27 @@ pub const JSValue = enum(i64) { /// - anything with a .length property returning a number /// /// If the "length" property does not exist, this function will return 0. - pub fn getLength(this: JSValue, globalThis: *JSGlobalObject) u64 { - const len = this.getLengthIfPropertyExistsInternal(globalThis); + pub fn getLength(this: JSValue, globalThis: *JSGlobalObject) JSError!u64 { + const len = try this.getLengthIfPropertyExistsInternal(globalThis); if (len == std.math.floatMax(f64)) { return 0; } - return @as(u64, @intFromFloat(@max(@min(len, std.math.maxInt(i52)), 0))); - } - - /// This function supports: - /// - Array, DerivedArray & friends - /// - String, DerivedString & friends - /// - TypedArray - /// - Map (size) - /// - WeakMap (size) - /// - Set (size) - /// - WeakSet (size) - /// - ArrayBuffer (byteLength) - /// - anything with a .length property returning a number - /// - /// If the "length" property does not exist, this function will return null. - pub fn tryGetLength(this: JSValue, globalThis: *JSGlobalObject) ?f64 { - const len = this.getLengthIfPropertyExistsInternal(globalThis); - if (len == std.math.floatMax(f64)) { - return null; - } - - return @as(u64, @intFromFloat(@max(@min(len, std.math.maxInt(i52)), 0))); + return @intFromFloat(std.math.clamp(len, 0, std.math.maxInt(i52))); } extern fn JSC__JSValue__getLengthIfPropertyExistsInternal(this: JSValue, globalThis: *JSGlobalObject) f64; /// Do not use this directly! /// /// If the property does not exist, this function will return max(f64) instead of 0. - pub fn getLengthIfPropertyExistsInternal(this: JSValue, globalThis: *JSGlobalObject) f64 { - return JSC__JSValue__getLengthIfPropertyExistsInternal(this, globalThis); + /// TODO this should probably just return an optional + pub fn getLengthIfPropertyExistsInternal(this: JSValue, globalThis: *JSGlobalObject) JSError!f64 { + var scope: CatchScope = undefined; + scope.init(globalThis, @src(), .enabled); + defer scope.deinit(); + const length = JSC__JSValue__getLengthIfPropertyExistsInternal(this, globalThis); + try scope.returnIfException(); + return length; } extern fn JSC__JSValue__isAggregateError(this: JSValue, globalObject: *JSGlobalObject) bool; @@ -2172,8 +2226,13 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__isIterable(this: JSValue, globalObject: *JSGlobalObject) bool; - pub fn isIterable(this: JSValue, globalObject: *JSGlobalObject) bool { - return JSC__JSValue__isIterable(this, globalObject); + pub fn isIterable(this: JSValue, globalObject: *JSGlobalObject) JSError!bool { + var scope: CatchScope = undefined; + scope.init(globalObject, @src(), .enabled); + defer scope.deinit(); + const is_iterable = JSC__JSValue__isIterable(this, globalObject); + try scope.returnIfException(); + return is_iterable; } extern fn JSC__JSValue__stringIncludes(this: JSValue, globalObject: *JSGlobalObject, other: JSValue) bool; @@ -2198,7 +2257,7 @@ pub const JSValue = enum(i64) { // TODO: remove this (no replacement) pub inline fn asObjectRef(this: JSValue) C_API.JSObjectRef { - return @as(C_API.JSObjectRef, @ptrCast(this.asVoid())); + return @ptrFromInt(@as(usize, @bitCast(@intFromEnum(this)))); } /// When the GC sees a JSValue referenced in the stack, it knows not to free it @@ -2208,19 +2267,6 @@ pub const JSValue = enum(i64) { std.mem.doNotOptimizeAway(this.asEncoded().asPtr); } - pub inline fn asNullableVoid(this: JSValue) ?*anyopaque { - return @as(?*anyopaque, @ptrFromInt(@as(usize, @bitCast(@intFromEnum(this))))); - } - - pub inline fn asVoid(this: JSValue) *anyopaque { - if (comptime bun.Environment.allow_assert) { - if (@intFromEnum(this) == 0) { - @panic("JSValue is null"); - } - } - return this.asNullableVoid().?; - } - pub fn uncheckedPtrCast(value: JSValue, comptime T: type) *T { return @alignCast(@ptrCast(value.asEncoded().asPtr)); } @@ -2450,6 +2496,8 @@ const JSString = JSC.JSString; const JSObject = JSC.JSObject; const JSArrayIterator = JSC.JSArrayIterator; const JSCell = JSC.JSCell; +const fromJSHostCall = JSC.fromJSHostCall; +const CatchScope = JSC.CatchScope; const AnyPromise = JSC.AnyPromise; const DOMURL = JSC.DOMURL; diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index adb8f33bb2..b3a71c7a6a 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -358,7 +358,7 @@ static JSValue handleVirtualModuleResult( case OnLoadResultTypeCode: { Bun__transpileVirtualModule(globalObject, specifier, referrer, &onLoadResult.value.sourceText.string, onLoadResult.value.sourceText.loader, res); if (!res->success) { - return reject(JSValue::decode(reinterpret_cast(res->result.err.ptr))); + return reject(JSValue::decode(res->result.err.value)); } auto provider = Zig::SourceProvider::create(globalObject, res->result.value); @@ -434,20 +434,23 @@ extern "C" void Bun__onFulfillAsyncModule( JSC::JSInternalPromise* promise = jsCast(JSC::JSValue::decode(encodedPromiseValue)); if (!res->success) { - throwException(scope, res->result.err, globalObject); - auto* exception = scope.exception(); - scope.clearException(); - return promise->reject(globalObject, exception); + return promise->reject(globalObject, JSValue::decode(res->result.err.value)); } auto specifierValue = Bun::toJS(globalObject, *specifier); - if (auto entry = globalObject->esmRegistryMap()->get(globalObject, specifierValue)) { + auto* map = globalObject->esmRegistryMap(); + RETURN_IF_EXCEPTION(scope, ); + auto entry = map->get(globalObject, specifierValue); + RETURN_IF_EXCEPTION(scope, ); + if (entry) { if (entry.isObject()) { auto* object = entry.getObject(); - if (auto state = object->getIfPropertyExists(globalObject, Bun::builtinNames(vm).statePublicName())) { - if (state.toInt32(globalObject) > JSC::JSModuleLoader::Status::Fetch) { + auto state = object->getIfPropertyExists(globalObject, Bun::builtinNames(vm).statePublicName()); + RETURN_IF_EXCEPTION(scope, ); + if (state && state.isInt32()) { + if (state.asInt32() > JSC::JSModuleLoader::Status::Fetch) { // it's a race! we lost. // https://github.com/oven-sh/bun/issues/6946 // https://github.com/oven-sh/bun/issues/12910 @@ -463,12 +466,15 @@ extern "C" void Bun__onFulfillAsyncModule( promise->resolve(globalObject, code); } else { auto* exception = scope.exception(); - scope.clearException(); - promise->reject(globalObject, exception); + if (!vm.isTerminationException(exception)) { + scope.clearException(); + promise->reject(globalObject, exception); + } } } else { auto&& provider = Zig::SourceProvider::create(jsDynamicCast(globalObject), res->result.value); promise->resolve(globalObject, JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); + scope.assertNoExceptionExceptTermination(); } } else { // the module has since been deleted from the registry. @@ -659,7 +665,9 @@ JSValue fetchCommonJSModule( } } - if (auto builtin = fetchBuiltinModuleWithoutResolution(globalObject, &specifier, res)) { + auto builtin = fetchBuiltinModuleWithoutResolution(globalObject, &specifier, res); + RETURN_IF_EXCEPTION(scope, {}); + if (builtin) { if (!res->success) { RELEASE_AND_RETURN(scope, builtin); } @@ -708,18 +716,22 @@ JSValue fetchCommonJSModule( JSMap* registry = globalObject->esmRegistryMap(); - const auto hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice = [&]() -> bool { + bool hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice = [&]() -> bool { JSValue entry = registry->get(globalObject, specifierValue); if (!entry || !entry.isObject()) { return false; } + // return value doesn't matter since we check for exceptions after calling this lambda and + // before checking the returned bool + RETURN_IF_EXCEPTION(scope, false); int status = entry.getObject()->getDirect(vm, WebCore::clientData(vm)->builtinNames().statePublicName()).asInt32(); return status > JSModuleLoader::Status::Fetch; - }; + }(); + RETURN_IF_EXCEPTION(scope, {}); - if (hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice()) { + if (hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice) { RELEASE_AND_RETURN(scope, jsNumber(-1)); } return fetchCommonJSModuleNonBuiltin(bunVM, vm, globalObject, &specifier, specifierValue, referrer, typeAttribute, res, target, specifierWtfString, BunLoaderTypeNone, scope); diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 39b4bad591..7e0108f168 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -677,6 +677,7 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal } else { headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue); + RETURN_IF_EXCEPTION(scope, void()); arrayValues.append(nameString); arrayValues.append(jsValue); RETURN_IF_EXCEPTION(scope, void()); diff --git a/src/bun.js/bindings/NodeModuleModule.zig b/src/bun.js/bindings/NodeModuleModule.zig index 2b903d806d..133cfd643f 100644 --- a/src/bun.js/bindings/NodeModuleModule.zig +++ b/src/bun.js/bindings/NodeModuleModule.zig @@ -5,12 +5,14 @@ const JSGlobalObject = JSC.JSGlobalObject; const JSValue = JSC.JSValue; const ErrorableString = JSC.ErrorableString; +export const NodeModuleModule__findPath = JSC.host_fn.wrap3(findPath); + // https://github.com/nodejs/node/blob/40ef9d541ed79470977f90eb445c291b95ab75a0/lib/internal/modules/cjs/loader.js#L666 -pub export fn NodeModuleModule__findPath( +fn findPath( global: *JSGlobalObject, request_bun_str: bun.String, paths_maybe: ?*JSC.JSArray, -) JSValue { +) bun.JSError!JSValue { var stack_buf = std.heap.stackFallback(8192, bun.default_allocator); const alloc = stack_buf.get(); @@ -25,12 +27,9 @@ pub export fn NodeModuleModule__findPath( // for each path const found = if (paths_maybe) |paths| found: { - var iter = paths.iterator(global); - while (iter.next()) |path| { - const cur_path = bun.String.fromJS(path, global) catch |err| switch (err) { - error.JSError => return .zero, - error.OutOfMemory => return global.throwOutOfMemoryValue(), - }; + var iter = try paths.iterator(global); + while (try iter.next()) |path| { + const cur_path = try bun.String.fromJS(path, global); defer cur_path.deref(); if (findPathInner(request_bun_str, cur_path, global)) |found| { @@ -65,7 +64,7 @@ fn findPathInner( true, ) catch |err| switch (err) { error.JSError => { - global.clearException(); + global.clearException(); // TODO sus return null; }, else => return null, diff --git a/src/bun.js/bindings/NodeTimerObject.cpp b/src/bun.js/bindings/NodeTimerObject.cpp index d3523c983d..2936bddeb4 100644 --- a/src/bun.js/bindings/NodeTimerObject.cpp +++ b/src/bun.js/bindings/NodeTimerObject.cpp @@ -19,7 +19,7 @@ using namespace JSC; static bool call(JSGlobalObject* globalObject, JSValue timerObject, JSValue callbackValue, JSValue argumentsValue) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); JSValue restoreAsyncContext {}; JSC::InternalFieldTuple* asyncContextData = nullptr; diff --git a/src/bun.js/bindings/ObjectBindings.cpp b/src/bun.js/bindings/ObjectBindings.cpp index 7ccd219cf3..fcdc4336b2 100644 --- a/src/bun.js/bindings/ObjectBindings.cpp +++ b/src/bun.js/bindings/ObjectBindings.cpp @@ -68,10 +68,12 @@ JSC::JSValue getIfPropertyExistsPrototypePollutionMitigationUnsafe(JSC::VM& vm, auto isDefined = getNonIndexPropertySlotPrototypePollutionMitigation(vm, object, globalObject, name, propertySlot); if (!isDefined) { + RETURN_IF_EXCEPTION(scope, {}); return JSValue::decode(JSC::JSValue::ValueDeleted); } - scope.assertNoException(); + scope.assertNoExceptionExceptTermination(); + RETURN_IF_EXCEPTION(scope, {}); JSValue value = propertySlot.getValue(globalObject, name); RETURN_IF_EXCEPTION(scope, {}); return value; diff --git a/src/bun.js/bindings/ProcessBindingTTYWrap.cpp b/src/bun.js/bindings/ProcessBindingTTYWrap.cpp index 9e5866e78b..2b13975d21 100644 --- a/src/bun.js/bindings/ProcessBindingTTYWrap.cpp +++ b/src/bun.js/bindings/ProcessBindingTTYWrap.cpp @@ -323,7 +323,9 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionInternalGetWindowSize, } array->putDirectIndex(globalObject, 0, jsNumber(width)); + RETURN_IF_EXCEPTION(throwScope, {}); array->putDirectIndex(globalObject, 1, jsNumber(height)); + RETURN_IF_EXCEPTION(throwScope, {}); return JSC::JSValue::encode(jsBoolean(true)); } diff --git a/src/bun.js/bindings/ZigErrorType.zig b/src/bun.js/bindings/ZigErrorType.zig index 8559dda282..ce38dbda88 100644 --- a/src/bun.js/bindings/ZigErrorType.zig +++ b/src/bun.js/bindings/ZigErrorType.zig @@ -1,6 +1,7 @@ pub const ZigErrorType = extern struct { code: ErrorCode, - ptr: ?*anyopaque, + value: bun.jsc.JSValue, }; +const bun = @import("bun"); const ErrorCode = @import("ErrorCode.zig").ErrorCode; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 6b3615dc3c..ef26ff8471 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -662,7 +662,7 @@ static String computeErrorInfoWithoutPrepareStackTrace( String& sourceURL, JSObject* errorInstance) { - + auto scope = DECLARE_THROW_SCOPE(vm); WTF::String name = "Error"_s; WTF::String message; @@ -673,7 +673,9 @@ static String computeErrorInfoWithoutPrepareStackTrace( lexicalGlobalObject = errorInstance->globalObject(); } name = instance->sanitizedNameString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); message = instance->sanitizedMessageString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); } } @@ -752,7 +754,7 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj JSArray* callSitesArray = JSC::constructArray(globalObject, globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), callSites); RETURN_IF_EXCEPTION(scope, {}); - return formatStackTraceToJSValue(vm, globalObject, lexicalGlobalObject, errorObject, callSitesArray, prepareStackTrace); + RELEASE_AND_RETURN(scope, formatStackTraceToJSValue(vm, globalObject, lexicalGlobalObject, errorObject, callSitesArray, prepareStackTrace)); } static String computeErrorInfoToString(JSC::VM& vm, Vector& stackTrace, OrdinalNumber& line, OrdinalNumber& column, String& sourceURL) @@ -2189,7 +2191,9 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig:: arguments.append(JSValue::decode(nativePtr)); auto callData = JSC::getCallData(function); - return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); + auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); + EXCEPTION_ASSERT(!!scope.exception() == !result); + return JSValue::encode(result); } extern "C" JSC::EncodedJSValue Bun__Jest__createTestModuleObject(JSC::JSGlobalObject*); @@ -2931,16 +2935,20 @@ void GlobalObject::finishCreation(VM& vm) m_JSBufferSubclassStructure.initLater( [](const Initializer& init) { + auto scope = DECLARE_CATCH_SCOPE(init.vm); auto* globalObject = static_cast(init.owner); auto* baseStructure = globalObject->typedArrayStructureWithTypedArrayType(); JSC::Structure* subclassStructure = JSC::InternalFunction::createSubclassStructure(globalObject, globalObject->JSBufferConstructor(), baseStructure); + scope.assertNoExceptionExceptTermination(); init.set(subclassStructure); }); m_JSResizableOrGrowableSharedBufferSubclassStructure.initLater( [](const Initializer& init) { + auto scope = DECLARE_CATCH_SCOPE(init.vm); auto* globalObject = static_cast(init.owner); auto* baseStructure = globalObject->resizableOrGrowableSharedTypedArrayStructureWithTypedArrayType(); JSC::Structure* subclassStructure = JSC::InternalFunction::createSubclassStructure(globalObject, globalObject->JSBufferConstructor(), baseStructure); + scope.assertNoExceptionExceptTermination(); init.set(subclassStructure); }); m_performMicrotaskFunction.initLater( @@ -3184,9 +3192,24 @@ void GlobalObject::finishCreation(VM& vm) [](const JSC::LazyProperty::Initializer& init) { auto* global = init.owner; auto& vm = init.vm; + auto scope = DECLARE_THROW_SCOPE(vm); + + // if we get the termination exception, we'd still like to set a non-null Map so that + // we don't segfault + auto setEmpty = [&]() { + ASSERT(scope.exception()); + init.set(JSC::JSMap::create(init.vm, init.owner->mapStructure())); + }; + JSMap* registry = nullptr; - if (auto loaderValue = global->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "Loader"_s))) { - if (auto registryValue = loaderValue.getObject()->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "registry"_s))) { + auto loaderValue = global->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "Loader"_s)); + scope.assertNoExceptionExceptTermination(); + RETURN_IF_EXCEPTION(scope, setEmpty()); + if (loaderValue) { + auto registryValue = loaderValue.getObject()->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "registry"_s)); + scope.assertNoExceptionExceptTermination(); + RETURN_IF_EXCEPTION(scope, setEmpty()); + if (registryValue) { registry = jsCast(registryValue); } } @@ -3613,6 +3636,7 @@ JSValue GlobalObject_getGlobalThis(VM& vm, JSObject* globalObject) void GlobalObject::addBuiltinGlobals(JSC::VM& vm) { + auto scope = DECLARE_CATCH_SCOPE(vm); m_builtinInternalFunctions.initialize(*this); auto clientData = WebCore::clientData(vm); @@ -3726,6 +3750,8 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) errorConstructor->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "prepareStackTrace"_s), JSC::CustomGetterSetter::create(vm, errorConstructorPrepareStackTraceGetter, errorConstructorPrepareStackTraceSetter), PropertyAttribute::DontEnum | PropertyAttribute::CustomValue); JSC::JSObject* consoleObject = this->get(this, JSC::Identifier::fromString(vm, "console"_s)).getObject(); + scope.assertNoExceptionExceptTermination(); + RETURN_IF_EXCEPTION(scope, ); consoleObject->putDirectBuiltinFunction(vm, this, vm.propertyNames->asyncIteratorSymbol, consoleObjectAsyncIteratorCodeGenerator(vm), PropertyAttribute::Builtin | 0); consoleObject->putDirectBuiltinFunction(vm, this, clientData->builtinNames().writePublicName(), consoleObjectWriteCodeGenerator(vm), PropertyAttribute::Builtin | 0); consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "Console"_s), CustomGetterSetter::create(vm, getConsoleConstructor, nullptr), PropertyAttribute::CustomValue | 0); @@ -4242,6 +4268,8 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb &source, typeAttributeString.isEmpty() ? nullptr : &typeAttribute); + RETURN_IF_EXCEPTION(scope, rejectedInternalPromise(globalObject, scope.exception()->value())); + ASSERT(result); if (auto* internalPromise = JSC::jsDynamicCast(result)) { return internalPromise; } else if (auto* promise = JSC::jsDynamicCast(result)) { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index a0a6b00a93..34db22da41 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -191,11 +191,34 @@ enum class AsymmetricMatcherConstructorType : int8_t { }; #if ASSERT_ENABLED -#define ASSERT_NO_PENDING_EXCEPTION(globalObject) DECLARE_THROW_SCOPE(globalObject->vm()).assertNoExceptionExceptTermination() +#define ASSERT_NO_PENDING_EXCEPTION(globalObject) DECLARE_CATCH_SCOPE(globalObject->vm()).assertNoExceptionExceptTermination() #else #define ASSERT_NO_PENDING_EXCEPTION(globalObject) void() #endif +// Ensure we instantiate the true and false variants of this function +template bool Bun__deepMatch( + JSValue objValue, + std::set* seenObjProperties, + JSValue subsetValue, + std::set* seenSubsetProperties, + JSGlobalObject* globalObject, + ThrowScope* throwScope, + MarkedArgumentBuffer* gcBuffer, + bool replacePropsWithAsymmetricMatchers, + bool isMatchingObjectContaining); + +template bool Bun__deepMatch( + JSValue objValue, + std::set* seenObjProperties, + JSValue subsetValue, + std::set* seenSubsetProperties, + JSGlobalObject* globalObject, + ThrowScope* throwScope, + MarkedArgumentBuffer* gcBuffer, + bool replacePropsWithAsymmetricMatchers, + bool isMatchingObjectContaining); + extern "C" bool Expect_readFlagsAndProcessPromise(JSC::EncodedJSValue instanceValue, JSC::JSGlobalObject* globalObject, ExpectFlags* flags, JSC::EncodedJSValue* value, AsymmetricMatcherConstructorType* constructorType); extern "C" int8_t AsymmetricMatcherConstructorType__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue) @@ -375,7 +398,9 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global } } - if (constructorObject->hasInstance(globalObject, otherProp)) { + bool hasInstance = constructorObject->hasInstance(globalObject, otherProp); + RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL); + if (hasInstance) { return AsymmetricMatcherResult::PASS; } @@ -453,10 +478,11 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global for (unsigned n = 0; n < otherLength; n++) { JSValue otherValue = otherArray->getIndex(globalObject, n); - ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm()); Vector, 16> stack; MarkedArgumentBuffer gcBuffer; - if (Bun__deepEquals(globalObject, expectedValue, otherValue, gcBuffer, stack, &scope, true)) { + bool foundNow = Bun__deepEquals(globalObject, expectedValue, otherValue, gcBuffer, stack, throwScope, true); + RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL); + if (foundNow) { found = true; break; } @@ -479,11 +505,12 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global JSValue patternObject = expectObjectContaining->m_objectValue.get(); if (patternObject.isObject()) { if (otherProp.isObject()) { - ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm()); // SAFETY: visited property sets are not required when // `enableAsymmetricMatchers` and `isMatchingObjectContaining` // are both true - if (Bun__deepMatch(otherProp, nullptr, patternObject, nullptr, globalObject, &scope, nullptr, false, true)) { + bool match = Bun__deepMatch(otherProp, nullptr, patternObject, nullptr, globalObject, throwScope, nullptr, false, true); + RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL); + if (match) { return AsymmetricMatcherResult::PASS; } } @@ -686,6 +713,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, ASSERT(c1); ASSERT(c2); std::optional isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c1, c2); + RETURN_IF_EXCEPTION(*scope, false); if (isSpecialEqual.has_value()) return std::move(*isSpecialEqual); isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c2, c1); if (isSpecialEqual.has_value()) return std::move(*isSpecialEqual); @@ -755,7 +783,9 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, JSC::PropertyNameArray a1(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Exclude); JSC::PropertyNameArray a2(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Exclude); JSObject::getOwnPropertyNames(o1, globalObject, a1, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(*scope, false); JSObject::getOwnPropertyNames(o2, globalObject, a2, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(*scope, false); size_t propertyLength = a1.size(); if constexpr (isStrict) { @@ -1000,23 +1030,28 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark } auto iter1 = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), set1, IterationKind::Keys); + RETURN_IF_EXCEPTION(*scope, {}); JSValue key1; while (iter1->next(globalObject, key1)) { - if (set2->has(globalObject, key1)) { + bool has = set2->has(globalObject, key1); + RETURN_IF_EXCEPTION(*scope, {}); + if (has) { continue; } // We couldn't find the key in the second set. This may be a false positive due to how // JSValues are represented in JSC, so we need to fall back to a linear search to be sure. auto iter2 = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), set2, IterationKind::Keys); + RETURN_IF_EXCEPTION(*scope, {}); JSValue key2; bool foundMatchingKey = false; while (iter2->next(globalObject, key2)) { - if (Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false)) { + bool equal = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); + RETURN_IF_EXCEPTION(*scope, {}); + if (equal) { foundMatchingKey = true; break; } - RETURN_IF_EXCEPTION(*scope, false); } if (!foundMatchingKey) { @@ -1040,22 +1075,26 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark } auto iter1 = JSMapIterator::create(globalObject, globalObject->mapIteratorStructure(), map1, IterationKind::Entries); + RETURN_IF_EXCEPTION(*scope, {}); JSValue key1, value1; while (iter1->nextKeyValue(globalObject, key1, value1)) { JSValue value2 = map2->get(globalObject, key1); + RETURN_IF_EXCEPTION(*scope, {}); if (value2.isUndefined()) { // We couldn't find the key in the second map. This may be a false positive due to // how JSValues are represented in JSC, so we need to fall back to a linear search // to be sure. auto iter2 = JSMapIterator::create(globalObject, globalObject->mapIteratorStructure(), map2, IterationKind::Entries); + RETURN_IF_EXCEPTION(*scope, {}); JSValue key2; bool foundMatchingKey = false; while (iter2->nextKeyValue(globalObject, key2, value2)) { - if (Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false)) { + bool keysEqual = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); + RETURN_IF_EXCEPTION(*scope, {}); + if (keysEqual) { foundMatchingKey = true; break; } - RETURN_IF_EXCEPTION(*scope, false); } if (!foundMatchingKey) { @@ -1065,7 +1104,9 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark // Compare both values below. } - if (!Bun__deepEquals(globalObject, value1, value2, gcBuffer, stack, scope, false)) { + bool valuesEqual = Bun__deepEquals(globalObject, value1, value2, gcBuffer, stack, scope, false); + RETURN_IF_EXCEPTION(*scope, {}); + if (!valuesEqual) { return false; } } @@ -1147,11 +1188,26 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark return false; } - if ( - left->errorType() != right->errorType() || // quick check on ctors (does not handle subclasses) - left->sanitizedNameString(globalObject) != right->sanitizedNameString(globalObject) || // manual `.name` changes (usually in subclasses) - left->sanitizedMessageString(globalObject) != right->sanitizedMessageString(globalObject) // `.message` - ) { + if (left->errorType() != right->errorType()) { + // quick check on ctors (does not handle subclasses) + return false; + } + + auto leftName = left->sanitizedNameString(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); + auto rightName = right->sanitizedNameString(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); + if (leftName != rightName) { + // manual `.name` changes (usually in subclasses) + return false; + } + + auto leftMessage = left->sanitizedMessageString(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); + auto rightMessage = right->sanitizedMessageString(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); + if (leftMessage != rightMessage) { + // `.message` return false; } @@ -1161,25 +1217,30 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark } } - VM& vm = globalObject->vm(); + VM& vm = JSC::getVM(globalObject); // `.cause` is non-enumerable, so it must be checked explicitly. // note that an undefined cause is different than a missing cause in // strict mode. const PropertyName cause(vm.propertyNames->cause); if constexpr (isStrict) { - if (left->hasProperty(globalObject, cause) != right->hasProperty(globalObject, cause)) { + bool leftHasCause = left->hasProperty(globalObject, cause); + RETURN_IF_EXCEPTION(*scope, {}); + bool rightHasCause = right->hasProperty(globalObject, cause); + RETURN_IF_EXCEPTION(*scope, {}); + if (leftHasCause != rightHasCause) { return false; } } auto leftCause = left->get(globalObject, cause); - RETURN_IF_EXCEPTION(*scope, false); + RETURN_IF_EXCEPTION(*scope, {}); auto rightCause = right->get(globalObject, cause); - RETURN_IF_EXCEPTION(*scope, false); - if (!Bun__deepEquals(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true)) { + RETURN_IF_EXCEPTION(*scope, {}); + bool causesEqual = Bun__deepEquals(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true); + RETURN_IF_EXCEPTION(*scope, {}); + if (!causesEqual) { return false; } - RETURN_IF_EXCEPTION(*scope, false); // check arbitrary enumerable properties. `.stack` is not checked. left->materializeErrorInfoIfNeeded(vm); @@ -1187,9 +1248,9 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark JSC::PropertyNameArray a1(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); JSC::PropertyNameArray a2(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); left->getPropertyNames(globalObject, a1, DontEnumPropertiesMode::Exclude); - RETURN_IF_EXCEPTION(*scope, false); + RETURN_IF_EXCEPTION(*scope, {}); right->getPropertyNames(globalObject, a2, DontEnumPropertiesMode::Exclude); - RETURN_IF_EXCEPTION(*scope, false); + RETURN_IF_EXCEPTION(*scope, {}); const size_t propertyArrayLength1 = a1.size(); const size_t propertyArrayLength2 = a2.size(); @@ -1207,14 +1268,11 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark PropertyName propertyName1 = PropertyName(i1); JSValue prop1 = left->get(globalObject, propertyName1); - RETURN_IF_EXCEPTION(*scope, false); - - if (!prop1) [[unlikely]] { - return false; - } + RETURN_IF_EXCEPTION(*scope, {}); + ASSERT(prop1); JSValue prop2 = right->getIfPropertyExists(globalObject, propertyName1); - RETURN_IF_EXCEPTION(*scope, false); + RETURN_IF_EXCEPTION(*scope, {}); if constexpr (!isStrict) { if (prop1.isUndefined() && prop2.isEmpty()) { @@ -1226,11 +1284,11 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark return false; } - if (!Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) { + bool propertiesEqual = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); + RETURN_IF_EXCEPTION(*scope, {}); + if (!propertiesEqual) { return false; } - - RETURN_IF_EXCEPTION(*scope, false); } // for the remaining properties in the other object, make sure they are undefined @@ -1240,7 +1298,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark PropertyName propertyName2 = PropertyName(i2); JSValue prop2 = right->getIfPropertyExists(globalObject, propertyName2); - RETURN_IF_EXCEPTION(*scope, false); + RETURN_IF_EXCEPTION(*scope, {}); if (!prop2.isUndefined()) { return false; @@ -1347,9 +1405,13 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark } JSString* s1 = c1->toStringInline(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); JSString* s2 = c2->toStringInline(globalObject); + RETURN_IF_EXCEPTION(*scope, {}); - return s1->equal(globalObject, s2); + bool stringsEqual = s1->equal(globalObject, s2); + RETURN_IF_EXCEPTION(*scope, {}); + return stringsEqual; } case JSFunctionType: { return false; @@ -1542,6 +1604,7 @@ bool Bun__deepMatch( PropertyNameArray subsetProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); subsetObj->getPropertyNames(globalObject, subsetProps, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(*throwScope, false); // TODO: add fast paths for: // - two "simple" objects (using ->forEachProperty in both) @@ -1555,6 +1618,7 @@ bool Bun__deepMatch( } PropertyNameArray objProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); obj->getPropertyNames(globalObject, objProps, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(*throwScope, false); if (objProps.size() != subsetProps.size()) { return false; } @@ -1582,6 +1646,7 @@ bool Bun__deepMatch( case AsymmetricMatcherResult::PASS: if (replacePropsWithAsymmetricMatchers) { obj->putDirectMayBeIndex(globalObject, property, subsetProp); + RETURN_IF_EXCEPTION(*throwScope, false); } // continue to next subset prop continue; @@ -1595,6 +1660,7 @@ bool Bun__deepMatch( case AsymmetricMatcherResult::PASS: if (replacePropsWithAsymmetricMatchers) { subsetObj->putDirectMayBeIndex(globalObject, property, prop); + RETURN_IF_EXCEPTION(*throwScope, false); } // continue to next subset prop continue; @@ -1647,7 +1713,8 @@ inline bool deepEqualsWrapperImpl(JSC::EncodedJSValue a, JSC::EncodedJSValue b, auto scope = DECLARE_THROW_SCOPE(vm); Vector, 16> stack; MarkedArgumentBuffer args; - return Bun__deepEquals(global, JSC::JSValue::decode(a), JSC::JSValue::decode(b), args, stack, &scope, true); + bool result = Bun__deepEquals(global, JSC::JSValue::decode(a), JSC::JSValue::decode(b), args, stack, &scope, true); + RELEASE_AND_RETURN(scope, result); } } @@ -2284,9 +2351,9 @@ double JSC__JSValue__getLengthIfPropertyExistsInternal(JSC::EncodedJSValue value if (auto* object = jsDynamicCast(cell)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + scope.release(); // zig binding handles exceptions if (JSValue lengthValue = object->getIfPropertyExists(globalObject, globalObject->vm().propertyNames->length)) { - RETURN_IF_EXCEPTION(scope, {}); - RELEASE_AND_RETURN(scope, lengthValue.toNumber(globalObject)); + return lengthValue.toNumber(globalObject); } } } @@ -2557,19 +2624,6 @@ bool JSC__JSValue__jestStrictDeepEquals(JSC::EncodedJSValue JSValue0, JSC::Encod #undef IMPL_DEEP_EQUALS_WRAPPER -bool JSC__JSValue__deepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers) -{ - JSValue obj = JSValue::decode(JSValue0); - JSValue subset = JSValue::decode(JSValue1); - - ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm()); - - std::set objVisited; - std::set subsetVisited; - MarkedArgumentBuffer gcBuffer; - return Bun__deepMatch(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false); -} - bool JSC__JSValue__jestDeepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers) { JSValue obj = JSValue::decode(JSValue0); @@ -2580,7 +2634,7 @@ bool JSC__JSValue__jestDeepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSVal std::set objVisited; std::set subsetVisited; MarkedArgumentBuffer gcBuffer; - return Bun__deepMatch(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false); + RELEASE_AND_RETURN(scope, Bun__deepMatch(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false)); } extern "C" bool Bun__JSValue__isAsyncContextFrame(JSC::EncodedJSValue value) @@ -2684,11 +2738,16 @@ JSC::EncodedJSValue JSObjectCallAsFunctionReturnValueHoldingAPILock(JSContextRef // CPP_DECL void JSC__PropertyNameArray__release(JSC__PropertyNameArray* arg0); size_t JSC__JSObject__getArrayLength(JSC::JSObject* arg0) { return arg0->getArrayLength(); } -JSC::EncodedJSValue JSC__JSObject__getIndex(JSC::EncodedJSValue jsValue, JSC::JSGlobalObject* arg1, - uint32_t arg3) +JSC::EncodedJSValue JSC__JSObject__getIndex(JSC::EncodedJSValue jsValue, JSC::JSGlobalObject* globalObject, + uint32_t index) { - ASSERT_NO_PENDING_EXCEPTION(arg1); - return JSC::JSValue::encode(JSC::JSValue::decode(jsValue).toObject(arg1)->getIndex(arg1, arg3)); + ASSERT_NO_PENDING_EXCEPTION(globalObject); + auto scope = DECLARE_THROW_SCOPE(getVM(globalObject)); + auto* object = JSC::JSValue::decode(jsValue).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto value = object->getIndex(globalObject, index); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(value); } JSC::EncodedJSValue JSC__JSValue__getDirectIndex(JSC::EncodedJSValue jsValue, JSC::JSGlobalObject* arg1, @@ -2756,10 +2815,10 @@ JSC::JSObject* JSC__JSString__toObject(JSC::JSString* arg0, JSC::JSGlobalObject* extern "C" JSC::JSInternalPromise* JSModuleLoader__import(JSC::JSGlobalObject* globalObject, const BunString* moduleNameStr) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); auto* promise = JSC::importModule(globalObject, JSC::Identifier::fromString(vm, moduleNameStr->toWTFString()), jsUndefined(), jsUndefined(), jsUndefined()); - RETURN_IF_EXCEPTION(scope, {}); + EXCEPTION_ASSERT(!!scope.exception() == !promise); return promise; } @@ -3201,7 +3260,7 @@ JSC__JSModuleLoader__loadAndEvaluateModule(JSC::JSGlobalObject* globalObject, const BunString* arg1) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); auto name = makeAtomString(arg1->toWTFString()); auto* promise = JSC::loadAndEvaluateModule(globalObject, name, JSC::jsUndefined(), JSC::jsUndefined()); @@ -3212,7 +3271,9 @@ JSC__JSModuleLoader__loadAndEvaluateModule(JSC::JSGlobalObject* globalObject, JSC::JSNativeStdFunction* resolverFunction = JSC::JSNativeStdFunction::create( vm, globalObject, 1, String(), resolverFunctionCallback); - return promise->then(globalObject, resolverFunction, nullptr); + auto* newPromise = promise->then(globalObject, resolverFunction, nullptr); + EXCEPTION_ASSERT(!!scope.exception() == !newPromise); + return newPromise; } #pragma mark - JSC::JSPromise @@ -3231,11 +3292,13 @@ void JSC__AnyPromise__wrap(JSC::JSGlobalObject* globalObject, EncodedJSValue enc if (auto* promise = jsDynamicCast(promiseValue)) { promise->reject(globalObject, exception->value()); + RETURN_IF_EXCEPTION(scope, ); return; } if (auto* promise = jsDynamicCast(promiseValue)) { promise->reject(globalObject, exception->value()); + RETURN_IF_EXCEPTION(scope, ); return; } @@ -3245,11 +3308,13 @@ void JSC__AnyPromise__wrap(JSC::JSGlobalObject* globalObject, EncodedJSValue enc if (auto* errorInstance = jsDynamicCast(result)) { if (auto* promise = jsDynamicCast(promiseValue)) { promise->reject(globalObject, errorInstance); + RETURN_IF_EXCEPTION(scope, ); return; } if (auto* promise = jsDynamicCast(promiseValue)) { promise->reject(globalObject, errorInstance); + RETURN_IF_EXCEPTION(scope, ); return; } @@ -3258,10 +3323,12 @@ void JSC__AnyPromise__wrap(JSC::JSGlobalObject* globalObject, EncodedJSValue enc if (auto* promise = jsDynamicCast(promiseValue)) { promise->resolve(globalObject, result); + RETURN_IF_EXCEPTION(scope, ); return; } if (auto* promise = jsDynamicCast(promiseValue)) { promise->resolve(globalObject, result); + RETURN_IF_EXCEPTION(scope, ); return; } @@ -3271,24 +3338,24 @@ void JSC__AnyPromise__wrap(JSC::JSGlobalObject* globalObject, EncodedJSValue enc JSC::EncodedJSValue JSC__JSPromise__wrap(JSC::JSGlobalObject* globalObject, void* ctx, JSC::EncodedJSValue (*func)(void*, JSC::JSGlobalObject*)) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_CATCH_SCOPE(vm); + auto scope = DECLARE_THROW_SCOPE(vm); JSValue result = JSC::JSValue::decode(func(ctx, globalObject)); if (scope.exception()) { auto* exception = scope.exception(); scope.clearException(); - return JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, exception->value())); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, exception->value()))); } if (auto* promise = jsDynamicCast(result)) { - return JSValue::encode(promise); + RELEASE_AND_RETURN(scope, JSValue::encode(promise)); } if (JSC::ErrorInstance* err = jsDynamicCast(result)) { - return JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, err)); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, err))); } - return JSValue::encode(JSC::JSPromise::resolvedPromise(globalObject, result)); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::resolvedPromise(globalObject, result))); } void JSC__JSPromise__reject(JSC::JSPromise* arg0, JSC::JSGlobalObject* globalObject, @@ -3974,15 +4041,16 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__getOwn(JSC::EncodedJSValue JSValue0 ASSERT_NO_PENDING_EXCEPTION(globalObject); VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); JSValue value = JSC::JSValue::decode(JSValue0); WTF::String propertyNameString = propertyName->tag == BunStringTag::Empty ? WTF::emptyString() : propertyName->toWTFString(BunString::ZeroCopy); auto identifier = JSC::Identifier::fromString(vm, propertyNameString); auto property = JSC::PropertyName(identifier); PropertySlot slot(value, PropertySlot::InternalMethodType::GetOwnProperty); if (value.getOwnPropertySlot(globalObject, property, slot)) { - return JSValue::encode(slot.getValue(globalObject, property)); + RELEASE_AND_RETURN(scope, JSValue::encode(slot.getValue(globalObject, property))); } - return JSValue::encode({}); + RELEASE_AND_RETURN(scope, JSValue::encode({})); } JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue arg1) @@ -3995,10 +4063,13 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu if (path.isString()) { String pathString = path.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); uint32_t length = pathString.length(); if (length == 0) { - JSValue prop = value.toObject(globalObject)->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); + auto* valueObject = value.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + JSValue prop = valueObject->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(prop); } @@ -4013,7 +4084,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu // if "." is the only character, it will search for an empty string twice. if (pathString.characterAt(0) == '.') { - currProp = currProp.toObject(globalObject)->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); + auto* currPropObject = currProp.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + currProp = currPropObject->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); RETURN_IF_EXCEPTION(scope, {}); if (currProp.isEmpty()) { return JSValue::encode(currProp); @@ -4027,7 +4100,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu if (i == length) { if (ic == '.') { - currProp = currProp.toObject(globalObject)->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); + auto* currPropObject = currProp.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + currProp = currPropObject->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(currProp); } @@ -4043,7 +4118,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu UChar previous = ic; ic = pathString.characterAt(i); if (previous == '.' && ic == '.') { - currProp = currProp.toObject(globalObject)->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); + auto* currPropObject = currProp.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + currProp = currPropObject->getIfPropertyExists(globalObject, PropertyName(Identifier::EmptyIdentifier)); RETURN_IF_EXCEPTION(scope, {}); if (currProp.isEmpty()) { return JSValue::encode(currProp); @@ -4066,7 +4143,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu String propNameStr = pathString.substring(i, j - i); PropertyName propName = PropertyName(Identifier::fromString(vm, propNameStr)); - currProp = currProp.toObject(globalObject)->getIfPropertyExists(globalObject, propName); + auto* currPropObject = currProp.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + currProp = currPropObject->getIfPropertyExists(globalObject, propName); RETURN_IF_EXCEPTION(scope, {}); if (currProp.isEmpty()) { return JSValue::encode(currProp); @@ -4081,7 +4160,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu if (isArray(globalObject, path)) { // each item in array is property name, ignore dot/bracket notation JSValue currProp = value; - forEachInArrayLike(globalObject, path.toObject(globalObject), [&](JSValue item) -> bool { + auto* pathObject = path.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + forEachInArrayLike(globalObject, pathObject, [&](JSValue item) -> bool { if (!(item.isString() || item.isNumber())) { currProp = {}; return false; @@ -4092,7 +4173,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu PropertyName propName = PropertyName(propNameString->toIdentifier(globalObject)); RETURN_IF_EXCEPTION(scope, {}); - currProp = currProp.toObject(globalObject)->getIfPropertyExists(globalObject, propName); + auto* currPropObject = currProp.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + currProp = currPropObject->getIfPropertyExists(globalObject, propName); RETURN_IF_EXCEPTION(scope, {}); if (currProp.isEmpty()) { return false; @@ -4100,7 +4183,7 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu return true; }); - + RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(currProp); } @@ -4648,13 +4731,13 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, bool getFromSourceURL = false; if (stackTrace != nullptr && stackTrace->size() > 0) { populateStackTrace(vm, *stackTrace, &except->stack, global, flags); - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } } else if (err->stackTrace() != nullptr && err->stackTrace()->size() > 0) { populateStackTrace(vm, *err->stackTrace(), &except->stack, global, flags); - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } } else { getFromSourceURL = true; @@ -4668,17 +4751,26 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } if (except->type == SYNTAX_ERROR_CODE) { except->message = Bun::toStringRef(err->sanitizedMessageString(global)); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; + } } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { except->message = Bun::toStringRef(global, message); } else { except->message = Bun::toStringRef(err->sanitizedMessageString(global)); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; + } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } except->name = Bun::toStringRef(err->sanitizedNameString(global)); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; + } except->runtime_type = err->runtimeTypeForCause(); @@ -4691,8 +4783,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } if (JSC::JSValue code = getNonObservable(vm, global, obj, names.codePublicName())) { @@ -4701,8 +4793,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } if (JSC::JSValue path = getNonObservable(vm, global, obj, names.pathPublicName())) { @@ -4711,8 +4803,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } if (JSC::JSValue fd = getNonObservable(vm, global, obj, names.fdPublicName())) { @@ -4721,8 +4813,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } if (JSC::JSValue errno_ = getNonObservable(vm, global, obj, names.errnoPublicName())) { @@ -4731,8 +4823,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (scope.exception()) [[unlikely]] { - scope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } } @@ -4742,14 +4834,12 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, // we don't want to serialize JSC::StackFrame longer than we need to // so in this case, we parse the stack trace as a string - auto catchScope = DECLARE_CATCH_SCOPE(vm); - // This one intentionally calls getters. if (JSC::JSValue stackValue = obj->getIfPropertyExists(global, vm.propertyNames->stack)) { if (stackValue.isString()) { WTF::String stack = stackValue.toWTFString(global); - if (catchScope.exception()) [[unlikely]] { - catchScope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } if (!stack.isEmpty()) { @@ -4793,8 +4883,8 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } - if (catchScope.exception()) [[unlikely]] { - catchScope.clearExceptionExceptTermination(); + if (!scope.clearExceptionExceptTermination()) [[unlikely]] { + return; } } @@ -5519,10 +5609,10 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: return; auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); + auto throwScopeForStackOverflowException = DECLARE_THROW_SCOPE(vm); if (!vm.isSafeToRecurse()) [[unlikely]] { - throwStackOverflowError(globalObject, throwScope); + throwStackOverflowError(globalObject, throwScopeForStackOverflowException); return; } @@ -5776,11 +5866,11 @@ void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSG return; auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_CATCH_SCOPE(vm); JSC::PropertyNameArray properties(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); { - auto scope = DECLARE_CATCH_SCOPE(vm); JSC::JSObject::getOwnPropertyNames(object, globalObject, properties, DontEnumPropertiesMode::Include); if (scope.exception()) { scope.clearException(); @@ -5805,8 +5895,11 @@ void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSG continue; JSC::PropertySlot slot(object, PropertySlot::InternalMethodType::Get); - if (!object->getPropertySlot(globalObject, property, slot)) + bool hasProperty = object->getPropertySlot(globalObject, property, slot); + scope.clearException(); + if (!hasProperty) { continue; + } if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == vm.propertyNames->underscoreProto @@ -5815,7 +5908,6 @@ void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSG } JSC::JSValue propertyValue = jsUndefined(); - auto scope = DECLARE_CATCH_SCOPE(vm); if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if ((slot.attributes() & PropertyAttribute::Accessor) != 0) { propertyValue = slot.getPureResult(); @@ -5843,6 +5935,7 @@ void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSG ZigString key = toZigString(name); JSC::EnsureStillAliveScope ensureStillAliveScope(propertyValue); + // TODO: properly propagate exception upwards iter(globalObject, arg2, &key, JSC::JSValue::encode(propertyValue), property.isSymbol(), property.isPrivateName()); } properties.releaseData(); @@ -6425,7 +6518,7 @@ extern "C" EncodedJSValue Bun__JSObject__getCodePropertyVMInquiry(JSC::JSGlobalO } auto& vm = global->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); if (object->type() == JSC::ProxyObjectType) [[unlikely]] { return {}; } @@ -6433,9 +6526,9 @@ extern "C" EncodedJSValue Bun__JSObject__getCodePropertyVMInquiry(JSC::JSGlobalO auto& builtinNames = WebCore::builtinNames(vm); PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, &vm); - ASSERT(!scope.exception()); + scope.assertNoExceptionExceptTermination(); if (!object->getNonIndexPropertySlot(global, builtinNames.codePublicName(), slot)) { - ASSERT(!scope.exception()); + scope.assertNoExceptionExceptTermination(); return {}; } diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index d4b56c87fc..c2b7184183 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -85,7 +85,7 @@ typedef struct BunString { typedef struct ZigErrorType { ZigErrorCode code; - void* ptr; + JSC::EncodedJSValue value; } ZigErrorType; typedef union ErrorableZigStringResult { ZigString value; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 9abc67f3b4..cfbb1c7f81 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -212,7 +212,6 @@ CPP_DECL JSC::EncodedJSValue JSC__JSValue__createStringArray(JSC::JSGlobalObject CPP_DECL JSC::EncodedJSValue JSC__JSValue__createTypeError(const ZigString* arg0, const ZigString* arg1, JSC::JSGlobalObject* arg2); CPP_DECL JSC::EncodedJSValue JSC__JSValue__createUninitializedUint8Array(JSC::JSGlobalObject* arg0, size_t arg1); CPP_DECL bool JSC__JSValue__deepEquals(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* arg2); -CPP_DECL bool JSC__JSValue__deepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* arg2, bool arg3); CPP_DECL bool JSC__JSValue__eqlCell(JSC::EncodedJSValue JSValue0, JSC::JSCell* arg1); CPP_DECL bool JSC__JSValue__eqlValue(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1); CPP_DECL JSC::EncodedJSValue JSC__JSValue__fastGet(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, unsigned char arg2); @@ -325,16 +324,6 @@ CPP_DECL void JSC__VM__throwError(JSC::VM* arg0, JSC::JSGlobalObject* arg1, JSC: CPP_DECL void JSC__VM__throwError(JSC::VM* arg0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2); CPP_DECL void JSC__VM__whenIdle(JSC::VM* arg0, void(* ArgFn1)()); -#pragma mark - JSC::ThrowScope - -CPP_DECL void JSC__ThrowScope__clearException(JSC::ThrowScope* arg0); -CPP_DECL JSC::Exception* JSC__ThrowScope__exception(JSC::ThrowScope* arg0); -CPP_DECL void JSC__ThrowScope__release(JSC::ThrowScope* arg0); - -#pragma mark - JSC::CatchScope - -CPP_DECL void JSC__CatchScope__clearException(JSC::CatchScope* arg0); -CPP_DECL JSC::Exception* JSC__CatchScope__exception(JSC::CatchScope* arg0); CPP_DECL void FFI__ptr__put(JSC::JSGlobalObject* arg0, JSC::EncodedJSValue JSValue1); #ifdef __cplusplus diff --git a/src/bun.js/bindings/helpers.h b/src/bun.js/bindings/helpers.h index 1c70272776..8314fa81b2 100644 --- a/src/bun.js/bindings/helpers.h +++ b/src/bun.js/bindings/helpers.h @@ -273,7 +273,7 @@ static WTF::StringView toStringView(ZigString str) static void throwException(JSC::ThrowScope& scope, ZigErrorType err, JSC::JSGlobalObject* global) { scope.throwException(global, - JSC::Exception::create(global->vm(), JSC::JSValue((JSC::JSCell*)err.ptr))); + JSC::Exception::create(global->vm(), JSC::JSValue::decode(err.value))); } static ZigString toZigString(JSC::JSValue val, JSC::JSGlobalObject* global) diff --git a/src/bun.js/bindings/node/http/JSConnectionsList.cpp b/src/bun.js/bindings/node/http/JSConnectionsList.cpp index 20ee467ea9..bdc549c83c 100644 --- a/src/bun.js/bindings/node/http/JSConnectionsList.cpp +++ b/src/bun.js/bindings/node/http/JSConnectionsList.cpp @@ -60,6 +60,7 @@ JSArray* JSConnectionsList::all(JSGlobalObject* globalObject) RETURN_IF_EXCEPTION(scope, {}); auto iter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), all, IterationKind::Keys); + RETURN_IF_EXCEPTION(scope, nullptr); JSValue item; size_t i = 0; @@ -85,6 +86,7 @@ JSArray* JSConnectionsList::idle(JSGlobalObject* globalObject) RETURN_IF_EXCEPTION(scope, {}); auto iter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), all, IterationKind::Keys); + RETURN_IF_EXCEPTION(scope, nullptr); JSValue item; size_t i = 0; @@ -112,6 +114,7 @@ JSArray* JSConnectionsList::active(JSGlobalObject* globalObject) RETURN_IF_EXCEPTION(scope, {}); auto iter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), active, IterationKind::Keys); + RETURN_IF_EXCEPTION(scope, nullptr); JSValue item; size_t i = 0; @@ -137,6 +140,7 @@ JSArray* JSConnectionsList::expired(JSGlobalObject* globalObject, uint64_t heade RETURN_IF_EXCEPTION(scope, {}); auto iter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), active, IterationKind::Keys); + RETURN_IF_EXCEPTION(scope, nullptr); JSValue item = iter->next(vm); size_t i = 0; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 100aa7a6cc..a583403ba0 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -70,20 +70,20 @@ pub fn exit(this: *EventLoop) void { defer this.debug.exit(); if (count == 1 and !this.virtual_machine.is_inside_deferred_task_queue) { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); + this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc) catch {}; } this.entered_event_loop_count -= 1; } -pub fn exitMaybeDrainMicrotasks(this: *EventLoop, allow_drain_microtask: bool) void { +pub fn exitMaybeDrainMicrotasks(this: *EventLoop, allow_drain_microtask: bool) bun.JSExecutionTerminated!void { const count = this.entered_event_loop_count; log("exit() = {d}", .{count - 1}); defer this.debug.exit(); if (allow_drain_microtask and count == 1 and !this.virtual_machine.is_inside_deferred_task_queue) { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); + try this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); } this.entered_event_loop_count -= 1; @@ -107,11 +107,15 @@ pub fn tickWhilePaused(this: *EventLoop, done: *bool) void { } extern fn JSC__JSGlobalObject__drainMicrotasks(*JSC.JSGlobalObject) void; -pub fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalObject, jsc_vm: *JSC.VM) void { +pub fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalObject, jsc_vm: *JSC.VM) bun.JSExecutionTerminated!void { JSC.markBinding(@src()); + var scope: JSC.CatchScope = undefined; + scope.init(globalObject, @src(), .enabled); + defer scope.deinit(); jsc_vm.releaseWeakRefs(); JSC__JSGlobalObject__drainMicrotasks(globalObject); + try scope.assertNoExceptionExceptTermination(); this.virtual_machine.is_inside_deferred_task_queue = true; this.deferred_tasks.run(); @@ -122,14 +126,14 @@ pub fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalOb } } -pub fn drainMicrotasks(this: *EventLoop) void { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); +pub fn drainMicrotasks(this: *EventLoop) bun.JSExecutionTerminated!void { + try this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); } // should be called after exit() pub fn maybeDrainMicrotasks(this: *EventLoop) void { if (this.entered_event_loop_count == 0 and !this.virtual_machine.is_inside_deferred_task_queue) { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); + this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc) catch {}; } } @@ -444,6 +448,9 @@ pub fn processGCTimer(this: *EventLoop) void { pub fn tick(this: *EventLoop) void { JSC.markBinding(@src()); + var scope: JSC.CatchScope = undefined; + scope.init(this.global, @src(), .enabled); + defer scope.deinit(); this.entered_event_loop_count += 1; this.debug.enter(); defer { @@ -462,7 +469,8 @@ pub fn tick(this: *EventLoop) void { while (this.tickWithCount(ctx) > 0) : (this.global.handleRejectedPromises()) { this.tickConcurrent(); } else { - this.drainMicrotasksWithGlobal(global, global_vm); + this.drainMicrotasksWithGlobal(global, global_vm) catch return; + if (scope.hasException()) return; this.tickConcurrent(); if (this.tasks.count > 0) continue; } diff --git a/src/bun.js/event_loop/Task.zig b/src/bun.js/event_loop/Task.zig index c29a78de2a..b35e534d62 100644 --- a/src/bun.js/event_loop/Task.zig +++ b/src/bun.js/event_loop/Task.zig @@ -95,7 +95,7 @@ pub const Task = TaggedPointerUnion(.{ pub fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u32 { var global = this.global; const global_vm = global.vm(); - var counter: usize = 0; + var counter: u32 = 0; if (comptime Environment.isDebug) { if (this.debug.js_call_count_outside_tick_queue > this.debug.drain_microtasks_count_outside_tick_queue) { @@ -460,7 +460,7 @@ pub fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u3 }, @field(Task.Tag, @typeName(RuntimeTranspilerStore)) => { var any: *RuntimeTranspilerStore = task.get(RuntimeTranspilerStore).?; - any.drain(); + any.drain() catch {}; }, @field(Task.Tag, @typeName(ServerAllConnectionsClosedTask)) => { var any: *ServerAllConnectionsClosedTask = task.get(ServerAllConnectionsClosedTask).?; @@ -485,16 +485,20 @@ pub fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u3 any.runFromJSThread(); }, - else => { + .@"shell.builtin.yes.YesTask", .@"bun.js.api.Timer.ImmediateObject", .@"bun.js.api.Timer.TimeoutObject" => { bun.Output.panic("Unexpected tag: {s}", .{@tagName(task.tag())}); }, + _ => { + // handle unnamed variants + bun.Output.panic("Unknown tag: {d}", .{@intFromEnum(task.tag())}); + }, } - this.drainMicrotasksWithGlobal(global, global_vm); + this.drainMicrotasksWithGlobal(global, global_vm) catch return counter; } this.tasks.head = if (this.tasks.count == 0) 0 else this.tasks.head; - return @as(u32, @truncate(counter)); + return counter; } const TaggedPointerUnion = bun.TaggedPointerUnion; diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 3e43f8151f..f3ae7a053d 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -356,7 +356,7 @@ pub const CallbackList = union(enum) { }, } } - fn callNextTick(self: *@This(), global: *JSC.JSGlobalObject) void { + fn callNextTick(self: *@This(), global: *JSC.JSGlobalObject) bun.JSError!void { switch (self.*) { .ack_nack => {}, .none => {}, @@ -366,8 +366,8 @@ pub const CallbackList = union(enum) { self.* = .none; }, .callback_array => { - var iter = self.callback_array.arrayIterator(global); - while (iter.next()) |item| { + var iter = try self.callback_array.arrayIterator(global); + while (try iter.next()) |item| { item.callNextTick(global, .{.null}); } self.callback_array.unprotect(); @@ -399,8 +399,8 @@ pub const SendHandle = struct { /// Call the callback and deinit pub fn complete(self: *SendHandle, global: *JSC.JSGlobalObject) void { - self.callbacks.callNextTick(global); - self.deinit(); + defer self.deinit(); + self.callbacks.callNextTick(global) catch {}; // TODO: properly propagate exception upwards } pub fn deinit(self: *SendHandle) void { self.data.deinit(); @@ -981,8 +981,8 @@ pub fn doSend(ipc: ?*SendQueue, globalObject: *JSC.JSGlobalObject, callFrame: *J if (serialized_array.isUndefinedOrNull()) { handle = .js_undefined; } else { - const serialized_handle = serialized_array.getIndex(globalObject, 0); - const serialized_message = serialized_array.getIndex(globalObject, 1); + const serialized_handle = try serialized_array.getIndex(globalObject, 0); + const serialized_message = try serialized_array.getIndex(globalObject, 1); handle = serialized_handle; message = serialized_message; } diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 36b43620f8..db9e03fc76 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -33,8 +33,8 @@ pub const JSHostFnZigWithContext = host_fn.JSHostFnZigWithContext; pub const JSHostFunctionTypeWithContext = host_fn.JSHostFunctionTypeWithContext; pub const toJSHostFn = host_fn.toJSHostFn; pub const toJSHostFnWithContext = host_fn.toJSHostFnWithContext; -pub const toJSHostValue = host_fn.toJSHostValue; -pub const fromJSHostValue = host_fn.fromJSHostValue; +pub const toJSHostCall = host_fn.toJSHostCall; +pub const fromJSHostCall = host_fn.fromJSHostCall; pub const createCallback = host_fn.createCallback; // JSC Classes Bindings @@ -79,6 +79,7 @@ pub const Weak = @import("Weak.zig").Weak; pub const WeakRefType = @import("Weak.zig").WeakRefType; pub const Exception = @import("bindings/Exception.zig").Exception; pub const SourceProvider = @import("bindings/SourceProvider.zig").SourceProvider; +pub const CatchScope = @import("bindings/CatchScope.zig"); // JavaScript-related pub const Errorable = @import("bindings/Errorable.zig").Errorable; diff --git a/src/bun.js/jsc/host_fn.zig b/src/bun.js/jsc/host_fn.zig index a563783e63..9bba8d08f5 100644 --- a/src/bun.js/jsc/host_fn.zig +++ b/src/bun.js/jsc/host_fn.zig @@ -81,20 +81,44 @@ pub fn toJSHostSetterValue(globalThis: *JSGlobalObject, value: error{ OutOfMemor return true; } -pub fn toJSHostValue(globalThis: *JSGlobalObject, value: error{ OutOfMemory, JSError }!JSValue) JSValue { - const normal = value catch |err| switch (err) { +/// Convert the return value of a function returning an error union into a maybe-empty JSValue +pub fn toJSHostCall( + globalThis: *JSGlobalObject, + src: std.builtin.SourceLocation, + comptime function: anytype, + // This can't use std.meta.ArgsTuple because that will turn comptime function parameters into + // runtime tuple values + args: anytype, +) JSValue { + var scope: jsc.CatchScope = undefined; + scope.init(globalThis, src, .assertions_only); + defer scope.deinit(); + + const returned: error{ OutOfMemory, JSError }!JSValue = @call(.auto, function, args); + const normal = returned catch |err| switch (err) { error.JSError => .zero, error.OutOfMemory => globalThis.throwOutOfMemoryValue(), }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(globalThis, normal, toJSHostValue); - } + scope.assertExceptionPresenceMatches(normal == .zero); return normal; } -pub fn fromJSHostValue(value: JSValue) bun.JSError!JSValue { - if (value == .zero) return error.JSError; - return value; +/// Convert the return value of a function returning a maybe-empty JSValue into an error union. +/// The wrapped function must return an empty JSValue if and only if it has thrown an exception. +pub fn fromJSHostCall( + globalThis: *JSGlobalObject, + /// For attributing thrown exceptions + src: std.builtin.SourceLocation, + comptime function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) bun.JSError!JSValue { + var scope: jsc.CatchScope = undefined; + scope.init(globalThis, src, .assertions_only); + defer scope.deinit(); + + const value = @call(.auto, function, args); + scope.assertExceptionPresenceMatches(value == .zero); + return if (value == .zero) error.JSError else value; } const ParsedHostFunctionErrorSet = struct { @@ -122,14 +146,7 @@ pub fn wrap1(comptime func: anytype) @"return": { const p = @typeInfo(@TypeOf(func)).@"fn".params; return struct { pub fn wrapped(arg0: p[0].type.?) callconv(.c) JSValue { - const value = func(arg0) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => arg0.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(arg0, value, func); - } - return value; + return toJSHostCall(arg0, @src(), func, .{arg0}); } }.wrapped; } @@ -141,14 +158,7 @@ pub fn wrap2(comptime func: anytype) @"return": { const p = @typeInfo(@TypeOf(func)).@"fn".params; return struct { pub fn wrapped(arg0: p[0].type.?, arg1: p[1].type.?) callconv(.c) JSValue { - const value = func(arg0, arg1) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => arg0.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(arg0, value, func); - } - return value; + return toJSHostCall(arg0, @src(), func, .{ arg0, arg1 }); } }.wrapped; } @@ -160,14 +170,7 @@ pub fn wrap3(comptime func: anytype) @"return": { const p = @typeInfo(@TypeOf(func)).@"fn".params; return struct { pub fn wrapped(arg0: p[0].type.?, arg1: p[1].type.?, arg2: p[2].type.?) callconv(.c) JSValue { - const value = func(arg0, arg1, arg2) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => arg0.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(arg0, value, func); - } - return value; + return toJSHostCall(arg0, @src(), func, .{ arg0, arg1, arg2 }); } }.wrapped; } @@ -179,14 +182,7 @@ pub fn wrap4(comptime func: anytype) @"return": { const p = @typeInfo(@TypeOf(func)).@"fn".params; return struct { pub fn wrapped(arg0: p[0].type.?, arg1: p[1].type.?, arg2: p[2].type.?, arg3: p[3].type.?) callconv(.c) JSValue { - const value = func(arg0, arg1, arg2, arg3) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => arg0.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(arg0, value, func); - } - return value; + return toJSHostCall(arg0, @src(), func, .{ arg0, arg1, arg2, arg3 }); } }.wrapped; } @@ -198,14 +194,7 @@ pub fn wrap5(comptime func: anytype) @"return": { const p = @typeInfo(@TypeOf(func)).@"fn".params; return struct { pub fn wrapped(arg0: p[0].type.?, arg1: p[1].type.?, arg2: p[2].type.?, arg3: p[3].type.?, arg4: p[4].type.?) callconv(.c) JSValue { - const value = func(arg0, arg1, arg2, arg3, arg4) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => arg0.throwOutOfMemoryValue(), - }; - if (Environment.allow_assert and Environment.is_canary) { - debugExceptionAssertion(arg0, value, func); - } - return value; + return toJSHostCall(arg0, @src(), func, .{ arg0, arg1, arg2, arg3, arg4 }); } }.wrapped; } @@ -469,7 +458,7 @@ pub fn DOMCall( arguments_ptr: [*]const jsc.JSValue, arguments_len: usize, ) callconv(jsc.conv) jsc.JSValue { - return jsc.toJSHostValue(globalObject, @field(Container, functionName)(globalObject, thisValue, arguments_ptr[0..arguments_len])); + return jsc.toJSHostCall(globalObject, @src(), @field(Container, functionName), .{ globalObject, thisValue, arguments_ptr[0..arguments_len] }); } pub const fastpath = @field(Container, functionName ++ "WithoutTypeChecks"); diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h index 509cd96180..de0e5750b9 100644 --- a/src/bun.js/modules/BunJSCModule.h +++ b/src/bun.js/modules/BunJSCModule.h @@ -584,8 +584,11 @@ JSC_DEFINE_HOST_FUNCTION(functionDrainMicrotasks, (JSGlobalObject * globalObject, CallFrame*)) { VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); vm.drainMicrotasks(); + RETURN_IF_EXCEPTION(scope, {}); Bun__drainMicrotasks(); + RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(jsUndefined()); } diff --git a/src/bun.js/modules/NodeBufferModule.h b/src/bun.js/modules/NodeBufferModule.h index eba221af55..8542ac4dcf 100644 --- a/src/bun.js/modules/NodeBufferModule.h +++ b/src/bun.js/modules/NodeBufferModule.h @@ -158,6 +158,7 @@ JSC_DEFINE_CUSTOM_SETTER(jsSetter_INSPECT_MAX_BYTES, (JSGlobalObject * lexicalGl DEFINE_NATIVE_MODULE(NodeBuffer) { INIT_NATIVE_MODULE(12); + auto scope = DECLARE_THROW_SCOPE(vm); put(JSC::Identifier::fromString(vm, "Buffer"_s), globalObject->JSBufferConstructor()); @@ -192,9 +193,11 @@ DEFINE_NATIVE_MODULE(NodeBuffer) JSC::Identifier atobI = JSC::Identifier::fromString(vm, "atob"_s); JSC::JSValue atobV = lexicalGlobalObject->get(globalObject, PropertyName(atobI)); + RETURN_IF_EXCEPTION(scope, ); JSC::Identifier btoaI = JSC::Identifier::fromString(vm, "btoa"_s); JSC::JSValue btoaV = lexicalGlobalObject->get(globalObject, PropertyName(btoaI)); + RETURN_IF_EXCEPTION(scope, ); put(atobI, atobV); put(btoaI, btoaV); diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 2dedf0cc5d..6a4b9aa8f5 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -129,7 +129,7 @@ fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { while (line_iter.next()) |line| { if (strings.hasPrefixComptime(line, key_processor)) { if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + const cpu = try values.getIndex(globalThis, cpu_index); cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); } // If this line starts a new processor, parse the index from the line @@ -140,26 +140,26 @@ fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue { } else if (strings.hasPrefixComptime(line, key_model_name)) { // If this is the model name, extract it and store on the current cpu const model_name = line[key_model_name.len..]; - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + const cpu = try values.getIndex(globalThis, cpu_index); cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.init(model_name).withEncoding().toJS(globalThis)); has_model_name = true; } } if (!has_model_name) { - const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index); + const cpu = try values.getIndex(globalThis, cpu_index); cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); } } else |_| { // Initialize model name to "unknown" - var it = values.arrayIterator(globalThis); - while (it.next()) |cpu| { + var it = try values.arrayIterator(globalThis); + while (try it.next()) |cpu| { cpu.put(globalThis, JSC.ZigString.static("model"), JSC.ZigString.static("unknown").withEncoding().toJS(globalThis)); } } // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) for (0..num_cpus) |cpu_index| { - const cpu = JSC.JSObject.getIndex(values, globalThis, @truncate(cpu_index)); + const cpu = try values.getIndex(globalThis, @truncate(cpu_index)); var path_buf: [128]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index}); @@ -632,9 +632,9 @@ fn networkInterfacesPosix(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSVal } // Does this entry already exist? - if (ret.get_unsafe(globalThis, interface_name)) |array| { + if (try ret.get(globalThis, interface_name)) |array| { // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); + const next_index: u32 = @intCast(try array.getLength(globalThis)); array.putIndex(globalThis, next_index, interface); } else { // Add it as an array with this interface as an element @@ -746,9 +746,9 @@ fn networkInterfacesWindows(globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSV // Does this entry already exist? const interface_name = bun.span(iface.name); - if (ret.get_unsafe(globalThis, interface_name)) |array| { + if (try ret.get(globalThis, interface_name)) |array| { // Add this interface entry to the existing array - const next_index = @as(u32, @intCast(array.getLength(globalThis))); + const next_index: u32 = @intCast(try array.getLength(globalThis)); array.putIndex(globalThis, next_index, interface); } else { // Add it as an array with this interface as an element diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index bb6c23d898..7647189c26 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -193,9 +193,7 @@ pub fn getExecArgv(global: *JSGlobalObject) callconv(.c) JSValue { return Bun__Process__getExecArgv(global); } -pub fn getCwd(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - return JSC.toJSHostValue(globalObject, getCwd_(globalObject)); -} +pub const getCwd = JSC.host_fn.wrap1(getCwd_); fn getCwd_(globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { var buf: bun.PathBuffer = undefined; switch (bun.api.node.path.getCwd(&buf)) { @@ -206,9 +204,7 @@ fn getCwd_(globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { } } -pub fn setCwd(globalObject: *JSC.JSGlobalObject, to: *JSC.ZigString) callconv(.C) JSC.JSValue { - return JSC.toJSHostValue(globalObject, setCwd_(globalObject, to)); -} +pub const setCwd = JSC.host_fn.wrap2(setCwd_); fn setCwd_(globalObject: *JSC.JSGlobalObject, to: *JSC.ZigString) bun.JSError!JSC.JSValue { if (to.len == 0) { return globalObject.throwInvalidArguments("Expected path to be a non-empty string", .{}); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 7df8e8d663..64ae53630c 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -800,11 +800,11 @@ pub const VectorArrayBuffer = struct { var bufferlist = std.ArrayList(bun.PlatformIOVec).init(allocator); var i: usize = 0; - const len = val.getLength(globalObject); + const len = try val.getLength(globalObject); bufferlist.ensureTotalCapacityPrecise(len) catch bun.outOfMemory(); while (i < len) { - const element = val.getIndex(globalObject, @as(u32, @truncate(i))); + const element = try val.getIndex(globalObject, @as(u32, @truncate(i))); if (!element.isCell()) { return globalObject.throwInvalidArguments("Expected ArrayBufferView[]", .{}); diff --git a/src/bun.js/node/util/parse_args.zig b/src/bun.js/node/util/parse_args.zig index be9f380498..83932a4518 100644 --- a/src/bun.js/node/util/parse_args.zig +++ b/src/bun.js/node/util/parse_args.zig @@ -24,7 +24,7 @@ const ArgsSlice = struct { start: u32, end: u32, - pub inline fn get(this: ArgsSlice, globalThis: *JSGlobalObject, i: u32) JSValue { + pub inline fn get(this: ArgsSlice, globalThis: *JSGlobalObject, i: u32) bun.JSError!JSValue { return this.array.getIndex(globalThis, this.start + i); } }; @@ -157,8 +157,8 @@ fn getDefaultArgs(globalThis: *JSGlobalObject) !ArgsSlice { const exec_argv = bun.api.node.process.getExecArgv(globalThis); const argv = bun.api.node.process.getArgv(globalThis); if (argv.isArray() and exec_argv.isArray()) { - var iter = exec_argv.arrayIterator(globalThis); - while (iter.next()) |item| { + var iter = try exec_argv.arrayIterator(globalThis); + while (try iter.next()) |item| { if (item.isString()) { const str = try item.toBunString(globalThis); defer str.deref(); @@ -166,12 +166,12 @@ fn getDefaultArgs(globalThis: *JSGlobalObject) !ArgsSlice { return .{ .array = argv, .start = 1, - .end = @intCast(argv.getLength(globalThis)), + .end = @intCast(try argv.getLength(globalThis)), }; } } } - return .{ .array = argv, .start = 2, .end = @intCast(argv.getLength(globalThis)) }; + return .{ .array = argv, .start = 2, .end = @intCast(try argv.getLength(globalThis)) }; } return .{ @@ -285,7 +285,7 @@ fn storeOption(globalThis: *JSGlobalObject, option_name: ValueRef, option_value: // values[long_option] starts out not present, // first value is added as new array [new_value], // subsequent values are pushed to existing array. - if (values.getOwn(globalThis, key)) |value_list| { + if (try values.getOwn(globalThis, key)) |value_list| { value_list.push(globalThis, new_value); } else { var value_list = try JSValue.createEmptyArray(globalThis, 1); @@ -316,10 +316,10 @@ fn parseOptionDefinitions(globalThis: *JSGlobalObject, options_obj: JSValue, opt try validators.validateObject(globalThis, obj, "options.{s}", .{option.long_name}, .{}); // type field is required - const option_type: JSValue = obj.getOwn(globalThis, "type") orelse .js_undefined; + const option_type: JSValue = try obj.getOwn(globalThis, "type") orelse .js_undefined; option.type = try validators.validateStringEnum(OptionValueType, globalThis, option_type, "options.{s}.type", .{option.long_name}); - if (obj.getOwn(globalThis, "short")) |short_option| { + if (try obj.getOwn(globalThis, "short")) |short_option| { try validators.validateString(globalThis, short_option, "options.{s}.short", .{option.long_name}); var short_option_str = try short_option.toBunString(globalThis); if (short_option_str.length() != 1) { @@ -329,13 +329,13 @@ fn parseOptionDefinitions(globalThis: *JSGlobalObject, options_obj: JSValue, opt option.short_name = short_option_str; } - if (obj.getOwn(globalThis, "multiple")) |multiple_value| { + if (try obj.getOwn(globalThis, "multiple")) |multiple_value| { if (!multiple_value.isUndefined()) { option.multiple = try validators.validateBoolean(globalThis, multiple_value, "options.{s}.multiple", .{option.long_name}); } } - if (obj.getOwn(globalThis, "default")) |default_value| { + if (try obj.getOwn(globalThis, "default")) |default_value| { if (!default_value.isUndefined()) { switch (option.type) { .string => { @@ -382,7 +382,7 @@ fn tokenizeArgs( const num_args: u32 = args.end - args.start; var index: u32 = 0; while (index < num_args) : (index += 1) { - const arg_ref: ValueRef = ValueRef{ .jsvalue = args.get(globalThis, index) }; + const arg_ref: ValueRef = ValueRef{ .jsvalue = try args.get(globalThis, index) }; const arg = arg_ref.asBunString(globalThis); const token_rawtype = classifyToken(arg, options); @@ -401,7 +401,7 @@ fn tokenizeArgs( while (index < num_args) : (index += 1) { try ctx.handleToken(.{ .positional = .{ .index = index, - .value = ValueRef{ .jsvalue = args.get(globalThis, index) }, + .value = ValueRef{ .jsvalue = try args.get(globalThis, index) }, } }); } break; // Finished processing args, leave while loop. @@ -417,7 +417,7 @@ fn tokenizeArgs( var has_inline_value = true; if (option_type == .string and index + 1 < num_args) { // e.g. '-f', "bar" - value = ValueRef{ .jsvalue = args.get(globalThis, index + 1) }; + value = ValueRef{ .jsvalue = try args.get(globalThis, index + 1) }; has_inline_value = false; log(" (lone_short_option consuming next token as value)", .{}); } @@ -451,7 +451,7 @@ fn tokenizeArgs( var has_inline_value = true; if (option_type == .string and index + 1 < num_args) { // e.g. '-f', "bar" - value = ValueRef{ .jsvalue = args.get(globalThis, index + 1) }; + value = ValueRef{ .jsvalue = try args.get(globalThis, index + 1) }; has_inline_value = false; log(" (short_option_group short option consuming next token as value)", .{}); } @@ -520,7 +520,7 @@ fn tokenizeArgs( var value: ?JSValue = null; if (option_type == .string and index + 1 < num_args and !negative) { // e.g. '--foo', "bar" - value = args.get(globalThis, index + 1); + value = try args.get(globalThis, index + 1); log(" (consuming next as value)", .{}); } @@ -665,23 +665,23 @@ pub fn parseArgs(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE const config = if (config_value.isUndefined()) null else config_value; // Phase 0.A: Get and validate type of input args - const config_args: JSValue = if (config) |c| c.getOwn(globalThis, "args") orelse .js_undefined else .js_undefined; + const config_args: JSValue = if (config) |c| try c.getOwn(globalThis, "args") orelse .js_undefined else .js_undefined; const args: ArgsSlice = if (!config_args.isUndefinedOrNull()) args: { try validators.validateArray(globalThis, config_args, "args", .{}, null); break :args .{ .array = config_args, .start = 0, - .end = @intCast(config_args.getLength(globalThis)), + .end = @intCast(try config_args.getLength(globalThis)), }; } else try getDefaultArgs(globalThis); // Phase 0.B: Parse and validate config - const config_strict: JSValue = (if (config) |c| c.getOwn(globalThis, "strict") else null) orelse JSValue.jsBoolean(true); - var config_allow_positionals: JSValue = if (config) |c| c.getOwn(globalThis, "allowPositionals") orelse JSC.jsBoolean(!config_strict.toBoolean()) else JSC.jsBoolean(!config_strict.toBoolean()); - const config_return_tokens: JSValue = (if (config) |c| c.getOwn(globalThis, "tokens") else null) orelse JSValue.jsBoolean(false); - const config_allow_negative: JSValue = if (config) |c| c.getOwn(globalThis, "allowNegative") orelse .false else .false; - const config_options: JSValue = if (config) |c| c.getOwn(globalThis, "options") orelse .js_undefined else .js_undefined; + const config_strict: JSValue = (if (config) |c| try c.getOwn(globalThis, "strict") else null) orelse JSValue.jsBoolean(true); + var config_allow_positionals: JSValue = if (config) |c| try c.getOwn(globalThis, "allowPositionals") orelse JSC.jsBoolean(!config_strict.toBoolean()) else JSC.jsBoolean(!config_strict.toBoolean()); + const config_return_tokens: JSValue = (if (config) |c| try c.getOwn(globalThis, "tokens") else null) orelse JSValue.jsBoolean(false); + const config_allow_negative: JSValue = if (config) |c| try c.getOwn(globalThis, "allowNegative") orelse .false else .false; + const config_options: JSValue = if (config) |c| try c.getOwn(globalThis, "options") orelse .js_undefined else .js_undefined; const strict = try validators.validateBoolean(globalThis, config_strict, "strict", .{}); @@ -739,7 +739,7 @@ pub fn parseArgs(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE for (option_defs.items) |option| { if (option.default_value) |default_value| { if (!option.long_name.eqlComptime("__proto__")) { - if (state.values.getOwn(globalThis, option.long_name) == null) { + if (try state.values.getOwn(globalThis, option.long_name) == null) { log(" Setting \"{}\" to default value", .{option.long_name}); state.values.putMayBeIndex(globalThis, &option.long_name, default_value); } diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index 1f3f412f45..9fed7e7f68 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -244,8 +244,8 @@ pub fn validateArray(globalThis: *JSGlobalObject, value: JSValue, comptime name_ pub fn validateStringArray(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype) bun.JSError!usize { try validateArray(globalThis, value, name_fmt, name_args, null); var i: usize = 0; - var iter = value.arrayIterator(globalThis); - while (iter.next()) |item| { + var iter = try value.arrayIterator(globalThis); + while (try iter.next()) |item| { if (!item.isString()) { return throwErrInvalidArgType(globalThis, name_fmt ++ "[{d}]", name_args ++ .{i}, "string", value); } @@ -257,8 +257,8 @@ pub fn validateStringArray(globalThis: *JSGlobalObject, value: JSValue, comptime pub fn validateBooleanArray(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype) bun.JSError!usize { try validateArray(globalThis, value, name_fmt, name_args, null); var i: usize = 0; - var iter = value.arrayIterator(globalThis); - while (iter.next()) |item| { + var iter = try value.arrayIterator(globalThis); + while (try iter.next()) |item| { if (!item.isBoolean()) { return throwErrInvalidArgType(globalThis, name_fmt ++ "[{d}]", name_args ++ .{i}, "boolean", value); } diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index a04e1fd0cd..64a44c91e8 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -513,7 +513,7 @@ pub const Expect = struct { const left = try this.getValue(globalThis, thisValue, "toBe", "expected"); const not = this.flags.not; - var pass = right.isSameValue(left, globalThis); + var pass = try right.isSameValue(left, globalThis); if (not) pass = !pass; if (pass) return .js_undefined; @@ -594,7 +594,7 @@ pub const Expect = struct { const not = this.flags.not; var pass = false; - const actual_length = value.getLengthIfPropertyExistsInternal(globalThis); + const actual_length = try value.getLengthIfPropertyExistsInternal(globalThis); if (actual_length == std.math.inf(f64)) { var fmt = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; @@ -652,15 +652,15 @@ pub const Expect = struct { }; if (list_value.jsTypeLoose().isArrayLike()) { - var itr = list_value.arrayIterator(globalThis); - while (itr.next()) |item| { + var itr = try list_value.arrayIterator(globalThis); + while (try itr.next()) |item| { // Confusingly, jest-extended uses `deepEqual`, instead of `toBe` if (try item.jestDeepEquals(expected, globalThis)) { pass = true; break; } } - } else if (list_value.isIterable(globalThis)) { + } else if (try list_value.isIterable(globalThis)) { var expected_entry = ExpectedEntry{ .globalThis = globalThis, .expected = expected, @@ -736,9 +736,9 @@ pub const Expect = struct { }; if (value.jsTypeLoose().isArrayLike()) { - var itr = value.arrayIterator(globalThis); - while (itr.next()) |item| { - if (item.isSameValue(expected, globalThis)) { + var itr = try value.arrayIterator(globalThis); + while (try itr.next()) |item| { + if (try item.isSameValue(expected, globalThis)) { pass = true; break; } @@ -756,7 +756,7 @@ pub const Expect = struct { } else if (value_string.len == 0 and expected_string.len == 0) { // edge case two empty strings are true pass = true; } - } else if (value.isIterable(globalThis)) { + } else if (try value.isIterable(globalThis)) { var expected_entry = ExpectedEntry{ .globalThis = globalThis, .expected = expected, @@ -770,7 +770,7 @@ pub const Expect = struct { item: JSValue, ) callconv(.C) void { const entry = bun.cast(*ExpectedEntry, entry_.?); - if (item.isSameValue(entry.expected, entry.globalThis)) { + if (item.isSameValue(entry.expected, entry.globalThis) catch return) { entry.pass.* = true; // TODO(perf): break out of the `forEach` when a match is found } @@ -828,11 +828,7 @@ pub const Expect = struct { return globalThis.throwInvalidArguments("Expected value must be an object\nReceived: {}", .{value.toFmt(&formatter)}); } - var pass = value.hasOwnPropertyValue(globalThis, expected); - - if (globalThis.hasException()) { - return .zero; - } + var pass = try value.hasOwnPropertyValue(globalThis, expected); if (not) pass = !pass; if (pass) return thisValue; @@ -880,7 +876,7 @@ pub const Expect = struct { const not = this.flags.not; var pass = brk: { - const count = expected.getLength(globalThis); + const count = try expected.getLength(globalThis); // jest-extended checks for truthiness before calling hasOwnProperty // https://github.com/jest-community/jest-extended/blob/711fdcc54d68c2b2c1992c7cfbdf0d0bd6be0f4d/src/matchers/toContainKeys.js#L1-L6 @@ -889,9 +885,9 @@ pub const Expect = struct { var i: u32 = 0; while (i < count) : (i += 1) { - const key = expected.getIndex(globalThis, i); + const key = try expected.getIndex(globalThis, i); - if (!value.hasOwnPropertyValue(globalThis, key)) { + if (!try value.hasOwnPropertyValue(globalThis, key)) { break :brk false; } } @@ -899,10 +895,6 @@ pub const Expect = struct { break :brk true; }; - if (globalThis.hasException()) { - return .zero; - } - if (not) pass = !pass; if (pass) return thisValue; @@ -951,16 +943,16 @@ pub const Expect = struct { const not = this.flags.not; var pass = false; - const count = expected.getLength(globalObject); + const count = try expected.getLength(globalObject); - var keys = value.keys(globalObject); - if (keys.getLength(globalObject) == count) { - var itr = keys.arrayIterator(globalObject); + var keys = try value.keys(globalObject); + if (try keys.getLength(globalObject) == count) { + var itr = try keys.arrayIterator(globalObject); outer: { - while (itr.next()) |item| { + while (try itr.next()) |item| { var i: u32 = 0; while (i < count) : (i += 1) { - const key = expected.getIndex(globalObject, i); + const key = try expected.getIndex(globalObject, i); if (try item.jestDeepEquals(key, globalObject)) break; } else break :outer; } @@ -1016,23 +1008,19 @@ pub const Expect = struct { const not = this.flags.not; var pass = false; - const count = expected.getLength(globalThis); + const count = try expected.getLength(globalThis); var i: u32 = 0; while (i < count) : (i += 1) { - const key = expected.getIndex(globalThis, i); + const key = try expected.getIndex(globalThis, i); - if (value.hasOwnPropertyValue(globalThis, key)) { + if (try value.hasOwnPropertyValue(globalThis, key)) { pass = true; break; } } - if (globalThis.hasException()) { - return .zero; - } - if (not) pass = !pass; if (pass) return thisValue; @@ -1078,9 +1066,9 @@ pub const Expect = struct { var pass = false; if (!value.isUndefinedOrNull()) { - const values = value.values(globalObject); - var itr = values.arrayIterator(globalObject); - while (itr.next()) |item| { + const values = try value.values(globalObject); + var itr = try values.arrayIterator(globalObject); + while (try itr.next()) |item| { if (try item.jestDeepEquals(expected, globalObject)) { pass = true; break; @@ -1136,14 +1124,14 @@ pub const Expect = struct { var pass = true; if (!value.isUndefinedOrNull()) { - const values = value.values(globalObject); - var itr = expected.arrayIterator(globalObject); - const count = values.getLength(globalObject); + const values = try value.values(globalObject); + var itr = try expected.arrayIterator(globalObject); + const count = try values.getLength(globalObject); - while (itr.next()) |item| { + while (try itr.next()) |item| { var i: u32 = 0; while (i < count) : (i += 1) { - const key = values.getIndex(globalObject, i); + const key = try values.getIndex(globalObject, i); if (try key.jestDeepEquals(item, globalObject)) break; } else { pass = false; @@ -1200,16 +1188,16 @@ pub const Expect = struct { var pass = false; if (!value.isUndefinedOrNull()) { - var values = value.values(globalObject); - var itr = expected.arrayIterator(globalObject); - const count = values.getLength(globalObject); - const expectedLength = expected.getLength(globalObject); + var values = try value.values(globalObject); + var itr = try expected.arrayIterator(globalObject); + const count = try values.getLength(globalObject); + const expectedLength = try expected.getLength(globalObject); if (count == expectedLength) { - while (itr.next()) |item| { + while (try itr.next()) |item| { var i: u32 = 0; while (i < count) : (i += 1) { - const key = values.getIndex(globalObject, i); + const key = try values.getIndex(globalObject, i); if (try key.jestDeepEquals(item, globalObject)) { pass = true; break; @@ -1270,14 +1258,14 @@ pub const Expect = struct { var pass = false; if (!value.isUndefinedOrNull()) { - var values = value.values(globalObject); - var itr = expected.arrayIterator(globalObject); - const count = values.getLength(globalObject); + var values = try value.values(globalObject); + var itr = try expected.arrayIterator(globalObject); + const count = try values.getLength(globalObject); - outer: while (itr.next()) |item| { + outer: while (try itr.next()) |item| { var i: u32 = 0; while (i < count) : (i += 1) { - const key = values.getIndex(globalObject, i); + const key = try values.getIndex(globalObject, i); if (try key.jestDeepEquals(item, globalObject)) { pass = true; break :outer; @@ -1340,8 +1328,8 @@ pub const Expect = struct { const expected_type = expected.jsType(); if (value_type.isArrayLike()) { - var itr = value.arrayIterator(globalThis); - while (itr.next()) |item| { + var itr = try value.arrayIterator(globalThis); + while (try itr.next()) |item| { if (try item.jestDeepEquals(expected, globalThis)) { pass = true; break; @@ -1366,7 +1354,7 @@ pub const Expect = struct { else strings.indexOf(value_string.slice(), expected_string.slice()) != null; } - } else if (value.isIterable(globalThis)) { + } else if (try value.isIterable(globalThis)) { var expected_entry = ExpectedEntry{ .globalThis = globalThis, .expected = expected, @@ -1685,7 +1673,7 @@ pub const Expect = struct { const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveProperty", "path, value"); - if (!expected_property_path.isString() and !expected_property_path.isIterable(globalThis)) { + if (!expected_property_path.isString() and !try expected_property_path.isIterable(globalThis)) { return globalThis.throw("Expected path must be a string or an array", .{}); } @@ -1697,7 +1685,7 @@ pub const Expect = struct { var received_property: JSValue = .zero; if (pass) { - received_property = value.getIfPropertyExistsFromPath(globalThis, expected_property_path); + received_property = try value.getIfPropertyExistsFromPath(globalThis, expected_property_path); pass = received_property != .zero; } @@ -2290,7 +2278,7 @@ pub const Expect = struct { if (globalThis.hasException()) return .zero; // no partial match for this case - if (!expected_message.isSameValue(received_message, globalThis)) return .js_undefined; + if (!try expected_message.isSameValue(received_message, globalThis)) return .js_undefined; return this.throw(globalThis, signature, "\n\nExpected message: not {any}\n", .{expected_message.toFmt(&formatter)}); } @@ -2397,7 +2385,7 @@ pub const Expect = struct { const signature = comptime getSignature("toThrow", "expected", false); if (_received_message) |received_message| { - if (received_message.isSameValue(expected_message, globalThis)) return .js_undefined; + if (try received_message.isSameValue(expected_message, globalThis)) return .js_undefined; } // error: message from received error does not match expected error message. @@ -2916,7 +2904,7 @@ pub const Expect = struct { const prop_matchers = _prop_matchers; - if (!value.jestDeepMatch(prop_matchers, globalThis, true)) { + if (!try value.jestDeepMatch(prop_matchers, globalThis, true)) { // TODO: print diff with properties from propertyMatchers const signature = comptime getSignature(fn_name, "propertyMatchers", false); const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ @@ -2986,11 +2974,11 @@ pub const Expect = struct { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); - const actual_length = value.getLengthIfPropertyExistsInternal(globalThis); + const actual_length = try value.getLengthIfPropertyExistsInternal(globalThis); if (actual_length == std.math.inf(f64)) { if (value.jsTypeLoose().isObject()) { - if (value.isIterable(globalThis)) { + if (try value.isIterable(globalThis)) { var any_properties_in_iterator = false; value.forEach(globalThis, &any_properties_in_iterator, struct { pub fn anythingInIterator( @@ -3060,7 +3048,7 @@ pub const Expect = struct { incrementExpectCallCounter(); const not = this.flags.not; - var pass = value.isObjectEmpty(globalThis); + var pass = try value.isObjectEmpty(globalThis); if (not) pass = !pass; if (pass) return thisValue; @@ -3153,7 +3141,7 @@ pub const Expect = struct { incrementExpectCallCounter(); const not = this.flags.not; - var pass = value.jsType().isArray() and @as(i32, @intCast(value.getLength(globalThis))) == size.toInt32(); + var pass = value.jsType().isArray() and @as(i32, @intCast(try value.getLength(globalThis))) == size.toInt32(); if (not) pass = !pass; if (pass) return .js_undefined; @@ -4198,7 +4186,8 @@ pub const Expect = struct { return globalThis.throw("Expected value must be a mock function: {}", .{value}); } - var pass = calls.getLength(globalThis) > 0; + const calls_length = try calls.getLength(globalThis); + var pass = calls_length > 0; const not = this.flags.not; if (not) pass = !pass; @@ -4207,11 +4196,11 @@ pub const Expect = struct { // handle failure if (not) { const signature = comptime getSignature("toHaveBeenCalled", "", true); - return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 0\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 0\n" ++ "Received number of calls: {any}\n", .{calls_length}); } const signature = comptime getSignature("toHaveBeenCalled", "", false); - return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: \\>= 1\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: \\>= 1\n" ++ "Received number of calls: {any}\n", .{calls_length}); } pub fn toHaveBeenCalledOnce(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4229,7 +4218,8 @@ pub const Expect = struct { return globalThis.throw("Expected value must be a mock function: {}", .{value}); } - var pass = @as(i32, @intCast(calls.getLength(globalThis))) == 1; + const calls_length = try calls.getLength(globalThis); + var pass = calls_length == 1; const not = this.flags.not; if (not) pass = !pass; @@ -4238,11 +4228,11 @@ pub const Expect = struct { // handle failure if (not) { const signature = comptime getSignature("toHaveBeenCalledOnce", "expected", true); - return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: not 1\n" ++ "Received number of calls: {d}\n", .{calls.getLength(globalThis)}); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: not 1\n" ++ "Received number of calls: {d}\n", .{calls_length}); } const signature = comptime getSignature("toHaveBeenCalledOnce", "expected", false); - return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 1\n" ++ "Received number of calls: {d}\n", .{calls.getLength(globalThis)}); + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 1\n" ++ "Received number of calls: {d}\n", .{calls_length}); } pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4268,7 +4258,7 @@ pub const Expect = struct { const times = arguments[0].coerce(i32, globalThis); - var pass = @as(i32, @intCast(calls.getLength(globalThis))) == times; + var pass = @as(i32, @intCast(try calls.getLength(globalThis))) == times; const not = this.flags.not; if (not) pass = !pass; @@ -4320,7 +4310,7 @@ pub const Expect = struct { const property_matchers = args[0]; - var pass = received_object.jestDeepMatch(property_matchers, globalThis, true); + var pass = try received_object.jestDeepMatch(property_matchers, globalThis, true); if (not) pass = !pass; if (pass) return .js_undefined; @@ -4360,20 +4350,20 @@ pub const Expect = struct { var pass = false; - if (calls.getLength(globalThis) > 0) { - var itr = calls.arrayIterator(globalThis); - while (itr.next()) |callItem| { + if (try calls.getLength(globalThis) > 0) { + var itr = try calls.arrayIterator(globalThis); + while (try itr.next()) |callItem| { if (callItem == .zero or !callItem.jsType().isArray()) { return globalThis.throw("Expected value must be a mock function with calls: {}", .{value}); } - if (callItem.getLength(globalThis) != arguments.len) { + if (try callItem.getLength(globalThis) != arguments.len) { continue; } - var callItr = callItem.arrayIterator(globalThis); + var callItr = try callItem.arrayIterator(globalThis); var match = true; - while (callItr.next()) |callArg| { + while (try callItr.next()) |callArg| { if (!try callArg.jestDeepEquals(arguments[callItr.i - 1], globalThis)) { match = false; break; @@ -4417,23 +4407,23 @@ pub const Expect = struct { return globalThis.throw("Expected value must be a mock function: {}", .{value}); } - const totalCalls = @as(u32, @intCast(calls.getLength(globalThis))); + const totalCalls: u32 = @truncate(try calls.getLength(globalThis)); var lastCallValue: JSValue = .zero; var pass = totalCalls > 0; if (pass) { - lastCallValue = calls.getIndex(globalThis, totalCalls - 1); + lastCallValue = try calls.getIndex(globalThis, totalCalls - 1); - if (lastCallValue == .zero or !lastCallValue.jsType().isArray()) { + if (!lastCallValue.jsType().isArray()) { return globalThis.throw("Expected value must be a mock function with calls: {}", .{value}); } - if (lastCallValue.getLength(globalThis) != arguments.len) { + if (try lastCallValue.getLength(globalThis) != arguments.len) { pass = false; } else { - var itr = lastCallValue.arrayIterator(globalThis); - while (itr.next()) |callArg| { + var itr = try lastCallValue.arrayIterator(globalThis); + while (try itr.next()) |callArg| { if (!try callArg.jestDeepEquals(arguments[itr.i - 1], globalThis)) { pass = false; break; @@ -4481,23 +4471,23 @@ pub const Expect = struct { return globalThis.throwInvalidArguments("toHaveBeenNthCalledWith() requires a positive integer argument", .{}); } - const totalCalls = calls.getLength(globalThis); + const totalCalls = try calls.getLength(globalThis); var nthCallValue: JSValue = .zero; var pass = totalCalls >= nthCallNum; if (pass) { - nthCallValue = calls.getIndex(globalThis, @as(u32, @intCast(nthCallNum)) - 1); + nthCallValue = try calls.getIndex(globalThis, @as(u32, @intCast(nthCallNum)) - 1); - if (nthCallValue == .zero or !nthCallValue.jsType().isArray()) { + if (!nthCallValue.jsType().isArray()) { return globalThis.throw("Expected value must be a mock function with calls: {}", .{value}); } - if (nthCallValue.getLength(globalThis) != (arguments.len - 1)) { + if (try nthCallValue.getLength(globalThis) != (arguments.len - 1)) { pass = false; } else { - var itr = nthCallValue.arrayIterator(globalThis); - while (itr.next()) |callArg| { + var itr = try nthCallValue.arrayIterator(globalThis); + while (try itr.next()) |callArg| { if (!try callArg.jestDeepEquals(arguments[itr.i], globalThis)) { pass = false; break; @@ -4567,7 +4557,7 @@ pub const Expect = struct { index, ); - const total_count = returns.getLength(globalThis); + const total_count = try returns.getLength(globalThis); const return_status: ReturnStatus = brk: { // Returns is an array of: @@ -5445,7 +5435,7 @@ pub const ExpectCustomAsymmetricMatcher = struct { captured_args.ensureStillAlive(); // prepare the args array as `[received, ...captured_args]` - const args_count = captured_args.getLength(globalThis); + const args_count = captured_args.getLength(globalThis) catch return false; var allocator = std.heap.stackFallback(8 * @sizeOf(JSValue), globalThis.allocator()); var matcher_args = std.ArrayList(JSValue).initCapacity(allocator.get(), args_count + 1) catch { globalThis.throwOutOfMemory() catch {}; @@ -5453,7 +5443,7 @@ pub const ExpectCustomAsymmetricMatcher = struct { }; matcher_args.appendAssumeCapacity(received); for (0..args_count) |i| { - matcher_args.appendAssumeCapacity(captured_args.getIndex(globalThis, @truncate(i))); + matcher_args.appendAssumeCapacity(captured_args.getIndex(globalThis, @truncate(i)) catch return false); } return Expect.executeCustomMatcher(globalThis, matcher_name, matcher_fn, matcher_args.items, this.flags, true) catch false; @@ -5466,34 +5456,30 @@ pub const ExpectCustomAsymmetricMatcher = struct { return JSValue.jsBoolean(matched); } + fn maybeClear(comptime dontThrow: bool, globalThis: *JSGlobalObject, err: bun.JSError) bun.JSError!bool { + if (dontThrow) { + globalThis.clearException(); + return false; + } + return err; + } + /// Calls a custom implementation (if provided) to stringify this asymmetric matcher, and returns true if it was provided and it succeed pub fn customPrint(_: *ExpectCustomAsymmetricMatcher, thisValue: JSValue, globalThis: *JSGlobalObject, writer: anytype, comptime dontThrow: bool) !bool { const matcher_fn: JSValue = js.matcherFnGetCached(thisValue) orelse return false; - if (matcher_fn.get_unsafe(globalThis, "toAsymmetricMatcher")) |fn_value| { + if (matcher_fn.get(globalThis, "toAsymmetricMatcher") catch |e| return maybeClear(dontThrow, globalThis, e)) |fn_value| { if (fn_value.jsType().isFunction()) { const captured_args: JSValue = js.capturedArgsGetCached(thisValue) orelse return false; var stack_fallback = std.heap.stackFallback(256, globalThis.allocator()); - const args_len = captured_args.getLength(globalThis); + const args_len = captured_args.getLength(globalThis) catch |e| return maybeClear(dontThrow, globalThis, e); var args = try std.ArrayList(JSValue).initCapacity(stack_fallback.get(), args_len); - var iter = captured_args.arrayIterator(globalThis); - while (iter.next()) |arg| { + var iter = captured_args.arrayIterator(globalThis) catch |e| return maybeClear(dontThrow, globalThis, e); + while (iter.next() catch |e| return maybeClear(dontThrow, globalThis, e)) |arg| { args.appendAssumeCapacity(arg); } - const result = matcher_fn.call(globalThis, thisValue, args.items) catch |err| { - if (dontThrow) { - globalThis.clearException(); - return false; - } - return err; - }; - try writer.print("{}", .{result.toBunString(globalThis) catch { - if (dontThrow) { - globalThis.clearException(); - return false; - } - return error.JSError; - }}); + const result = matcher_fn.call(globalThis, thisValue, args.items) catch |e| return maybeClear(dontThrow, globalThis, e); + try writer.print("{}", .{result.toBunString(globalThis) catch |e| return maybeClear(dontThrow, globalThis, e)}); } } return false; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 95c5e6d358..b541296ca2 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -279,7 +279,7 @@ pub const Jest = struct { return globalThis.throwInvalidArgumentType(name, "callback", "function"); } - if (function.getLength(globalThis) > 0) { + if (try function.getLength(globalThis) > 0) { return globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); } @@ -735,7 +735,7 @@ pub const TestScope = struct { switch (promise.status(vm.global.vm())) { .rejected => { if (!promise.isHandled(vm.global.vm()) and this.tag != .fail) { - _ = vm.unhandledRejection(vm.global, promise.result(vm.global.vm()), promise.asValue()); + vm.unhandledRejection(vm.global, promise.result(vm.global.vm()), promise.asValue()); } return switch (this.tag) { @@ -913,6 +913,7 @@ pub const DescribeScope = struct { pub const beforeAll = createCallback(.beforeAll); pub const beforeEach = createCallback(.beforeEach); + // TODO this should return JSError pub fn execCallback(this: *DescribeScope, globalObject: *JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { var hooks = &@field(this, @tagName(hook) ++ "s"); defer { @@ -933,7 +934,7 @@ pub const DescribeScope = struct { } const vm = VirtualMachine.get(); - var result: JSValue = switch (cb.getLength(globalObject)) { + var result: JSValue = switch (cb.getLength(globalObject) catch |e| return globalObject.takeException(e)) { // TODO is this right? 0 => callJSFunctionForTestRunner(vm, globalObject, cb, &.{}), else => brk: { this.done = false; @@ -1095,7 +1096,7 @@ pub const DescribeScope = struct { switch (prom.status(globalObject.vm())) { .fulfilled => {}, else => { - _ = globalObject.bunVM().unhandledRejection(globalObject, prom.result(globalObject.vm()), prom.asValue()); + globalObject.bunVM().unhandledRejection(globalObject, prom.result(globalObject.vm()), prom.asValue()); return .js_undefined; }, } @@ -1834,7 +1835,7 @@ inline fn createScope( function.protect(); } - const func_params_length = function.getLength(globalThis); + const func_params_length = try function.getLength(globalThis); var arg_size: usize = 0; var has_callback = false; if (func_params_length > 0) { @@ -2062,16 +2063,16 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa return .js_undefined; } - var iter = array.arrayIterator(globalThis); + var iter = try array.arrayIterator(globalThis); var test_idx: usize = 0; - while (iter.next()) |item| { - const func_params_length = function.getLength(globalThis); + while (try iter.next()) |item| { + const func_params_length = try function.getLength(globalThis); const item_is_array = !item.isEmptyOrUndefinedOrNull() and item.jsType().isArray(); var arg_size: usize = 1; if (item_is_array) { - arg_size = item.getLength(globalThis); + arg_size = try item.getLength(globalThis); } // add room for callback function @@ -2085,8 +2086,8 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa if (item_is_array) { // Spread array as args - var item_iter = item.arrayIterator(globalThis); - while (item_iter.next()) |array_item| { + var item_iter = try item.arrayIterator(globalThis); + while (try item_iter.next()) |array_item| { if (array_item == .zero) { allocator.free(function_args); break; @@ -2206,7 +2207,7 @@ fn callJSFunctionForTestRunner(vm: *JSC.VirtualMachine, globalObject: *JSGlobalO vm.eventLoop().enter(); defer vm.eventLoop().exit(); - globalObject.clearTerminationException(); + globalObject.clearTerminationException(); // TODO this is sus return function.call(globalObject, .js_undefined, args) catch |err| globalObject.takeException(err); } diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index daa4c316f1..e58ee6ab40 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -107,7 +107,7 @@ pub const JestPrettyFormat = struct { .globalThis = global, .quote_strings = options.quote_strings, }; - const tag = JestPrettyFormat.Formatter.Tag.get(vals[0], global); + const tag = try JestPrettyFormat.Formatter.Tag.get(vals[0], global); var unbuffered_writer = if (comptime Writer != RawWriter) writer.context.unbuffered_writer.context.writer() @@ -197,7 +197,7 @@ pub const JestPrettyFormat = struct { } any = true; - tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + tag = try JestPrettyFormat.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .StringPossiblyFormatted; } @@ -219,7 +219,7 @@ pub const JestPrettyFormat = struct { _ = writer.write(" ") catch 0; } any = true; - tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + tag = try JestPrettyFormat.Formatter.Tag.get(this_value, global); if (tag.tag == .String and fmt.remaining_values.len > 0) { tag.tag = .StringPossiblyFormatted; } @@ -358,12 +358,12 @@ pub const JestPrettyFormat = struct { cell: JSValue.JSType = .Cell, }; - pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { - switch (@intFromEnum(value)) { - 0, 0xa => return Result{ + pub fn get(value: JSValue, globalThis: *JSGlobalObject) bun.JSError!Result { + switch (value) { + .zero, .js_undefined => return Result{ .tag = .Undefined, }, - 0x2 => return Result{ + .null => return Result{ .tag = .Null, }, else => {}, @@ -439,11 +439,11 @@ pub const JestPrettyFormat = struct { // Is this a react element? if (js_type.isObject() and js_type != .ProxyObject) { - if (value.getOwnTruthy(globalThis, "$$typeof")) |typeof_symbol| { + if (try value.getOwnTruthy(globalThis, "$$typeof")) |typeof_symbol| { var reactElement = ZigString.init("react.element"); var react_fragment = ZigString.init("react.fragment"); - if (JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &reactElement), globalThis) or JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_fragment), globalThis)) { + if (try typeof_symbol.isSameValue(.symbolFor(globalThis, &reactElement), globalThis) or try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_fragment), globalThis)) { return .{ .tag = .JSX, .cell = js_type }; } } @@ -576,7 +576,7 @@ pub const JestPrettyFormat = struct { Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, // undefined is overloaded to mean the '%o" field - Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors) catch return, + Tag.Undefined => this.format(Tag.get(next_value, globalThis) catch return, Writer, writer_, next_value, globalThis, enable_ansi_colors) catch return, else => unreachable, } @@ -677,10 +677,10 @@ pub const JestPrettyFormat = struct { pub fn forEach(_: *JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); if (this.formatter.failed) return; - const key = JSC.JSObject.getIndex(nextValue, globalObject, 0); - const value = JSC.JSObject.getIndex(nextValue, globalObject, 1); + const key = JSC.JSObject.getIndex(nextValue, globalObject, 0) catch return; + const value = JSC.JSObject.getIndex(nextValue, globalObject, 1) catch return; this.formatter.writeIndent(Writer, this.writer) catch return; - const key_tag = Tag.get(key, globalObject); + const key_tag = Tag.get(key, globalObject) catch return; this.formatter.format( key_tag, @@ -691,7 +691,7 @@ pub const JestPrettyFormat = struct { enable_ansi_colors, ) catch return; this.writer.writeAll(" => ") catch return; - const value_tag = Tag.get(value, globalObject); + const value_tag = Tag.get(value, globalObject) catch return; this.formatter.format( value_tag, Writer, @@ -714,7 +714,7 @@ pub const JestPrettyFormat = struct { var this: *@This() = bun.cast(*@This(), ctx orelse return); if (this.formatter.failed) return; this.formatter.writeIndent(Writer, this.writer) catch return; - const key_tag = Tag.get(nextValue, globalObject); + const key_tag = Tag.get(nextValue, globalObject) catch return; this.formatter.format( key_tag, Writer, @@ -794,7 +794,7 @@ pub const JestPrettyFormat = struct { .failed = false, }; - const tag = Tag.get(value, globalThis); + const tag = Tag.get(value, globalThis) catch return; if (tag.cell.isHidden()) return; if (ctx.i == 0) { @@ -1137,7 +1137,7 @@ pub const JestPrettyFormat = struct { } }, .Array => { - const len = @as(u32, @truncate(value.getLength(this.globalThis))); + const len: u32 = @truncate(try value.getLength(this.globalThis)); if (len == 0) { writer.writeAll("[]"); this.addForNewLine(2); @@ -1163,7 +1163,7 @@ pub const JestPrettyFormat = struct { { const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, 0, null)); - const tag = Tag.get(element, this.globalThis); + const tag = try Tag.get(element, this.globalThis); was_good_time = was_good_time or !tag.tag.isPrimitive() or this.goodTimeForANewLine(); @@ -1194,7 +1194,7 @@ pub const JestPrettyFormat = struct { this.writeIndent(Writer, writer_) catch unreachable; const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, i, null)); - const tag = Tag.get(element, this.globalThis); + const tag = try Tag.get(element, this.globalThis); try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); @@ -1260,7 +1260,7 @@ pub const JestPrettyFormat = struct { }; return; } else if (value.as(JSC.DOMFormData) != null) { - const toJSONFunction = value.get_unsafe(this.globalThis, "toJSON").?; + const toJSONFunction = (try value.get(this.globalThis, "toJSON")).?; this.addForNewLine("FormData (entries) ".len); writer.writeAll(comptime Output.prettyFmt("FormData (entries) ", enable_ansi_colors)); @@ -1341,7 +1341,7 @@ pub const JestPrettyFormat = struct { writer.writeAll(comptime Output.prettyFmt("" ++ fmt ++ "", enable_ansi_colors)); }, .Map => { - const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length_value = try value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; @@ -1369,7 +1369,7 @@ pub const JestPrettyFormat = struct { writer.writeAll("\n"); }, .Set => { - const length_value = value.get_unsafe(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length_value = try value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); const length = length_value.toInt32(); const prev_quote_strings = this.quote_strings; @@ -1421,7 +1421,7 @@ pub const JestPrettyFormat = struct { }, .Event => { const event_type_value: JSValue = brk: { - const value_: JSValue = value.get_unsafe(this.globalThis, "type") orelse break :brk .js_undefined; + const value_: JSValue = try value.get(this.globalThis, "type") orelse break :brk .js_undefined; if (value_.isString()) { break :brk value_; } @@ -1465,7 +1465,7 @@ pub const JestPrettyFormat = struct { .{}, ); - const tag = Tag.get(message_value, this.globalThis); + const tag = try Tag.get(message_value, this.globalThis); try this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); writer.writeAll(", \n"); } @@ -1479,7 +1479,7 @@ pub const JestPrettyFormat = struct { .{}, ); const data: JSValue = (try value.fastGet(this.globalThis, .data)) orelse .js_undefined; - const tag = Tag.get(data, this.globalThis); + const tag = try Tag.get(data, this.globalThis); if (tag.cell.isStringLike()) { try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); @@ -1496,7 +1496,7 @@ pub const JestPrettyFormat = struct { .{}, ); - const tag = Tag.get(data, this.globalThis); + const tag = try Tag.get(data, this.globalThis); try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); writer.writeAll("\n"); } @@ -1521,8 +1521,8 @@ pub const JestPrettyFormat = struct { defer if (tag_name_slice.isAllocated()) tag_name_slice.deinit(); - if (value.get_unsafe(this.globalThis, "type")) |type_value| { - const _tag = Tag.get(type_value, this.globalThis); + if (try value.get(this.globalThis, "type")) |type_value| { + const _tag = try Tag.get(type_value, this.globalThis); if (_tag.cell == .Symbol) {} else if (_tag.cell.isStringLike()) { try type_value.toZigString(&tag_name_str, this.globalThis); @@ -1551,7 +1551,7 @@ pub const JestPrettyFormat = struct { writer.writeAll(tag_name_slice.slice()); if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("", enable_ansi_colors)); - if (value.get_unsafe(this.globalThis, "key")) |key_value| { + if (try value.get(this.globalThis, "key")) |key_value| { if (!key_value.isUndefinedOrNull()) { if (needs_space) writer.writeAll(" key=") @@ -1562,13 +1562,13 @@ pub const JestPrettyFormat = struct { this.quote_strings = true; defer this.quote_strings = old_quote_strings; - try this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); + try this.format(try Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); needs_space = true; } } - if (value.get_unsafe(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| { const prev_quote_strings = this.quote_strings; defer this.quote_strings = prev_quote_strings; this.quote_strings = true; @@ -1581,7 +1581,7 @@ pub const JestPrettyFormat = struct { }).init(this.globalThis, props_obj); defer props_iter.deinit(); - const children_prop = props.get_unsafe(this.globalThis, "children"); + const children_prop = try props.get(this.globalThis, "children"); if (props_iter.len > 0) { { this.indent += 1; @@ -1593,7 +1593,7 @@ pub const JestPrettyFormat = struct { continue; const property_value = props_iter.value; - const tag = Tag.get(property_value, this.globalThis); + const tag = try Tag.get(property_value, this.globalThis); if (tag.cell.isHidden()) continue; @@ -1639,7 +1639,7 @@ pub const JestPrettyFormat = struct { } if (children_prop) |children| { - const tag = Tag.get(children, this.globalThis); + const tag = try Tag.get(children, this.globalThis); const print_children = switch (tag.tag) { .String, .JSX, .Array => true, @@ -1674,14 +1674,14 @@ pub const JestPrettyFormat = struct { this.indent += 1; this.writeIndent(Writer, writer_) catch unreachable; defer this.indent -|= 1; - try this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); + try this.format(try Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); } writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; }, .Array => { - const length = children.getLength(this.globalThis); + const length = try children.getLength(this.globalThis); if (length == 0) break :print_children; writer.writeAll(">\n"); @@ -1696,8 +1696,8 @@ pub const JestPrettyFormat = struct { var j: usize = 0; while (j < length) : (j += 1) { - const child = JSC.JSObject.getIndex(children, this.globalThis, @as(u32, @intCast(j))); - try this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); + const child = try JSC.JSObject.getIndex(children, this.globalThis, @as(u32, @intCast(j))); + try this.format(try Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); if (j + 1 < length) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch unreachable; @@ -1764,7 +1764,7 @@ pub const JestPrettyFormat = struct { .parent = value, }; - value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); + try value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); if (iter.i == 0) { var object_name = ZigString.Empty; diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 6122e926cc..b86d08c4e7 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -12,6 +12,7 @@ pub export fn Bun__getVM() *JSC.VirtualMachine { return JSC.VirtualMachine.get(); } +/// Caller must check for termination exception pub export fn Bun__drainMicrotasks() void { JSC.VirtualMachine.get().eventLoop().tick(); } @@ -115,7 +116,7 @@ pub export fn Bun__handleRejectedPromise(global: *JSGlobalObject, promise: *JSC. if (result == .zero) return; - _ = jsc_vm.unhandledRejection(global, result, promise.toJS()); + jsc_vm.unhandledRejection(global, result, promise.toJS()); jsc_vm.autoGarbageCollect(); } diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 9f9beceac5..22995f2f3e 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -105,7 +105,7 @@ pub fn doReadFromS3(this: *Blob, comptime Function: anytype, global: *JSGlobalOb const WrappedFn = struct { pub fn wrapped(b: *Blob, g: *JSGlobalObject, by: []u8) JSC.JSValue { - return JSC.toJSHostValue(g, Function(b, g, by, .clone)); + return JSC.toJSHostCall(g, @src(), Function, .{ b, g, by, .clone }); } }; return S3BlobDownloadTask.init(global, this, WrappedFn.wrapped); @@ -1231,7 +1231,7 @@ pub fn writeFileInternal(globalThis: *JSC.JSGlobalObject, path_or_blob_: *PathOr bun.isRegularFile(path_or_blob.blob.store.?.data.file.mode)))) { if (data.isString()) { - const len = data.getLength(globalThis); + const len = try data.getLength(globalThis); if (len < 256 * 1024) { const str = try data.toBunString(globalThis); @@ -2012,7 +2012,7 @@ pub fn toStreamWithOffset( fn lifetimeWrap(comptime Fn: anytype, comptime lifetime: JSC.WebCore.Lifetime) fn (*Blob, *JSC.JSGlobalObject) JSC.JSValue { return struct { fn wrap(this: *Blob, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.toJSHostValue(globalObject, Fn(this, globalObject, lifetime)); + return JSC.toJSHostCall(globalObject, @src(), Fn, .{ this, globalObject, lifetime }); } }.wrap; } @@ -3762,13 +3762,13 @@ fn fromJSWithoutDeferGC( var fail_if_top_value_is_not_typed_array_like = false; switch (current.jsTypeLoose()) { .Array, .DerivedArray => { - var top_iter = JSC.JSArrayIterator.init(current, global); + var top_iter = try JSC.JSArrayIterator.init(current, global); might_only_be_one_thing = top_iter.len == 1; if (top_iter.len == 0) { return Blob{ .globalThis = global }; } if (might_only_be_one_thing) { - top_value = top_iter.next().?; + top_value = (try top_iter.next()).?; } }, else => { @@ -3875,10 +3875,10 @@ fn fromJSWithoutDeferGC( }, .Array, .DerivedArray => { - var iter = JSC.JSArrayIterator.init(current, global); + var iter = try JSC.JSArrayIterator.init(current, global); try stack.ensureUnusedCapacity(iter.len); var any_arrays = false; - while (iter.next()) |item| { + while (try iter.next()) |item| { if (item.isUndefinedOrNull()) continue; // When it's a string or ArrayBuffer inside an array, we can avoid the extra push/pop diff --git a/src/bun.js/webcore/Body.zig b/src/bun.js/webcore/Body.zig index 4684c06575..2a8b087f7c 100644 --- a/src/bun.js/webcore/Body.zig +++ b/src/bun.js/webcore/Body.zig @@ -1153,7 +1153,7 @@ pub fn Mixin(comptime Type: type) type { fn lifetimeWrap(comptime Fn: anytype, comptime lifetime: JSC.WebCore.Lifetime) fn (*AnyBlob, *JSC.JSGlobalObject) JSC.JSValue { return struct { fn wrap(this: *AnyBlob, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.toJSHostValue(globalObject, Fn(this, globalObject, lifetime)); + return JSC.toJSHostCall(globalObject, @src(), Fn, .{ this, globalObject, lifetime }); } }.wrap; } diff --git a/src/bun.js/webcore/blob/read_file.zig b/src/bun.js/webcore/blob/read_file.zig index 13cc393747..4ce7514532 100644 --- a/src/bun.js/webcore/blob/read_file.zig +++ b/src/bun.js/webcore/blob/read_file.zig @@ -36,7 +36,7 @@ pub fn NewReadFileHandler(comptime Function: anytype) type { blob.size = @min(@as(SizeType, @truncate(bytes.len)), blob.size); const WrappedFn = struct { pub fn wrapped(b: *Blob, g: *JSGlobalObject, by: []u8) JSC.JSValue { - return JSC.toJSHostValue(g, Function(b, g, by, .temporary)); + return JSC.toJSHostCall(g, @src(), Function, .{ b, g, by, .temporary }); } }; diff --git a/src/bun.js/webcore/fetch.zig b/src/bun.js/webcore/fetch.zig index 3c5177bd79..2a3da09bbe 100644 --- a/src/bun.js/webcore/fetch.zig +++ b/src/bun.js/webcore/fetch.zig @@ -1853,7 +1853,7 @@ pub fn Bun__fetch_( inline for (0..2) |i| { if (objects_to_try[i] != .zero) { if (try objects_to_try[i].get(globalThis, "unix")) |socket_path| { - if (socket_path.isString() and socket_path.getLength(ctx) > 0) { + if (socket_path.isString() and try socket_path.getLength(ctx) > 0) { if (socket_path.toSliceCloneWithAllocator(globalThis, allocator)) |slice| { break :extract_unix_socket_path slice; } @@ -2001,7 +2001,7 @@ pub fn Bun__fetch_( inline for (0..2) |i| { if (objects_to_try[i] != .zero) { if (try objects_to_try[i].get(globalThis, "proxy")) |proxy_arg| { - if (proxy_arg.isString() and proxy_arg.getLength(ctx) > 0) { + if (proxy_arg.isString() and try proxy_arg.getLength(ctx) > 0) { var href = try JSC.URL.hrefFromJS(proxy_arg, globalThis); if (href.tag == .Dead) { const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 43f8e8f508..22294ae0e4 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -81,7 +81,7 @@ pub const Start = union(Tag) { var chunk_size: Blob.SizeType = 0; var empty = true; - if (value.getOwn(globalThis, "asUint8Array")) |val| { + if (try value.getOwn(globalThis, "asUint8Array")) |val| { if (val.isBoolean()) { as_uint8array = val.toBoolean(); empty = false; diff --git a/src/bun.zig b/src/bun.zig index 72e9fcf79b..7410e259c1 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -123,6 +123,15 @@ pub const JSError = error{ OutOfMemory, }; +pub const JSExecutionTerminated = error{ + /// JavaScript execution has been terminated. + /// This condition is indicated by throwing an exception, so most code should still handle it + /// with JSError. If you expect that you will not throw any errors other than the termination + /// exception, you can catch JSError, assert that the exception is the termination exception, + /// and return error.JSExecutionTerminated. + JSExecutionTerminated, +}; + pub const JSOOM = OOM || JSError; pub const detectCI = @import("ci_info.zig").detectCI; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 9896d18894..cacdd6735e 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1527,7 +1527,7 @@ pub const TestCommand = struct { switch (promise.status(vm.global.vm())) { .rejected => { - _ = vm.unhandledRejection(vm.global, promise.result(vm.global.vm()), promise.asValue()); + vm.unhandledRejection(vm.global, promise.result(vm.global.vm()), promise.asValue()); reporter.summary.fail += 1; if (reporter.jest.bail == reporter.summary.fail) { diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 271be57437..193c949eeb 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1363,7 +1363,7 @@ for (const [filename, { functions, typedefs }] of files) { switch (returnStrategy.type) { case "jsvalue": - zigInternal.add(`return JSC.toJSHostValue(${globalObjectArg}, `); + zigInternal.add(`return JSC.toJSHostCall(${globalObjectArg}, @src(), `); break; case "basic-out-param": zigInternal.add(`out.* = @as(bun.JSError!${returnStrategy.abiType}, `); @@ -1373,7 +1373,12 @@ for (const [filename, { functions, typedefs }] of files) { break; } - zigInternal.line(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}(`); + zigInternal.add(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}`); + if (returnStrategy.type === "jsvalue") { + zigInternal.line(", .{"); + } else { + zigInternal.line("("); + } zigInternal.indent(); for (const arg of vari.args) { const argName = arg.zigMappedName!; @@ -1421,7 +1426,7 @@ for (const [filename, { functions, typedefs }] of files) { zigInternal.dedent(); switch (returnStrategy.type) { case "jsvalue": - zigInternal.line(`));`); + zigInternal.line(`});`); break; case "basic-out-param": case "void": diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 4ae300fa0b..19fb13650a 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1958,7 +1958,7 @@ const JavaScriptCoreBindings = struct { if (comptime Environment.enable_logs) log_zig_getter("${typeName}", "${name}"); return switch (@typeInfo(@typeInfo(@TypeOf(${typeName}.${getter})).@"fn".return_type.?)) { .error_union => { - return @call(.always_inline, jsc.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.${getter}, .{this, ${thisValue ? "thisValue," : ""} globalObject})}); + return @call(.always_inline, jsc.toJSHostCall, .{globalObject, @src(), ${typeName}.${getter}, .{this, ${thisValue ? "thisValue," : ""} globalObject}}); }, else => @call(.always_inline, ${typeName}.${getter}, .{this, ${thisValue ? "thisValue," : ""} globalObject}), }; @@ -2002,7 +2002,7 @@ const JavaScriptCoreBindings = struct { output += ` pub fn ${names.fn}(thisValue: *${typeName}, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame${proto[name].passThis ? ", js_this_value: jsc.JSValue" : ""}) callconv(jsc.conv) jsc.JSValue { if (comptime Environment.enable_logs) log_zig_method("${typeName}", "${name}", callFrame); - return @call(.always_inline, jsc.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.${fn}, .{thisValue, globalObject, callFrame${proto[name].passThis ? ", js_this_value" : ""}})}); + return @call(.always_inline, jsc.toJSHostCall, .{globalObject, @src(), ${typeName}.${fn}, .{thisValue, globalObject, callFrame${proto[name].passThis ? ", js_this_value" : ""}}}); } `; } @@ -2020,7 +2020,7 @@ const JavaScriptCoreBindings = struct { if (comptime Environment.enable_logs) log_zig_class_getter("${typeName}", "${name}"); return switch (@typeInfo(@typeInfo(@TypeOf(${typeName}.${getter})).@"fn".return_type.?)) { .error_union => { - return @call(.always_inline, jsc.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.${getter}, .{globalObject, ${thisValue ? "thisValue," : ""} propertyName})}); + return @call(.always_inline, jsc.toJSHostCall, .{globalObject, @src(), ${typeName}.${getter}, .{globalObject, ${thisValue ? "thisValue," : ""} propertyName}}); }, else => { return @call(.always_inline, ${typeName}.${getter}, .{globalObject, ${thisValue ? "thisValue," : ""} propertyName}); @@ -2087,7 +2087,7 @@ const JavaScriptCoreBindings = struct { output += ` pub fn ${symbolName(typeName, "onStructuredCloneDeserialize")}(globalObject: *jsc.JSGlobalObject, ptr: [*]u8, end: [*]u8) callconv(jsc.conv) jsc.JSValue { if (comptime Environment.enable_logs) log_zig_structured_clone_deserialize("${typeName}"); - return @call(.always_inline, jsc.toJSHostValue, .{ globalObject, @call(.always_inline, ${typeName}.onStructuredCloneDeserialize, .{globalObject, ptr, end}) }); + return @call(.always_inline, jsc.toJSHostCall, .{ globalObject, @src(), ${typeName}.onStructuredCloneDeserialize, .{globalObject, ptr, end} }); } `; } else { diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index 5116e1f390..84c0aa7065 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -215,7 +215,7 @@ export function getJS2NativeZig(gs2NativeZigPath: string) { .filter(x => x.type === "zig") .flatMap(call => [ `export fn ${symbol(call)}_workaround(global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue {`, - ` return JSC.toJSHostValue(global, @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), call.filename))}).${call.symbol}(global));`, + ` return JSC.toJSHostCall(global, @src(), @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), call.filename))}).${call.symbol}, .{global});`, "}", ]), ...wrapperCalls diff --git a/src/codegen/generate-node-errors.ts b/src/codegen/generate-node-errors.ts index aafbe8c4e1..bb41dc1389 100644 --- a/src/codegen/generate-node-errors.ts +++ b/src/codegen/generate-node-errors.ts @@ -124,6 +124,7 @@ zig += ` extern fn Bun__createErrorWithCode(globalThis: *JSC.JSGlobalObject, code: Error, message: *bun.String) JSC.JSValue; /// Creates an Error object with the given error code. + /// If an error is thrown while creating the Error object, returns that error instead. /// Derefs the message string. pub fn toJS(this: Error, globalThis: *JSC.JSGlobalObject, message: *bun.String) JSC.JSValue { defer message.deref(); diff --git a/src/csrf.zig b/src/csrf.zig index cfdef4b424..7a9c3abd76 100644 --- a/src/csrf.zig +++ b/src/csrf.zig @@ -230,7 +230,7 @@ pub fn csrf__generate_impl(globalObject: *JSC.JSGlobalObject, callframe: *JSC.Ca if (jsSecret.isEmptyOrUndefinedOrNull()) { return globalObject.throwInvalidArguments("Secret is required", .{}); } - if (!jsSecret.isString() or jsSecret.getLength(globalObject) == 0) { + if (!jsSecret.isString() or try jsSecret.getLength(globalObject) == 0) { return globalObject.throwInvalidArguments("Secret must be a non-empty string", .{}); } secret = try jsSecret.toSlice(globalObject, bun.default_allocator); @@ -316,7 +316,7 @@ pub fn csrf__verify_impl(globalObject: *JSC.JSGlobalObject, call_frame: *JSC.Cal if (jsToken.isUndefinedOrNull()) { return globalObject.throwInvalidArguments("Token is required", .{}); } - if (!jsToken.isString() or jsToken.getLength(globalObject) == 0) { + if (!jsToken.isString() or try jsToken.getLength(globalObject) == 0) { return globalObject.throwInvalidArguments("Token must be a non-empty string", .{}); } const token = try jsToken.toSlice(globalObject, bun.default_allocator); diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index 47bd0e5833..76d35336f6 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -158,8 +158,8 @@ fn parserOptionsFromJS(globalThis: *JSC.JSGlobalObject, allocator: Allocator, op _ = allocator; // autofix if (try jsobj.getTruthy(globalThis, "flags")) |val| { if (val.isArray()) { - var iter = val.arrayIterator(globalThis); - while (iter.next()) |item| { + var iter = try val.arrayIterator(globalThis); + while (try iter.next()) |item| { const bunstr = try item.toBunString(globalThis); defer bunstr.deref(); const str = bunstr.toUTF8(bun.default_allocator); diff --git a/src/css/values/color_js.zig b/src/css/values/color_js.zig index 4ac9e2c647..5d4b84b0dc 100644 --- a/src/css/values/color_js.zig +++ b/src/css/values/color_js.zig @@ -183,18 +183,18 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = rgba.alpha, .red = rgba.red, .green = rgba.green, .blue = rgba.blue } } }; } else if (args[0].jsType().isArrayLike()) { - switch (args[0].getLength(globalThis)) { + switch (try args[0].getLength(globalThis)) { 3 => { - const r = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 0), "[0]"); - const g = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 1), "[1]"); - const b = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 2), "[2]"); + const r = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 0), "[0]"); + const g = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 1), "[1]"); + const b = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 2), "[2]"); break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = 255, .red = @intCast(r), .green = @intCast(g), .blue = @intCast(b) } } }; }, 4 => { - const r = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 0), "[0]"); - const g = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 1), "[1]"); - const b = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 2), "[2]"); - const a = try colorIntFromJS(globalThis, args[0].getIndex(globalThis, 3), "[3]"); + const r = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 0), "[0]"); + const g = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 1), "[1]"); + const b = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 2), "[2]"); + const a = try colorIntFromJS(globalThis, try args[0].getIndex(globalThis, 3), "[3]"); break :brk .{ .result = css.CssColor{ .rgba = .{ .alpha = @intCast(a), .red = @intCast(r), .green = @intCast(g), .blue = @intCast(b) } } }; }, else => { diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 9dccf83abf..44dbc53791 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -1418,8 +1418,8 @@ pub const struct_any_reply = struct { reply.toJSResponse(allocator, globalThis, lookup_name); if (response.isArray()) { - var iterator = response.arrayIterator(globalThis); - while (iterator.next()) |item| { + var iterator = try response.arrayIterator(globalThis); + while (try iterator.next()) |item| { try append(globalThis, array, i, item, lookup_name); } } else { diff --git a/src/install/install.zig b/src/install/install.zig index 3766862d54..555513b573 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6736,8 +6736,8 @@ pub const PackageManager = struct { if (input_str.len > 0) try all_positionals.append(input_str.slice()); } else if (input.isArray()) { - var iter = input.arrayIterator(globalThis); - while (iter.next()) |item| { + var iter = try input.arrayIterator(globalThis); + while (try iter.next()) |item| { const slice = item.toSliceCloneWithAllocator(globalThis, allocator) orelse return .zero; if (globalThis.hasException()) return .zero; if (slice.len == 0) continue; diff --git a/src/install/npm.zig b/src/install/npm.zig index d333ea5c20..4f0c652c91 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -724,8 +724,8 @@ pub const OperatingSystem = enum(u16) { pub fn jsFunctionOperatingSystemIsMatch(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const args = callframe.arguments_old(1); var operating_system = negatable(.none); - var iter = args.ptr[0].arrayIterator(globalObject); - while (iter.next()) |item| { + var iter = try args.ptr[0].arrayIterator(globalObject); + while (try iter.next()) |item| { const slice = try item.toSlice(globalObject, bun.default_allocator); defer slice.deinit(); operating_system.apply(slice.slice()); @@ -841,8 +841,8 @@ pub const Architecture = enum(u16) { pub fn jsFunctionArchitectureIsMatch(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const args = callframe.arguments_old(1); var architecture = negatable(.none); - var iter = args.ptr[0].arrayIterator(globalObject); - while (iter.next()) |item| { + var iter = try args.ptr[0].arrayIterator(globalObject); + while (try iter.next()) |item| { const slice = try item.toSlice(globalObject, bun.default_allocator); defer slice.deinit(); architecture.apply(slice.slice()); diff --git a/src/js_ast.zig b/src/js_ast.zig index b2f5021920..edeb32c2a1 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -8228,7 +8228,7 @@ pub const Macro = struct { switch (loaded_result.unwrap(vm.jsc, .leave_unhandled)) { .rejected => |result| { - _ = vm.unhandledRejection(vm.global, result, loaded_result.asValue()); + vm.unhandledRejection(vm.global, result, loaded_result.asValue()); vm.disableMacroMode(); return error.MacroLoadError; }, @@ -8303,7 +8303,7 @@ pub const Macro = struct { this: *Run, value: JSC.JSValue, ) MacroError!Expr { - return switch (JSC.ConsoleObject.Formatter.Tag.get(value, this.global).tag) { + return switch ((try JSC.ConsoleObject.Formatter.Tag.get(value, this.global)).tag) { .Error => this.coerce(value, .Error), .Undefined => this.coerce(value, .Undefined), .Null => this.coerce(value, .Null), @@ -8409,7 +8409,7 @@ pub const Macro = struct { return _entry.value_ptr.*; } - var iter = JSC.JSArrayIterator.init(value, this.global); + var iter = try JSC.JSArrayIterator.init(value, this.global); if (iter.len == 0) { const result = Expr.init( E.Array, @@ -8435,7 +8435,7 @@ pub const Macro = struct { errdefer this.allocator.free(array); var i: usize = 0; - while (iter.next()) |item| { + while (try iter.next()) |item| { array[i] = try this.run(item); if (array[i].isMissing()) continue; @@ -8539,7 +8539,7 @@ pub const Macro = struct { } if (rejected or promise_result.isError() or promise_result.isAggregateError(this.global) or promise_result.isException(this.global.vm())) { - _ = this.macro.vm.unhandledRejection(this.global, promise_result, promise.asValue()); + this.macro.vm.unhandledRejection(this.global, promise_result, promise.asValue()); return error.MacroFailed; } this.is_top_level = false; diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 334b96c173..c54cd8d883 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -570,7 +570,7 @@ pub export fn napi_get_array_length(env_: napi_env, value_: napi_value, result_: return env.setLastError(.array_expected); } - result.* = @as(u32, @truncate(value.getLength(env.toJS()))); + result.* = @truncate(value.getLength(env.toJS()) catch return env.setLastError(.pending_exception)); return env.ok(); } pub export fn napi_strict_equals(env_: napi_env, lhs_: napi_value, rhs_: napi_value, result_: ?*bool) napi_status { @@ -582,8 +582,8 @@ pub export fn napi_strict_equals(env_: napi_env, lhs_: napi_value, rhs_: napi_va return env.invalidArg(); }; const lhs, const rhs = .{ lhs_.get(), rhs_.get() }; - // there is some nuance with NaN here i'm not sure about - result.* = lhs.isSameValue(rhs, env.toJS()); + // TODO: this needs to be strictEquals not isSameValue (NaN !== NaN and -0 === 0) + result.* = lhs.isSameValue(rhs, env.toJS()) catch return env.setLastError(.pending_exception); return env.ok(); } pub extern fn napi_call_function(env: napi_env, recv: napi_value, func: napi_value, argc: usize, argv: [*c]const napi_value, result: *napi_value) napi_status; @@ -1592,7 +1592,7 @@ pub const ThreadSafeFunction = struct { break :brk .{ !this.isClosing(), t }; }; - this.call(task, !is_first); + this.call(task, !is_first) catch return false; if (queue_finalizer_after_call) { this.maybeQueueFinalizer(); @@ -1604,10 +1604,10 @@ pub const ThreadSafeFunction = struct { /// This function can be called multiple times in one tick of the event loop. /// See: https://github.com/nodejs/node/pull/38506 /// In that case, we need to drain microtasks. - fn call(this: *ThreadSafeFunction, task: ?*anyopaque, is_first: bool) void { + fn call(this: *ThreadSafeFunction, task: ?*anyopaque, is_first: bool) bun.JSExecutionTerminated!void { const env = this.env; if (!is_first) { - this.event_loop.drainMicrotasks(); + try this.event_loop.drainMicrotasks(); } const globalObject = env.toJS(); diff --git a/src/shell/ParsedShellScript.zig b/src/shell/ParsedShellScript.zig index f48092dc6c..1dfce3de9d 100644 --- a/src/shell/ParsedShellScript.zig +++ b/src/shell/ParsedShellScript.zig @@ -110,7 +110,7 @@ pub fn createParsedShellScript(globalThis: *JSC.JSGlobalObject, callframe: *JSC. } const string_args = arguments[0]; const template_args_js = arguments[1]; - var template_args = template_args_js.arrayIterator(globalThis); + var template_args = try template_args_js.arrayIterator(globalThis); var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, shargs.arena_allocator()); var jsstrings = try std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4); diff --git a/src/shell/shell.zig b/src/shell/shell.zig index fcc8cfece0..e4c3160a64 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -3715,10 +3715,10 @@ pub fn shellCmdFromJS( var builder = ShellSrcBuilder.init(globalThis, out_script, jsstrings); var jsobjref_buf: [128]u8 = [_]u8{0} ** 128; - var string_iter = string_args.arrayIterator(globalThis); + var string_iter = try string_args.arrayIterator(globalThis); var i: u32 = 0; const last = string_iter.len -| 1; - while (string_iter.next()) |js_value| { + while (try string_iter.next()) |js_value| { defer i += 1; if (!try builder.appendJSValueStr(js_value, false)) { return globalThis.throw("Shell script string contains invalid UTF-16", .{}); @@ -3726,7 +3726,7 @@ pub fn shellCmdFromJS( // const str = js_value.getZigString(globalThis); // try script.appendSlice(str.full()); if (i < last) { - const template_value = template_args.next() orelse { + const template_value = try template_args.next() orelse { return globalThis.throw("Shell script is missing JSValue arg", .{}); }; try handleTemplateValue(globalThis, template_value, out_jsobjs, out_script, jsstrings, jsobjref_buf[0..]); @@ -3806,10 +3806,10 @@ pub fn handleTemplateValue( } if (template_value.jsType().isArray()) { - var array = template_value.arrayIterator(globalThis); + var array = try template_value.arrayIterator(globalThis); const last = array.len -| 1; var i: u32 = 0; - while (array.next()) |arr| : (i += 1) { + while (try array.next()) |arr| : (i += 1) { try handleTemplateValue(globalThis, arr, out_jsobjs, out_script, jsstrings, jsobjref_buf); if (i < last) { const str = bun.String.static(" "); @@ -3822,7 +3822,7 @@ pub fn handleTemplateValue( } if (template_value.isObject()) { - if (template_value.getOwnTruthy(globalThis, "raw")) |maybe_str| { + if (try template_value.getOwnTruthy(globalThis, "raw")) |maybe_str| { const bunstr = try maybe_str.toBunString(globalThis); defer bunstr.deref(); if (!try builder.appendBunStr(bunstr, false)) { @@ -4308,7 +4308,7 @@ pub const TestingAPIs = struct { const template_args_js = arguments.nextEat() orelse { return globalThis.throw("shell: expected 2 arguments, got 0", .{}); }; - var template_args = template_args_js.arrayIterator(globalThis); + var template_args = try template_args_js.arrayIterator(globalThis); var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); var jsstrings = try std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4); defer { @@ -4376,7 +4376,7 @@ pub const TestingAPIs = struct { const template_args_js = arguments.nextEat() orelse { return globalThis.throw("shell: expected 2 arguments, got 0", .{}); }; - var template_args = template_args_js.arrayIterator(globalThis); + var template_args = try template_args_js.arrayIterator(globalThis); var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); var jsstrings = try std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4); defer { diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index df0664368a..5608ee9bcf 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -586,10 +586,10 @@ pub const PostgresSQLQuery = struct { const bigint = js_bigint.isBoolean() and js_bigint.asBoolean(); const simple = js_simple.isBoolean() and js_simple.asBoolean(); if (simple) { - if (values.getLength(globalThis) > 0) { + if (try values.getLength(globalThis) > 0) { return globalThis.throwInvalidArguments("simple query cannot have parameters", .{}); } - if (query.getLength(globalThis) >= std.math.maxInt(i32)) { + if (try query.getLength(globalThis) >= std.math.maxInt(i32)) { return globalThis.throwInvalidArguments("query is too long", .{}); } } @@ -866,7 +866,7 @@ pub const PostgresRequest = struct { // of parameters. try writer.short(len); - var iter = QueryBindingIterator.init(values_array, columns_value, globalObject); + var iter = try QueryBindingIterator.init(values_array, columns_value, globalObject); for (0..len) |i| { const parameter_field = parameter_fields[i]; const is_custom_type = std.math.maxInt(short) < parameter_field; @@ -874,7 +874,7 @@ pub const PostgresRequest = struct { const force_text = is_custom_type or (tag.isBinaryFormatSupported() and brk: { iter.to(@truncate(i)); - if (iter.next()) |value| { + if (try iter.next()) |value| { break :brk value.isString(); } if (iter.anyFailed()) { @@ -905,7 +905,7 @@ pub const PostgresRequest = struct { debug("Bind: {} ({d} args)", .{ bun.fmt.quote(name), len }); iter.to(0); var i: usize = 0; - while (iter.next()) |value| : (i += 1) { + while (try iter.next()) |value| : (i += 1) { const tag: types.Tag = brk: { if (i >= len) { // parameter in array but not in parameter_fields @@ -3050,9 +3050,9 @@ const QueryBindingIterator = union(enum) { array: JSC.JSArrayIterator, objects: ObjectIterator, - pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) QueryBindingIterator { + pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!QueryBindingIterator { if (columns.isEmptyOrUndefinedOrNull()) { - return .{ .array = JSC.JSArrayIterator.init(array, globalObject) }; + return .{ .array = try JSC.JSArrayIterator.init(array, globalObject) }; } return .{ @@ -3060,8 +3060,8 @@ const QueryBindingIterator = union(enum) { .array = array, .columns = columns, .globalObject = globalObject, - .columns_count = columns.getLength(globalObject), - .array_length = array.getLength(globalObject), + .columns_count = try columns.getLength(globalObject), + .array_length = try array.getLength(globalObject), }, }; } @@ -3089,12 +3089,12 @@ const QueryBindingIterator = union(enum) { const globalObject = this.globalObject; if (this.current_row == .zero) { - this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i)); - if (this.current_row.isEmptyOrUndefinedOrNull()) { - if (!globalObject.hasException()) - return globalObject.throw("Expected a row to be returned at index {d}", .{row_i}) catch null; + this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i)) catch { this.any_failed = true; return null; + }; + if (this.current_row.isEmptyOrUndefinedOrNull()) { + return globalObject.throw("Expected a row to be returned at index {d}", .{row_i}) catch null; } } @@ -3106,12 +3106,12 @@ const QueryBindingIterator = union(enum) { } } - const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i)); - if (property == .zero or property.isUndefined()) { - if (!globalObject.hasException()) - return globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i }) catch null; + const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i)) catch { this.any_failed = true; return null; + }; + if (property.isUndefined()) { + return globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i }) catch null; } const value = this.current_row.getOwnByValue(globalObject, property); @@ -3125,7 +3125,7 @@ const QueryBindingIterator = union(enum) { } }; - pub fn next(this: *QueryBindingIterator) ?JSC.JSValue { + pub fn next(this: *QueryBindingIterator) bun.JSError!?JSC.JSValue { return switch (this.*) { .array => |*iter| iter.next(), .objects => |*iter| iter.next(), @@ -3213,9 +3213,9 @@ const Signature = struct { name.deinit(); } - var iter = QueryBindingIterator.init(array_value, columns, globalObject); + var iter = try QueryBindingIterator.init(array_value, columns, globalObject); - while (iter.next()) |value| { + while (try iter.next()) |value| { if (value.isEmptyOrUndefinedOrNull()) { // Allow postgres to decide the type try fields.append(0); diff --git a/src/sql/postgres/postgres_types.zig b/src/sql/postgres/postgres_types.zig index 7ab4dae90c..4119ad23da 100644 --- a/src/sql/postgres/postgres_types.zig +++ b/src/sql/postgres/postgres_types.zig @@ -353,10 +353,9 @@ pub const Tag = enum(short) { return .int8; } - if (tag.isArrayLike() and value.getLength(globalObject) > 0) { - return Tag.fromJS(globalObject, value.getIndex(globalObject, 0)); + if (tag.isArrayLike() and try value.getLength(globalObject) > 0) { + return Tag.fromJS(globalObject, try value.getIndex(globalObject, 0)); } - if (globalObject.hasException()) return error.JSError; // Ban these types: if (tag == .NumberObject) { diff --git a/src/string.zig b/src/string.zig index 2025afe79a..24380fad21 100644 --- a/src/string.zig +++ b/src/string.zig @@ -514,18 +514,22 @@ pub const String = extern struct { } pub fn fromJS(value: bun.JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!String { + var scope: JSC.CatchScope = undefined; + scope.init(globalObject, @src(), .assertions_only); + defer scope.deinit(); var out: String = String.dead; - if (BunString__fromJS(globalObject, value, &out)) { - if (comptime bun.Environment.isDebug) { - bun.assert(out.tag != .Dead); - } - return out; + const ok = BunString__fromJS(globalObject, value, &out); + + // If there is a pending exception, but stringifying succeeds, we don't return JSError. + // We do need to always call hasException() to satisfy the need for an exception check. + const has_exception = scope.hasException(); + if (ok) { + bun.debugAssert(out.tag != .Dead); + } else { + bun.debugAssert(has_exception); } - if (comptime bun.Environment.isDebug) { - bun.assert(globalObject.hasException()); - } - return error.JSError; + return if (ok) out else error.JSError; } pub fn toJS(this: *const String, globalObject: *bun.JSC.JSGlobalObject) JSC.JSValue { @@ -549,7 +553,7 @@ pub const String = extern struct { /// calls toJS on all elements of `array`. pub fn toJSArray(globalObject: *bun.JSC.JSGlobalObject, array: []const bun.String) bun.JSError!JSC.JSValue { JSC.markBinding(@src()); - return bun.jsc.fromJSHostValue(BunString__createArray(globalObject, array.ptr, array.len)); + return bun.jsc.fromJSHostCall(globalObject, @src(), BunString__createArray, .{ globalObject, array.ptr, array.len }); } pub fn toZigString(this: String) ZigString { diff --git a/src/transpiler.zig b/src/transpiler.zig index 848e389289..fdede87e22 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -241,7 +241,7 @@ pub const PluginRunner = struct { if (!path_value.isString()) { return JSC.ErrorableString.err( error.JSErrorObject, - bun.String.static("Expected \"path\" to be a string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + bun.String.static("Expected \"path\" to be a string in onResolve plugin").toErrorInstance(this.global_object), ); } @@ -250,7 +250,7 @@ pub const PluginRunner = struct { if (file_path.length() == 0) { return JSC.ErrorableString.err( error.JSErrorObject, - bun.String.static("Expected \"path\" to be a non-empty string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + bun.String.static("Expected \"path\" to be a non-empty string in onResolve plugin").toErrorInstance(this.global_object), ); } else if // TODO: validate this better @@ -261,7 +261,7 @@ pub const PluginRunner = struct { { return JSC.ErrorableString.err( error.JSErrorObject, - bun.String.static("\"path\" is invalid in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + bun.String.static("\"path\" is invalid in onResolve plugin").toErrorInstance(this.global_object), ); } var static_namespace = true; @@ -270,7 +270,7 @@ pub const PluginRunner = struct { if (!namespace_value.isString()) { return JSC.ErrorableString.err( error.JSErrorObject, - bun.String.static("Expected \"namespace\" to be a string").toErrorInstance(this.global_object).asVoid(), + bun.String.static("Expected \"namespace\" to be a string").toErrorInstance(this.global_object), ); } diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 4abe8b7b1f..e3cac8e08c 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -6,7 +6,7 @@ pub fn jsSend(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callfram if (!args_array.isObject() or !args_array.isArray()) { return globalObject.throw("Arguments must be an array", .{}); } - var iter = args_array.arrayIterator(globalObject); + var iter = try args_array.arrayIterator(globalObject); var args = try std.ArrayList(JSArgument).initCapacity(bun.default_allocator, iter.len); defer { for (args.items) |*item| { @@ -15,7 +15,7 @@ pub fn jsSend(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callfram args.deinit(); } - while (iter.next()) |arg_js| { + while (try iter.next()) |arg_js| { args.appendAssumeCapacity(try fromJS(globalObject, arg_js) orelse { return globalObject.throwInvalidArgumentType("sendCommand", "argument", "string or buffer"); }); @@ -390,7 +390,7 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe return globalObject.throw("Fields must be an array", .{}); } - var iter = fields_array.arrayIterator(globalObject); + var iter = try fields_array.arrayIterator(globalObject); var args = try std.ArrayList(JSC.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1); defer { for (args.items) |item| { @@ -402,7 +402,7 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe args.appendAssumeCapacity(JSC.ZigString.Slice.fromUTF8NeverFree(key.slice())); // Add field names as arguments - while (iter.next()) |field_js| { + while (try iter.next()) |field_js| { const field_str = try field_js.toBunString(globalObject); defer field_str.deref(); @@ -495,7 +495,7 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); } - var iter = array_arg.arrayIterator(globalObject); + var iter = try array_arg.arrayIterator(globalObject); if (iter.len % 2 != 0) { return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); } @@ -514,7 +514,7 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe args.appendAssumeCapacity(key_slice); // Add field-value pairs - while (iter.next()) |field_js| { + while (try iter.next()) |field_js| { // Add field name const field_str = try field_js.toBunString(globalObject); defer field_str.deref(); @@ -522,7 +522,7 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe args.appendAssumeCapacity(field_slice); // Add value - if (iter.next()) |value_js| { + if (try iter.next()) |value_js| { const value_str = try value_js.toBunString(globalObject); defer value_str.deref(); const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator); diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index f23b5535ab..f840f0987b 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -43,6 +43,10 @@ const words: Record ".stdDir()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 49 }, ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 284 }, "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 175 }, + + "global.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 28 }, + "globalObject.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 49 }, + "globalThis.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 139 }, }; const words_keys = [...Object.keys(words)]; diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt new file mode 100644 index 0000000000..0714b5bca3 --- /dev/null +++ b/test/no-validate-exceptions.txt @@ -0,0 +1,2807 @@ +# List of tests for which we do NOT set validateExceptionChecks=1 when running in ASan CI +test/bake/dev-and-prod.test.ts +test/bake/dev/plugins.test.ts +test/bake/framework-router.test.ts +test/bundler/bun-build-api.test.ts +test/bundler/bundler_banner.test.ts +test/bundler/bundler_browser.test.ts +test/bundler/bundler_bun.test.ts +test/bundler/bundler_cjs2esm.test.ts +test/bundler/bundler_comments.test.ts +test/bundler/bundler_compile.test.ts +test/bundler/bundler_decorator_metadata.test.ts +test/bundler/bundler_defer.test.ts +test/bundler/bundler_drop.test.ts +test/bundler/bundler_env.test.ts +test/bundler/bundler_footer.test.ts +test/bundler/bundler_html.test.ts +test/bundler/bundler_jsx.test.ts +test/bundler/bundler_minify.test.ts +test/bundler/bundler_naming.test.ts +test/bundler/bundler_plugin.test.ts +test/bundler/bundler_regressions.test.ts +test/bundler/bundler_string.test.ts +test/bundler/css/css-modules.test.ts +test/bundler/css/wpt/background-computed.test.ts +test/bundler/css/wpt/color-computed-rgb.test.ts +test/bundler/css/wpt/color-computed.test.ts +test/bundler/css/wpt/relative_color_out_of_gamut.test.ts +test/bundler/esbuild/css.test.ts +test/bundler/esbuild/dce.test.ts +test/bundler/esbuild/extra.test.ts +test/bundler/esbuild/importstar_ts.test.ts +test/bundler/esbuild/importstar.test.ts +test/bundler/esbuild/loader.test.ts +test/bundler/esbuild/lower.test.ts +test/bundler/esbuild/packagejson.test.ts +test/bundler/esbuild/splitting.test.ts +test/bundler/esbuild/ts.test.ts +test/bundler/esbuild/tsconfig.test.ts +test/bundler/html-import-manifest.test.ts +test/bundler/transpiler/bun-pragma.test.ts +test/bundler/transpiler/jsx-production.test.ts +test/bundler/transpiler/macro-test.test.ts +test/bundler/transpiler/runtime-transpiler.test.ts +test/bundler/transpiler/transpiler.test.js +test/cli/init/init.test.ts +test/cli/install/bun-add.test.ts +test/cli/install/bun-audit.test.ts +test/cli/install/bun-create.test.ts +test/cli/install/bun-info.test.ts +test/cli/install/bun-install-dep.test.ts +test/cli/install/bun-install-lifecycle-scripts.test.ts +test/cli/install/bun-install-patch.test.ts +test/cli/install/bun-install-registry.test.ts +test/cli/install/bun-install-retry.test.ts +test/cli/install/bun-link.test.ts +test/cli/install/bun-lock.test.ts +test/cli/install/bun-lockb.test.ts +test/cli/install/bun-pack.test.ts +test/cli/install/bun-patch.test.ts +test/cli/install/bun-pm.test.ts +test/cli/install/bun-publish.test.ts +test/cli/install/bun-remove.test.ts +test/cli/install/bun-update.test.ts +test/cli/install/bun-upgrade.test.ts +test/cli/install/bun-workspaces.test.ts +test/cli/install/bunx.test.ts +test/cli/install/catalogs.test.ts +test/cli/install/npmrc.test.ts +test/cli/install/overrides.test.ts +test/cli/run/env.test.ts +test/cli/run/esm-defineProperty.test.ts +test/cli/run/jsx-namespaced-attributes.test.ts +test/cli/run/log-test.test.ts +test/cli/run/require-and-import-trailing.test.ts +test/cli/run/run-autoinstall.test.ts +test/cli/run/run-eval.test.ts +test/cli/run/self-reference.test.ts +test/cli/run/shell-keepalive.test.ts +test/cli/test/bun-test.test.ts +test/cli/watch/watch.test.ts +test/config/bunfig/preload.test.ts +test/integration/bun-types/bun-types.test.ts +test/integration/bun-types/fixture/serve-types.test.ts +test/integration/esbuild/esbuild.test.ts +test/integration/jsdom/jsdom.test.ts +test/integration/mysql2/mysql2.test.ts +test/integration/nest/nest_metadata.test.ts +test/integration/sass/sass.test.ts +test/integration/sharp/sharp.test.ts +test/integration/svelte/client-side.test.ts +test/integration/typegraphql/src/typegraphql.test.ts +test/internal/bindgen.test.ts +test/js/bun/bun-object/deep-match.spec.ts +test/js/bun/bun-object/write.spec.ts +test/js/bun/console/bun-inspect-table.test.ts +test/js/bun/console/console-iterator.test.ts +test/js/bun/console/console-table.test.ts +test/js/bun/cookie/cookie-expires-validation.test.ts +test/js/bun/cookie/cookie-map.test.ts +test/js/bun/cookie/cookie.test.ts +test/js/bun/crypto/cipheriv-decipheriv.test.ts +test/js/bun/crypto/wpt-webcrypto.generateKey.test.ts +test/js/bun/dns/resolve-dns.test.ts +test/js/bun/glob/scan.test.ts +test/js/bun/globals.test.js +test/js/bun/http/async-iterator-stream.test.ts +test/js/bun/http/bun-connect-x509.test.ts +test/js/bun/http/bun-serve-args.test.ts +test/js/bun/http/bun-serve-cookies.test.ts +test/js/bun/http/bun-serve-file.test.ts +test/js/bun/http/bun-serve-headers.test.ts +test/js/bun/http/bun-serve-html-entry.test.ts +test/js/bun/http/bun-serve-html.test.ts +test/js/bun/http/bun-serve-routes.test.ts +test/js/bun/http/bun-serve-static.test.ts +test/js/bun/http/bun-server.test.ts +test/js/bun/http/decodeURIComponentSIMD.test.ts +test/js/bun/http/fetch-file-upload.test.ts +test/js/bun/http/hspec.test.ts +test/js/bun/http/http-server-chunking.test.ts +test/js/bun/http/http-spec.ts +test/js/bun/http/leaks-test.test.ts +test/js/bun/http/proxy.test.ts +test/js/bun/http/serve-body-leak.test.ts +test/js/bun/http/serve-listen.test.ts +test/js/bun/http/serve.test.ts +test/js/bun/import-attributes/import-attributes.test.ts +test/js/bun/ini/ini.test.ts +test/js/bun/io/bun-write.test.js +test/js/bun/jsc/bun-jsc.test.ts +test/js/bun/jsc/domjit.test.ts +test/js/bun/net/socket.test.ts +test/js/bun/net/tcp-server.test.ts +test/js/bun/net/tcp.spec.ts +test/js/bun/net/tcp.test.ts +test/js/bun/patch/patch.test.ts +test/js/bun/perf_hooks/histogram.test.ts +test/js/bun/plugin/plugins.test.ts +test/js/bun/resolve/build-error.test.ts +test/js/bun/resolve/bun-lock.test.ts +test/js/bun/resolve/esModule-annotation.test.js +test/js/bun/resolve/import-custom-condition.test.ts +test/js/bun/resolve/import-empty.test.js +test/js/bun/resolve/import-meta-resolve.test.mjs +test/js/bun/resolve/import-meta.test.js +test/js/bun/resolve/jsonc.test.ts +test/js/bun/resolve/require.test.ts +test/js/bun/resolve/toml/toml.test.js +test/js/bun/s3/s3-insecure.test.ts +test/js/bun/s3/s3-list-objects.test.ts +test/js/bun/s3/s3-storage-class.test.ts +test/js/bun/s3/s3.test.ts +test/js/bun/shell/bunshell-default.test.ts +test/js/bun/shell/bunshell-file.test.ts +test/js/bun/shell/bunshell-instance.test.ts +test/js/bun/shell/commands/basename.test.ts +test/js/bun/shell/commands/dirname.test.ts +test/js/bun/shell/commands/exit.test.ts +test/js/bun/shell/commands/false.test.ts +test/js/bun/shell/commands/mv.test.ts +test/js/bun/shell/commands/rm.test.ts +test/js/bun/shell/commands/seq.test.ts +test/js/bun/shell/commands/true.test.ts +test/js/bun/shell/commands/which.test.ts +test/js/bun/shell/commands/yes.test.ts +test/js/bun/shell/env.positionals.test.ts +test/js/bun/shell/exec.test.ts +test/js/bun/shell/lazy.test.ts +test/js/bun/shell/leak.test.ts +test/js/bun/shell/lex.test.ts +test/js/bun/shell/shell-hang.test.ts +test/js/bun/shell/shell-load.test.ts +test/js/bun/shell/shelloutput.test.ts +test/js/bun/shell/throw.test.ts +test/js/bun/spawn/bun-ipc-inherit.test.ts +test/js/bun/spawn/job-object-bug.test.ts +test/js/bun/spawn/spawn_waiter_thread.test.ts +test/js/bun/spawn/spawn-empty-arrayBufferOrBlob.test.ts +test/js/bun/spawn/spawn-path.test.ts +test/js/bun/spawn/spawn-stdin-destroy.test.ts +test/js/bun/spawn/spawn-stream-serve.test.ts +test/js/bun/spawn/spawn-streaming-stdout.test.ts +test/js/bun/spawn/spawn-stress.test.ts +test/js/bun/spawn/spawn.ipc.bun-node.test.ts +test/js/bun/spawn/spawn.ipc.node-bun.test.ts +test/js/bun/spawn/spawn.ipc.test.ts +test/js/bun/sqlite/sql-timezone.test.js +test/js/bun/stream/direct-readable-stream.test.tsx +test/js/bun/symbols.test.ts +test/js/bun/test/done-async.test.ts +test/js/bun/test/expect-assertions.test.ts +test/js/bun/test/jest-extended.test.js +test/js/bun/test/mock-fn.test.js +test/js/bun/test/mock/6874/A.test.ts +test/js/bun/test/mock/6874/B.test.ts +test/js/bun/test/mock/6879/6879.test.ts +test/js/bun/test/mock/mock-module.test.ts +test/js/bun/test/snapshot-tests/bun-snapshots.test.ts +test/js/bun/test/snapshot-tests/existing-snapshots.test.ts +test/js/bun/test/snapshot-tests/new-snapshot.test.ts +test/js/bun/test/snapshot-tests/snapshots/more.test.ts +test/js/bun/test/snapshot-tests/snapshots/moremore.test.ts +test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +test/js/bun/test/stack.test.ts +test/js/bun/test/test-failing.test.ts +test/js/bun/test/test-only.test.ts +test/js/bun/test/test-test.test.ts +test/js/bun/udp/dgram.test.ts +test/js/bun/udp/udp_socket.test.ts +test/js/bun/util/arraybuffersink.test.ts +test/js/bun/util/bun-cryptohasher.test.ts +test/js/bun/util/bun-file-exists.test.js +test/js/bun/util/bun-file.test.ts +test/js/bun/util/bun-isMainThread.test.js +test/js/bun/util/BunObject.test.ts +test/js/bun/util/concat.test.js +test/js/bun/util/cookie.test.js +test/js/bun/util/error-gc-test.test.js +test/js/bun/util/escapeHTML.test.js +test/js/bun/util/filesink.test.ts +test/js/bun/util/filesystem_router.test.ts +test/js/bun/util/fileUrl.test.js +test/js/bun/util/fuzzy-wuzzy.test.ts +test/js/bun/util/hash.test.js +test/js/bun/util/heap-snapshot.test.ts +test/js/bun/util/index-of-line.test.ts +test/js/bun/util/inspect-error.test.js +test/js/bun/util/inspect.test.js +test/js/bun/util/mmap.test.js +test/js/bun/util/password.test.ts +test/js/bun/util/readablestreamtoarraybuffer.test.ts +test/js/bun/util/stringWidth.test.ts +test/js/bun/util/text-loader.test.ts +test/js/bun/util/unsafe.test.js +test/js/bun/util/v8-heap-snapshot.test.ts +test/js/bun/util/which.test.ts +test/js/bun/util/zstd.test.ts +test/js/bun/websocket/websocket-server.test.ts +test/js/deno/abort/abort-controller.test.ts +test/js/deno/crypto/random.test.ts +test/js/deno/crypto/webcrypto.test.ts +test/js/deno/encoding/encoding.test.ts +test/js/deno/event/custom-event.test.ts +test/js/deno/event/event-target.test.ts +test/js/deno/event/event.test.ts +test/js/deno/fetch/blob.test.ts +test/js/deno/fetch/body.test.ts +test/js/deno/fetch/headers.test.ts +test/js/deno/fetch/request.test.ts +test/js/deno/fetch/response.test.ts +test/js/deno/performance/performance.test.ts +test/js/deno/url/url.test.ts +test/js/deno/url/urlsearchparams.test.ts +test/js/deno/v8/error.test.ts +test/js/first_party/undici/undici.test.ts +test/js/junit-reporter/junit.test.js +test/js/node/assert/assert-promise.test.ts +test/js/node/assert/assert.spec.ts +test/js/node/async_hooks/AsyncLocalStorage.test.ts +test/js/node/buffer-concat.test.ts +test/js/node/buffer.test.js +test/js/node/child_process/child_process_ipc_large_disconnect.test.js +test/js/node/child_process/child_process_ipc.test.js +test/js/node/child_process/child_process_send_cb.test.js +test/js/node/child_process/child_process-node.test.js +test/js/node/child_process/child-process-exec.test.ts +test/js/node/child_process/child-process-stdio.test.js +test/js/node/cluster.test.ts +test/js/node/cluster/test-docs-http-server.ts +test/js/node/cluster/test-worker-no-exit-http.ts +test/js/node/crypto/crypto-hmac-algorithm.test.ts +test/js/node/crypto/crypto-oneshot.test.ts +test/js/node/crypto/crypto-random.test.ts +test/js/node/crypto/crypto-rsa.test.js +test/js/node/crypto/crypto.hmac.test.ts +test/js/node/crypto/crypto.key-objects.test.ts +test/js/node/crypto/crypto.test.ts +test/js/node/crypto/ecdh.test.ts +test/js/node/crypto/node-crypto.test.js +test/js/node/crypto/pbkdf2.test.ts +test/js/node/diagnostics_channel/diagnostics_channel.test.ts +test/js/node/dns/dns-lookup-keepalive.test.ts +test/js/node/dns/node-dns.test.js +test/js/node/events/event-emitter.test.ts +test/js/node/fs/cp.test.ts +test/js/node/fs/dir.test.ts +test/js/node/fs/fs-leak.test.js +test/js/node/fs/fs-mkdir.test.ts +test/js/node/fs/fs-oom.test.ts +test/js/node/fs/fs-promises-writeFile-async-iterator.test.ts +test/js/node/fs/fs-stats-truncate.test.ts +test/js/node/fs/fs.test.ts +test/js/node/fs/glob.test.ts +test/js/node/fs/promises.test.js +test/js/node/http/client-timeout-error.test.ts +test/js/node/http/node-fetch.test.js +test/js/node/http/node-http-backpressure.test.ts +test/js/node/http/node-http-parser.test.ts +test/js/node/http/node-http-primoridals.test.ts +test/js/node/http/node-http-transfer-encoding.test.ts +test/js/node/http/node-http.test.ts +test/js/node/http/numeric-header.test.ts +test/js/node/module/node-module-module.test.js +test/js/node/module/require-extensions.test.ts +test/js/node/net/double-connect.test.ts +test/js/node/net/node-net-allowHalfOpen.test.js +test/js/node/net/node-net-server.test.ts +test/js/node/net/node-net.test.ts +test/js/node/net/server.spec.ts +test/js/node/no-addons.test.ts +test/js/node/os/os.test.js +test/js/node/path/browserify.test.js +test/js/node/path/matches-glob.test.ts +test/js/node/path/parse-format.test.js +test/js/node/path/to-namespaced-path.test.js +test/js/node/process/call-constructor.test.js +test/js/node/process/process-args.test.js +test/js/node/process/process-nexttick.test.js +test/js/node/process/process-stdin.test.ts +test/js/node/process/process-stdio.test.ts +test/js/node/process/process.test.js +test/js/node/promise/reject-tostring.test.ts +test/js/node/readline/pause_stdin_should_exit.test.ts +test/js/node/readline/readline_never_unrefs.test.ts +test/js/node/readline/readline_promises.node.test.ts +test/js/node/readline/readline.node.test.ts +test/js/node/readline/stdin_fell_asleep.test.ts +test/js/node/stream/node-stream-uint8array.test.ts +test/js/node/stream/node-stream.test.js +test/js/node/string_decoder/string-decoder.test.js +test/js/node/string-module.test.js +test/js/node/stubs.test.js +test/js/node/test_runner/node-test.test.ts +test/js/node/test/parallel/test-abortsignal-any.mjs +test/js/node/test/parallel/test-assert-async.js +test/js/node/test/parallel/test-assert-builtins-not-read-from-filesystem.js +test/js/node/test/parallel/test-assert-calltracker-calls.js +test/js/node/test/parallel/test-assert-calltracker-getCalls.js +test/js/node/test/parallel/test-assert-calltracker-report.js +test/js/node/test/parallel/test-assert-calltracker-verify.js +test/js/node/test/parallel/test-assert-checktag.js +test/js/node/test/parallel/test-assert-deep-with-error.js +test/js/node/test/parallel/test-assert-esm-cjs-message-verify.js +test/js/node/test/parallel/test-assert-fail-deprecation.js +test/js/node/test/parallel/test-assert-if-error.js +test/js/node/test/parallel/test-assert-strict-exists.js +test/js/node/test/parallel/test-assert.js +test/js/node/test/parallel/test-async-hooks-asyncresource-constructor.js +test/js/node/test/parallel/test-async-hooks-constructor.js +test/js/node/test/parallel/test-async-hooks-recursive-stack-runInAsyncScope.js +test/js/node/test/parallel/test-async-hooks-run-in-async-scope-caught-exception.js +test/js/node/test/parallel/test-async-hooks-run-in-async-scope-this-arg.js +test/js/node/test/parallel/test-async-hooks-vm-gc.js +test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-1.js +test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-2.js +test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-3.js +test/js/node/test/parallel/test-async-hooks-worker-asyncfn-terminate-4.js +test/js/node/test/parallel/test-async-local-storage-bind.js +test/js/node/test/parallel/test-async-local-storage-contexts.js +test/js/node/test/parallel/test-async-local-storage-deep-stack.js +test/js/node/test/parallel/test-async-local-storage-enter-with.js +test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js +test/js/node/test/parallel/test-async-local-storage-http-multiclients.js +test/js/node/test/parallel/test-async-local-storage-snapshot.js +test/js/node/test/parallel/test-async-wrap-constructor.js +test/js/node/test/parallel/test-atomics-wake.js +test/js/node/test/parallel/test-bad-unicode.js +test/js/node/test/parallel/test-beforeexit-event-exit.js +test/js/node/test/parallel/test-binding-constants.js +test/js/node/test/parallel/test-blob-createobjecturl.js +test/js/node/test/parallel/test-blocklist-clone.js +test/js/node/test/parallel/test-blocklist.js +test/js/node/test/parallel/test-broadcastchannel-custom-inspect.js +test/js/node/test/parallel/test-btoa-atob.js +test/js/node/test/parallel/test-buffer-alloc.js +test/js/node/test/parallel/test-buffer-arraybuffer.js +test/js/node/test/parallel/test-buffer-ascii.js +test/js/node/test/parallel/test-buffer-backing-arraybuffer.js +test/js/node/test/parallel/test-buffer-badhex.js +test/js/node/test/parallel/test-buffer-bigint64.js +test/js/node/test/parallel/test-buffer-bytelength.js +test/js/node/test/parallel/test-buffer-compare-offset.js +test/js/node/test/parallel/test-buffer-compare.js +test/js/node/test/parallel/test-buffer-concat.js +test/js/node/test/parallel/test-buffer-constants.js +test/js/node/test/parallel/test-buffer-constructor-deprecation-error.js +test/js/node/test/parallel/test-buffer-constructor-node-modules-paths.js +test/js/node/test/parallel/test-buffer-constructor-node-modules.js +test/js/node/test/parallel/test-buffer-constructor-outside-node-modules.js +test/js/node/test/parallel/test-buffer-copy.js +test/js/node/test/parallel/test-buffer-equals.js +test/js/node/test/parallel/test-buffer-failed-alloc-typed-arrays.js +test/js/node/test/parallel/test-buffer-fakes.js +test/js/node/test/parallel/test-buffer-fill.js +test/js/node/test/parallel/test-buffer-from.js +test/js/node/test/parallel/test-buffer-includes.js +test/js/node/test/parallel/test-buffer-indexof.js +test/js/node/test/parallel/test-buffer-inheritance.js +test/js/node/test/parallel/test-buffer-inspect.js +test/js/node/test/parallel/test-buffer-isascii.js +test/js/node/test/parallel/test-buffer-isencoding.js +test/js/node/test/parallel/test-buffer-isutf8.js +test/js/node/test/parallel/test-buffer-iterator.js +test/js/node/test/parallel/test-buffer-new.js +test/js/node/test/parallel/test-buffer-no-negative-allocation.js +test/js/node/test/parallel/test-buffer-nopendingdep-map.js +test/js/node/test/parallel/test-buffer-of-no-deprecation.js +test/js/node/test/parallel/test-buffer-over-max-length.js +test/js/node/test/parallel/test-buffer-parent-property.js +test/js/node/test/parallel/test-buffer-pending-deprecation.js +test/js/node/test/parallel/test-buffer-pool-untransferable.js +test/js/node/test/parallel/test-buffer-prototype-inspect.js +test/js/node/test/parallel/test-buffer-read.js +test/js/node/test/parallel/test-buffer-readdouble.js +test/js/node/test/parallel/test-buffer-readfloat.js +test/js/node/test/parallel/test-buffer-readint.js +test/js/node/test/parallel/test-buffer-readuint.js +test/js/node/test/parallel/test-buffer-safe-unsafe.js +test/js/node/test/parallel/test-buffer-set-inspect-max-bytes.js +test/js/node/test/parallel/test-buffer-sharedarraybuffer.js +test/js/node/test/parallel/test-buffer-slice.js +test/js/node/test/parallel/test-buffer-slow.js +test/js/node/test/parallel/test-buffer-swap.js +test/js/node/test/parallel/test-buffer-tojson.js +test/js/node/test/parallel/test-buffer-tostring-range.js +test/js/node/test/parallel/test-buffer-tostring-rangeerror.js +test/js/node/test/parallel/test-buffer-tostring.js +test/js/node/test/parallel/test-buffer-write-fast.js +test/js/node/test/parallel/test-buffer-write.js +test/js/node/test/parallel/test-buffer-writedouble.js +test/js/node/test/parallel/test-buffer-writefloat.js +test/js/node/test/parallel/test-buffer-writeint.js +test/js/node/test/parallel/test-buffer-writeuint.js +test/js/node/test/parallel/test-buffer-zero-fill-cli.js +test/js/node/test/parallel/test-buffer-zero-fill-reset.js +test/js/node/test/parallel/test-buffer-zero-fill.js +test/js/node/test/parallel/test-c-ares.js +test/js/node/test/parallel/test-child-process-advanced-serialization-largebuffer.js +test/js/node/test/parallel/test-child-process-advanced-serialization.js +test/js/node/test/parallel/test-child-process-can-write-to-stdout.js +test/js/node/test/parallel/test-child-process-constructor.js +test/js/node/test/parallel/test-child-process-cwd.js +test/js/node/test/parallel/test-child-process-default-options.js +test/js/node/test/parallel/test-child-process-destroy.js +test/js/node/test/parallel/test-child-process-detached.js +test/js/node/test/parallel/test-child-process-dgram-reuseport.js +test/js/node/test/parallel/test-child-process-disconnect.js +test/js/node/test/parallel/test-child-process-double-pipe.js +test/js/node/test/parallel/test-child-process-emfile.js +test/js/node/test/parallel/test-child-process-env.js +test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js +test/js/node/test/parallel/test-child-process-exec-any-shells-windows.js +test/js/node/test/parallel/test-child-process-exec-cwd.js +test/js/node/test/parallel/test-child-process-exec-encoding.js +test/js/node/test/parallel/test-child-process-exec-env.js +test/js/node/test/parallel/test-child-process-exec-error.js +test/js/node/test/parallel/test-child-process-exec-maxbuf.js +test/js/node/test/parallel/test-child-process-exec-std-encoding.js +test/js/node/test/parallel/test-child-process-exec-stdout-stderr-data-string.js +test/js/node/test/parallel/test-child-process-exec-timeout-expire.js +test/js/node/test/parallel/test-child-process-exec-timeout-kill.js +test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js +test/js/node/test/parallel/test-child-process-execfile-maxbuf.js +test/js/node/test/parallel/test-child-process-execFile-promisified-abortController.js +test/js/node/test/parallel/test-child-process-execfile.js +test/js/node/test/parallel/test-child-process-execfilesync-maxbuf.js +test/js/node/test/parallel/test-child-process-execsync-maxbuf.js +test/js/node/test/parallel/test-child-process-exit-code.js +test/js/node/test/parallel/test-child-process-flush-stdio.js +test/js/node/test/parallel/test-child-process-fork-abort-signal.js +test/js/node/test/parallel/test-child-process-fork-and-spawn.js +test/js/node/test/parallel/test-child-process-fork-args.js +test/js/node/test/parallel/test-child-process-fork-close.js +test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js +test/js/node/test/parallel/test-child-process-fork-detached.js +test/js/node/test/parallel/test-child-process-fork-exec-path.js +test/js/node/test/parallel/test-child-process-fork-no-shell.js +test/js/node/test/parallel/test-child-process-fork-ref.js +test/js/node/test/parallel/test-child-process-fork-ref2.js +test/js/node/test/parallel/test-child-process-fork-stdio-string-variant.js +test/js/node/test/parallel/test-child-process-fork-timeout-kill-signal.js +test/js/node/test/parallel/test-child-process-fork-url.mjs +test/js/node/test/parallel/test-child-process-fork.js +test/js/node/test/parallel/test-child-process-fork3.js +test/js/node/test/parallel/test-child-process-ipc-next-tick.js +test/js/node/test/parallel/test-child-process-ipc.js +test/js/node/test/parallel/test-child-process-kill.js +test/js/node/test/parallel/test-child-process-net-reuseport.js +test/js/node/test/parallel/test-child-process-no-deprecation.js +test/js/node/test/parallel/test-child-process-promisified.js +test/js/node/test/parallel/test-child-process-prototype-tampering.mjs +test/js/node/test/parallel/test-child-process-reject-null-bytes.js +test/js/node/test/parallel/test-child-process-send-after-close.js +test/js/node/test/parallel/test-child-process-send-cb.js +test/js/node/test/parallel/test-child-process-send-type-error.js +test/js/node/test/parallel/test-child-process-send-utf8.js +test/js/node/test/parallel/test-child-process-set-blocking.js +test/js/node/test/parallel/test-child-process-silent.js +test/js/node/test/parallel/test-child-process-spawn-args.js +test/js/node/test/parallel/test-child-process-spawn-argv0.js +test/js/node/test/parallel/test-child-process-spawn-controller.js +test/js/node/test/parallel/test-child-process-spawn-error.js +test/js/node/test/parallel/test-child-process-spawn-event.js +test/js/node/test/parallel/test-child-process-spawn-shell.js +test/js/node/test/parallel/test-child-process-spawn-timeout-kill-signal.js +test/js/node/test/parallel/test-child-process-spawn-typeerror.js +test/js/node/test/parallel/test-child-process-spawnsync-args.js +test/js/node/test/parallel/test-child-process-spawnsync-env.js +test/js/node/test/parallel/test-child-process-spawnsync-input.js +test/js/node/test/parallel/test-child-process-spawnsync-kill-signal.js +test/js/node/test/parallel/test-child-process-spawnsync-maxbuf.js +test/js/node/test/parallel/test-child-process-spawnsync-shell.js +test/js/node/test/parallel/test-child-process-spawnsync-timeout.js +test/js/node/test/parallel/test-child-process-spawnsync-validation-errors.js +test/js/node/test/parallel/test-child-process-spawnsync.js +test/js/node/test/parallel/test-child-process-stdin-ipc.js +test/js/node/test/parallel/test-child-process-stdin.js +test/js/node/test/parallel/test-child-process-stdio-big-write-end.js +test/js/node/test/parallel/test-child-process-stdio-inherit.js +test/js/node/test/parallel/test-child-process-stdio-overlapped.js +test/js/node/test/parallel/test-child-process-stdio.js +test/js/node/test/parallel/test-child-process-stdout-flush-exit.js +test/js/node/test/parallel/test-child-process-stdout-flush.js +test/js/node/test/parallel/test-child-process-stdout-ipc.js +test/js/node/test/parallel/test-cli-eval-event.js +test/js/node/test/parallel/test-cli-options-precedence.js +test/js/node/test/parallel/test-client-request-destroy.js +test/js/node/test/parallel/test-cluster-advanced-serialization.js +test/js/node/test/parallel/test-cluster-bind-privileged-port.js +test/js/node/test/parallel/test-cluster-call-and-destroy.js +test/js/node/test/parallel/test-cluster-child-index-dgram.js +test/js/node/test/parallel/test-cluster-child-index-net.js +test/js/node/test/parallel/test-cluster-concurrent-disconnect.js +test/js/node/test/parallel/test-cluster-cwd.js +test/js/node/test/parallel/test-cluster-dgram-ipv6only.js +test/js/node/test/parallel/test-cluster-dgram-reuse.js +test/js/node/test/parallel/test-cluster-dgram-reuseport.js +test/js/node/test/parallel/test-cluster-disconnect-before-exit.js +test/js/node/test/parallel/test-cluster-disconnect-exitedAfterDisconnect-race.js +test/js/node/test/parallel/test-cluster-disconnect-idle-worker.js +test/js/node/test/parallel/test-cluster-disconnect-leak.js +test/js/node/test/parallel/test-cluster-disconnect-with-no-workers.js +test/js/node/test/parallel/test-cluster-eaddrinuse.js +test/js/node/test/parallel/test-cluster-fork-env.js +test/js/node/test/parallel/test-cluster-fork-windowsHide.js +test/js/node/test/parallel/test-cluster-http-pipe.js +test/js/node/test/parallel/test-cluster-invalid-message.js +test/js/node/test/parallel/test-cluster-ipc-throw.js +test/js/node/test/parallel/test-cluster-kill-disconnect.js +test/js/node/test/parallel/test-cluster-kill-infinite-loop.js +test/js/node/test/parallel/test-cluster-listening-port.js +test/js/node/test/parallel/test-cluster-message.js +test/js/node/test/parallel/test-cluster-net-listen.js +test/js/node/test/parallel/test-cluster-primary-error.js +test/js/node/test/parallel/test-cluster-primary-kill.js +test/js/node/test/parallel/test-cluster-process-disconnect.js +test/js/node/test/parallel/test-cluster-rr-domain-listen.js +test/js/node/test/parallel/test-cluster-rr-handle-keep-loop-alive.js +test/js/node/test/parallel/test-cluster-rr-ref.js +test/js/node/test/parallel/test-cluster-send-deadlock.js +test/js/node/test/parallel/test-cluster-setup-primary-argv.js +test/js/node/test/parallel/test-cluster-setup-primary-cumulative.js +test/js/node/test/parallel/test-cluster-setup-primary-emit.js +test/js/node/test/parallel/test-cluster-setup-primary-multiple.js +test/js/node/test/parallel/test-cluster-setup-primary.js +test/js/node/test/parallel/test-cluster-shared-handle-bind-privileged-port.js +test/js/node/test/parallel/test-cluster-uncaught-exception.js +test/js/node/test/parallel/test-cluster-worker-constructor.js +test/js/node/test/parallel/test-cluster-worker-death.js +test/js/node/test/parallel/test-cluster-worker-destroy.js +test/js/node/test/parallel/test-cluster-worker-disconnect-on-error.js +test/js/node/test/parallel/test-cluster-worker-disconnect.js +test/js/node/test/parallel/test-cluster-worker-events.js +test/js/node/test/parallel/test-cluster-worker-exit.js +test/js/node/test/parallel/test-cluster-worker-forced-exit.js +test/js/node/test/parallel/test-cluster-worker-init.js +test/js/node/test/parallel/test-cluster-worker-isconnected.js +test/js/node/test/parallel/test-cluster-worker-isdead.js +test/js/node/test/parallel/test-cluster-worker-kill-signal.js +test/js/node/test/parallel/test-cluster-worker-kill.js +test/js/node/test/parallel/test-cluster-worker-no-exit.js +test/js/node/test/parallel/test-cluster-worker-wait-server-close.js +test/js/node/test/parallel/test-common-countdown.js +test/js/node/test/parallel/test-common-expect-warning.js +test/js/node/test/parallel/test-common-must-not-call.js +test/js/node/test/parallel/test-config-json-schema.js +test/js/node/test/parallel/test-console-assign-undefined.js +test/js/node/test/parallel/test-console-async-write-error.js +test/js/node/test/parallel/test-console-group.js +test/js/node/test/parallel/test-console-instance.js +test/js/node/test/parallel/test-console-issue-43095.js +test/js/node/test/parallel/test-console-log-stdio-broken-dest.js +test/js/node/test/parallel/test-console-log-throw-primitive.js +test/js/node/test/parallel/test-console-methods.js +test/js/node/test/parallel/test-console-no-swallow-stack-overflow.js +test/js/node/test/parallel/test-console-not-call-toString.js +test/js/node/test/parallel/test-console-self-assign.js +test/js/node/test/parallel/test-console-sync-write-error.js +test/js/node/test/parallel/test-console-tty-colors.js +test/js/node/test/parallel/test-console-with-frozen-intrinsics.js +test/js/node/test/parallel/test-coverage-with-inspector-disabled.js +test/js/node/test/parallel/test-crypto-async-sign-verify.js +test/js/node/test/parallel/test-crypto-certificate.js +test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js +test/js/node/test/parallel/test-crypto-classes.js +test/js/node/test/parallel/test-crypto-dh-constructor.js +test/js/node/test/parallel/test-crypto-dh-curves.js +test/js/node/test/parallel/test-crypto-dh-errors.js +test/js/node/test/parallel/test-crypto-dh-generate-keys.js +test/js/node/test/parallel/test-crypto-dh-leak.js +test/js/node/test/parallel/test-crypto-dh-odd-key.js +test/js/node/test/parallel/test-crypto-dh-padding.js +test/js/node/test/parallel/test-crypto-dh-shared.js +test/js/node/test/parallel/test-crypto-dh.js +test/js/node/test/parallel/test-crypto-domain.js +test/js/node/test/parallel/test-crypto-ecdh-convert-key.js +test/js/node/test/parallel/test-crypto-encoding-validation-error.js +test/js/node/test/parallel/test-crypto-from-binary.js +test/js/node/test/parallel/test-crypto-gcm-explicit-short-tag.js +test/js/node/test/parallel/test-crypto-gcm-implicit-short-tag.js +test/js/node/test/parallel/test-crypto-getcipherinfo.js +test/js/node/test/parallel/test-crypto-hash-stream-pipe.js +test/js/node/test/parallel/test-crypto-hash.js +test/js/node/test/parallel/test-crypto-hkdf.js +test/js/node/test/parallel/test-crypto-hmac.js +test/js/node/test/parallel/test-crypto-key-objects.js +test/js/node/test/parallel/test-crypto-keygen-async-dsa-key-object.js +test/js/node/test/parallel/test-crypto-keygen-async-dsa.js +test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js +test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js +test/js/node/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js +test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js +test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key.js +test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js +test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js +test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js +test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js +test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js +test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve.js +test/js/node/test/parallel/test-crypto-keygen-async-rsa.js +test/js/node/test/parallel/test-crypto-keygen-bit-length.js +test/js/node/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js +test/js/node/test/parallel/test-crypto-keygen-eddsa.js +test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js +test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js +test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js +test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-ec.js +test/js/node/test/parallel/test-crypto-keygen-key-object-without-encoding.js +test/js/node/test/parallel/test-crypto-keygen-key-objects.js +test/js/node/test/parallel/test-crypto-keygen-missing-oid.js +test/js/node/test/parallel/test-crypto-keygen-non-standard-public-exponent.js +test/js/node/test/parallel/test-crypto-keygen-promisify.js +test/js/node/test/parallel/test-crypto-keygen-rfc8017-9-1.js +test/js/node/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js +test/js/node/test/parallel/test-crypto-keygen-rsa-pss.js +test/js/node/test/parallel/test-crypto-keygen-sync.js +test/js/node/test/parallel/test-crypto-lazy-transform-writable.js +test/js/node/test/parallel/test-crypto-no-algorithm.js +test/js/node/test/parallel/test-crypto-oaep-zero-length.js +test/js/node/test/parallel/test-crypto-oneshot-hash.js +test/js/node/test/parallel/test-crypto-op-during-process-exit.js +test/js/node/test/parallel/test-crypto-padding.js +test/js/node/test/parallel/test-crypto-pbkdf2.js +test/js/node/test/parallel/test-crypto-prime.js +test/js/node/test/parallel/test-crypto-private-decrypt-gh32240.js +test/js/node/test/parallel/test-crypto-psychic-signatures.js +test/js/node/test/parallel/test-crypto-publicDecrypt-fails-first-time.js +test/js/node/test/parallel/test-crypto-random.js +test/js/node/test/parallel/test-crypto-randomfillsync-regression.js +test/js/node/test/parallel/test-crypto-randomuuid.js +test/js/node/test/parallel/test-crypto-scrypt.js +test/js/node/test/parallel/test-crypto-secret-keygen.js +test/js/node/test/parallel/test-crypto-sign-verify.js +test/js/node/test/parallel/test-crypto-stream.js +test/js/node/test/parallel/test-crypto-subtle-zero-length.js +test/js/node/test/parallel/test-crypto-update-encoding.js +test/js/node/test/parallel/test-crypto-verify-failure.js +test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js +test/js/node/test/parallel/test-crypto-worker-thread.js +test/js/node/test/parallel/test-crypto-x509.js +test/js/node/test/parallel/test-datetime-change-notify.js +test/js/node/test/parallel/test-debug-process.js +test/js/node/test/parallel/test-debugger-backtrace.js +test/js/node/test/parallel/test-debugger-exec.js +test/js/node/test/parallel/test-debugger-invalid-json.mjs +test/js/node/test/parallel/test-debugger-low-level.js +test/js/node/test/parallel/test-debugger-preserve-breaks.js +test/js/node/test/parallel/test-debugger-repeat-last.js +test/js/node/test/parallel/test-debugger-restart-message.js +test/js/node/test/parallel/test-delayed-require.js +test/js/node/test/parallel/test-destroy-socket-in-lookup.js +test/js/node/test/parallel/test-dgram-abort-closed.js +test/js/node/test/parallel/test-dgram-address.js +test/js/node/test/parallel/test-dgram-async-dispose.mjs +test/js/node/test/parallel/test-dgram-bind-default-address.js +test/js/node/test/parallel/test-dgram-bind-error-repeat.js +test/js/node/test/parallel/test-dgram-bind-socket-close-before-lookup.js +test/js/node/test/parallel/test-dgram-bind.js +test/js/node/test/parallel/test-dgram-bytes-length.js +test/js/node/test/parallel/test-dgram-close-during-bind.js +test/js/node/test/parallel/test-dgram-close-in-listening.js +test/js/node/test/parallel/test-dgram-close-is-not-callback.js +test/js/node/test/parallel/test-dgram-close-signal.js +test/js/node/test/parallel/test-dgram-close.js +test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js +test/js/node/test/parallel/test-dgram-cluster-close-in-listening.js +test/js/node/test/parallel/test-dgram-connect-send-callback-buffer-length.js +test/js/node/test/parallel/test-dgram-connect-send-callback-buffer.js +test/js/node/test/parallel/test-dgram-connect-send-callback-multi-buffer.js +test/js/node/test/parallel/test-dgram-connect-send-default-host.js +test/js/node/test/parallel/test-dgram-connect-send-empty-array.js +test/js/node/test/parallel/test-dgram-connect-send-empty-buffer.js +test/js/node/test/parallel/test-dgram-connect-send-empty-packet.js +test/js/node/test/parallel/test-dgram-connect-send-multi-buffer-copy.js +test/js/node/test/parallel/test-dgram-connect-send-multi-string-array.js +test/js/node/test/parallel/test-dgram-connect.js +test/js/node/test/parallel/test-dgram-custom-lookup.js +test/js/node/test/parallel/test-dgram-deprecation-error.js +test/js/node/test/parallel/test-dgram-error-message-address.js +test/js/node/test/parallel/test-dgram-implicit-bind.js +test/js/node/test/parallel/test-dgram-ipv6only.js +test/js/node/test/parallel/test-dgram-listen-after-bind.js +test/js/node/test/parallel/test-dgram-membership.js +test/js/node/test/parallel/test-dgram-msgsize.js +test/js/node/test/parallel/test-dgram-multicast-loopback.js +test/js/node/test/parallel/test-dgram-multicast-set-interface.js +test/js/node/test/parallel/test-dgram-multicast-setTTL.js +test/js/node/test/parallel/test-dgram-oob-buffer.js +test/js/node/test/parallel/test-dgram-recv-error.js +test/js/node/test/parallel/test-dgram-ref.js +test/js/node/test/parallel/test-dgram-reuseport.js +test/js/node/test/parallel/test-dgram-send-address-types.js +test/js/node/test/parallel/test-dgram-send-bad-arguments.js +test/js/node/test/parallel/test-dgram-send-callback-buffer-empty-address.js +test/js/node/test/parallel/test-dgram-send-callback-buffer-length-empty-address.js +test/js/node/test/parallel/test-dgram-send-callback-buffer-length.js +test/js/node/test/parallel/test-dgram-send-callback-buffer.js +test/js/node/test/parallel/test-dgram-send-callback-multi-buffer-empty-address.js +test/js/node/test/parallel/test-dgram-send-callback-multi-buffer.js +test/js/node/test/parallel/test-dgram-send-callback-recursive.js +test/js/node/test/parallel/test-dgram-send-cb-quelches-error.js +test/js/node/test/parallel/test-dgram-send-default-host.js +test/js/node/test/parallel/test-dgram-send-empty-array.js +test/js/node/test/parallel/test-dgram-send-empty-buffer.js +test/js/node/test/parallel/test-dgram-send-empty-packet.js +test/js/node/test/parallel/test-dgram-send-error.js +test/js/node/test/parallel/test-dgram-send-invalid-msg-type.js +test/js/node/test/parallel/test-dgram-send-multi-buffer-copy.js +test/js/node/test/parallel/test-dgram-send-multi-string-array.js +test/js/node/test/parallel/test-dgram-sendto.js +test/js/node/test/parallel/test-dgram-setBroadcast.js +test/js/node/test/parallel/test-dgram-setTTL.js +test/js/node/test/parallel/test-dgram-udp4.js +test/js/node/test/parallel/test-dgram-udp6-link-local-address.js +test/js/node/test/parallel/test-dgram-udp6-send-default-host.js +test/js/node/test/parallel/test-dgram-unref-in-cluster.js +test/js/node/test/parallel/test-dgram-unref.js +test/js/node/test/parallel/test-diagnostics-channel-bind-store.js +test/js/node/test/parallel/test-diagnostics-channel-has-subscribers.js +test/js/node/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js +test/js/node/test/parallel/test-diagnostics-channel-pub-sub.js +test/js/node/test/parallel/test-diagnostics-channel-safe-subscriber-errors.js +test/js/node/test/parallel/test-diagnostics-channel-symbol-named.js +test/js/node/test/parallel/test-diagnostics-channel-sync-unsubscribe.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-error.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback-run-stores.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-callback.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise-error.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise-run-stores.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-promise.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-error.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync-run-stores.js +test/js/node/test/parallel/test-diagnostics-channel-tracing-channel-sync.js +test/js/node/test/parallel/test-diagnostics-channel-udp.js +test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js +test/js/node/test/parallel/test-dns-channel-cancel-promise.js +test/js/node/test/parallel/test-dns-channel-cancel.js +test/js/node/test/parallel/test-dns-channel-timeout.js +test/js/node/test/parallel/test-dns-default-order-ipv4.js +test/js/node/test/parallel/test-dns-default-order-ipv6.js +test/js/node/test/parallel/test-dns-default-order-verbatim.js +test/js/node/test/parallel/test-dns-get-server.js +test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js +test/js/node/test/parallel/test-dns-lookup.js +test/js/node/test/parallel/test-dns-lookupService-promises.js +test/js/node/test/parallel/test-dns-lookupService.js +test/js/node/test/parallel/test-dns-multi-channel.js +test/js/node/test/parallel/test-dns-promises-exists.js +test/js/node/test/parallel/test-dns-resolve-promises.js +test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js +test/js/node/test/parallel/test-dns-resolveany.js +test/js/node/test/parallel/test-dns-resolvens-typeerror.js +test/js/node/test/parallel/test-dns-set-default-order.js +test/js/node/test/parallel/test-dns-setlocaladdress.js +test/js/node/test/parallel/test-dns-setserver-when-querying.js +test/js/node/test/parallel/test-dns-setservers-type-check.js +test/js/node/test/parallel/test-dns.js +test/js/node/test/parallel/test-domain-crypto.js +test/js/node/test/parallel/test-domain-ee-error-listener.js +test/js/node/test/parallel/test-domain-nested-throw.js +test/js/node/test/parallel/test-domain-vm-promise-isolation.js +test/js/node/test/parallel/test-domexception-cause.js +test/js/node/test/parallel/test-dsa-fips-invalid-key.js +test/js/node/test/parallel/test-emit-after-uncaught-exception.js +test/js/node/test/parallel/test-error-prepare-stack-trace.js +test/js/node/test/parallel/test-eslint-alphabetize-errors.js +test/js/node/test/parallel/test-eslint-alphabetize-primordials.js +test/js/node/test/parallel/test-eslint-async-iife-no-unused-result.js +test/js/node/test/parallel/test-eslint-avoid-prototype-pollution.js +test/js/node/test/parallel/test-eslint-crypto-check.js +test/js/node/test/parallel/test-eslint-documented-deprecation-codes.js +test/js/node/test/parallel/test-eslint-documented-errors.js +test/js/node/test/parallel/test-eslint-duplicate-requires.js +test/js/node/test/parallel/test-eslint-eslint-check.js +test/js/node/test/parallel/test-eslint-inspector-check.js +test/js/node/test/parallel/test-eslint-lowercase-name-for-primitive.js +test/js/node/test/parallel/test-eslint-no-array-destructuring.js +test/js/node/test/parallel/test-eslint-no-unescaped-regexp-dot.js +test/js/node/test/parallel/test-eslint-non-ascii-character.js +test/js/node/test/parallel/test-eslint-prefer-assert-iferror.js +test/js/node/test/parallel/test-eslint-prefer-assert-methods.js +test/js/node/test/parallel/test-eslint-prefer-common-mustnotcall.js +test/js/node/test/parallel/test-eslint-prefer-common-mustsucceed.js +test/js/node/test/parallel/test-eslint-prefer-optional-chaining.js +test/js/node/test/parallel/test-eslint-prefer-primordials.js +test/js/node/test/parallel/test-eslint-prefer-proto.js +test/js/node/test/parallel/test-eslint-prefer-util-format-errors.js +test/js/node/test/parallel/test-eslint-require-common-first.js +test/js/node/test/parallel/test-eslint-required-modules.js +test/js/node/test/parallel/test-eval-strict-referenceerror.js +test/js/node/test/parallel/test-eval.js +test/js/node/test/parallel/test-event-capture-rejections.js +test/js/node/test/parallel/test-event-emitter-add-listeners.js +test/js/node/test/parallel/test-event-emitter-check-listener-leaks.js +test/js/node/test/parallel/test-event-emitter-emit-context.js +test/js/node/test/parallel/test-event-emitter-error-monitor.js +test/js/node/test/parallel/test-event-emitter-errors.js +test/js/node/test/parallel/test-event-emitter-get-max-listeners.js +test/js/node/test/parallel/test-event-emitter-invalid-listener.js +test/js/node/test/parallel/test-event-emitter-listener-count.js +test/js/node/test/parallel/test-event-emitter-listeners-side-effects.js +test/js/node/test/parallel/test-event-emitter-listeners.js +test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-null.js +test/js/node/test/parallel/test-event-emitter-max-listeners-warning-for-symbol.js +test/js/node/test/parallel/test-event-emitter-max-listeners-warning.js +test/js/node/test/parallel/test-event-emitter-max-listeners.js +test/js/node/test/parallel/test-event-emitter-method-names.js +test/js/node/test/parallel/test-event-emitter-modify-in-emit.js +test/js/node/test/parallel/test-event-emitter-no-error-provided-to-error-event.js +test/js/node/test/parallel/test-event-emitter-num-args.js +test/js/node/test/parallel/test-event-emitter-once.js +test/js/node/test/parallel/test-event-emitter-prepend.js +test/js/node/test/parallel/test-event-emitter-remove-all-listeners.js +test/js/node/test/parallel/test-event-emitter-remove-listeners.js +test/js/node/test/parallel/test-event-emitter-set-max-listeners-side-effects.js +test/js/node/test/parallel/test-event-emitter-special-event-names.js +test/js/node/test/parallel/test-event-emitter-subclass.js +test/js/node/test/parallel/test-event-emitter-symbols.js +test/js/node/test/parallel/test-event-target.js +test/js/node/test/parallel/test-events-add-abort-listener.mjs +test/js/node/test/parallel/test-events-customevent.js +test/js/node/test/parallel/test-events-getmaxlisteners.js +test/js/node/test/parallel/test-events-list.js +test/js/node/test/parallel/test-events-listener-count-with-listener.js +test/js/node/test/parallel/test-events-on-async-iterator.js +test/js/node/test/parallel/test-events-once.js +test/js/node/test/parallel/test-events-static-geteventlisteners.js +test/js/node/test/parallel/test-events-uncaught-exception-stack.js +test/js/node/test/parallel/test-eventsource-disabled.js +test/js/node/test/parallel/test-eventtarget-once-twice.js +test/js/node/test/parallel/test-eventtarget.js +test/js/node/test/parallel/test-exception-handler.js +test/js/node/test/parallel/test-exception-handler2.js +test/js/node/test/parallel/test-fetch.mjs +test/js/node/test/parallel/test-file-read-noexist.js +test/js/node/test/parallel/test-file-validate-mode-flag.js +test/js/node/test/parallel/test-file-write-stream.js +test/js/node/test/parallel/test-file-write-stream2.js +test/js/node/test/parallel/test-file-write-stream3.js +test/js/node/test/parallel/test-file-write-stream4.js +test/js/node/test/parallel/test-filehandle-close.js +test/js/node/test/parallel/test-finalization-registry-shutdown.js +test/js/node/test/parallel/test-fs-access.js +test/js/node/test/parallel/test-fs-append-file-flush.js +test/js/node/test/parallel/test-fs-append-file-sync.js +test/js/node/test/parallel/test-fs-append-file.js +test/js/node/test/parallel/test-fs-assert-encoding-error.js +test/js/node/test/parallel/test-fs-buffer.js +test/js/node/test/parallel/test-fs-buffertype-writesync.js +test/js/node/test/parallel/test-fs-chmod-mask.js +test/js/node/test/parallel/test-fs-chmod.js +test/js/node/test/parallel/test-fs-chown-type-check.js +test/js/node/test/parallel/test-fs-close-errors.js +test/js/node/test/parallel/test-fs-close.js +test/js/node/test/parallel/test-fs-constants.js +test/js/node/test/parallel/test-fs-copyfile-respect-permissions.js +test/js/node/test/parallel/test-fs-copyfile.js +test/js/node/test/parallel/test-fs-empty-readStream.js +test/js/node/test/parallel/test-fs-exists.js +test/js/node/test/parallel/test-fs-existssync-false.js +test/js/node/test/parallel/test-fs-fchmod.js +test/js/node/test/parallel/test-fs-fchown.js +test/js/node/test/parallel/test-fs-filehandle-use-after-close.js +test/js/node/test/parallel/test-fs-fsync.js +test/js/node/test/parallel/test-fs-lchmod.js +test/js/node/test/parallel/test-fs-lchown.js +test/js/node/test/parallel/test-fs-link.js +test/js/node/test/parallel/test-fs-long-path.js +test/js/node/test/parallel/test-fs-make-callback.js +test/js/node/test/parallel/test-fs-makeStatsCallback.js +test/js/node/test/parallel/test-fs-mkdir-mode-mask.js +test/js/node/test/parallel/test-fs-mkdir-recursive-eaccess.js +test/js/node/test/parallel/test-fs-mkdir-rmdir.js +test/js/node/test/parallel/test-fs-mkdir.js +test/js/node/test/parallel/test-fs-mkdtemp-prefix-check.js +test/js/node/test/parallel/test-fs-mkdtemp.js +test/js/node/test/parallel/test-fs-non-number-arguments-throw.js +test/js/node/test/parallel/test-fs-null-bytes.js +test/js/node/test/parallel/test-fs-open-mode-mask.js +test/js/node/test/parallel/test-fs-open-no-close.js +test/js/node/test/parallel/test-fs-open-numeric-flags.js +test/js/node/test/parallel/test-fs-open.js +test/js/node/test/parallel/test-fs-options-immutable.js +test/js/node/test/parallel/test-fs-promises-exists.js +test/js/node/test/parallel/test-fs-promises-file-handle-append-file.js +test/js/node/test/parallel/test-fs-promises-file-handle-chmod.js +test/js/node/test/parallel/test-fs-promises-file-handle-dispose.js +test/js/node/test/parallel/test-fs-promises-file-handle-read-worker.js +test/js/node/test/parallel/test-fs-promises-file-handle-read.js +test/js/node/test/parallel/test-fs-promises-file-handle-readFile.js +test/js/node/test/parallel/test-fs-promises-file-handle-stat.js +test/js/node/test/parallel/test-fs-promises-file-handle-stream.js +test/js/node/test/parallel/test-fs-promises-file-handle-sync.js +test/js/node/test/parallel/test-fs-promises-file-handle-truncate.js +test/js/node/test/parallel/test-fs-promises-file-handle-write.js +test/js/node/test/parallel/test-fs-promises-file-handle-writeFile.js +test/js/node/test/parallel/test-fs-promises-readfile-empty.js +test/js/node/test/parallel/test-fs-promises-readfile-with-fd.js +test/js/node/test/parallel/test-fs-promises-readfile.js +test/js/node/test/parallel/test-fs-promises-write-optional-params.js +test/js/node/test/parallel/test-fs-promises-writefile-typedarray.js +test/js/node/test/parallel/test-fs-promises-writefile-with-fd.js +test/js/node/test/parallel/test-fs-promises-writefile.js +test/js/node/test/parallel/test-fs-promisified.js +test/js/node/test/parallel/test-fs-read-empty-buffer.js +test/js/node/test/parallel/test-fs-read-file-assert-encoding.js +test/js/node/test/parallel/test-fs-read-file-sync-hostname.js +test/js/node/test/parallel/test-fs-read-file-sync.js +test/js/node/test/parallel/test-fs-read-offset-null.js +test/js/node/test/parallel/test-fs-read-optional-params.js +test/js/node/test/parallel/test-fs-read-position-validation.mjs +test/js/node/test/parallel/test-fs-read-promises-optional-params.js +test/js/node/test/parallel/test-fs-read-promises-position-validation.mjs +test/js/node/test/parallel/test-fs-read-stream-autoClose.js +test/js/node/test/parallel/test-fs-read-stream-concurrent-reads.js +test/js/node/test/parallel/test-fs-read-stream-double-close.js +test/js/node/test/parallel/test-fs-read-stream-encoding.js +test/js/node/test/parallel/test-fs-read-stream-err.js +test/js/node/test/parallel/test-fs-read-stream-fd-leak.js +test/js/node/test/parallel/test-fs-read-stream-fd.js +test/js/node/test/parallel/test-fs-read-stream-file-handle.js +test/js/node/test/parallel/test-fs-read-stream-inherit.js +test/js/node/test/parallel/test-fs-read-stream-patch-open.js +test/js/node/test/parallel/test-fs-read-stream-resume.js +test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js +test/js/node/test/parallel/test-fs-read-type.js +test/js/node/test/parallel/test-fs-read-zero-length.js +test/js/node/test/parallel/test-fs-read.js +test/js/node/test/parallel/test-fs-readdir-buffer.js +test/js/node/test/parallel/test-fs-readdir-pipe.js +test/js/node/test/parallel/test-fs-readdir-recursive.js +test/js/node/test/parallel/test-fs-readdir-stack-overflow.js +test/js/node/test/parallel/test-fs-readdir-types-symlinks.js +test/js/node/test/parallel/test-fs-readdir-types.js +test/js/node/test/parallel/test-fs-readdir-ucs2.js +test/js/node/test/parallel/test-fs-readdir.js +test/js/node/test/parallel/test-fs-readfile-empty.js +test/js/node/test/parallel/test-fs-readfile-eof.js +test/js/node/test/parallel/test-fs-readfile-error.js +test/js/node/test/parallel/test-fs-readfile-fd.js +test/js/node/test/parallel/test-fs-readfile-flags.js +test/js/node/test/parallel/test-fs-readfile-pipe-large.js +test/js/node/test/parallel/test-fs-readfile-pipe.js +test/js/node/test/parallel/test-fs-readfile-unlink.js +test/js/node/test/parallel/test-fs-readfile-zero-byte-liar.js +test/js/node/test/parallel/test-fs-readfile.js +test/js/node/test/parallel/test-fs-readfilesync-enoent.js +test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js +test/js/node/test/parallel/test-fs-readlink-type-check.js +test/js/node/test/parallel/test-fs-readSync-optional-params.js +test/js/node/test/parallel/test-fs-readSync-position-validation.mjs +test/js/node/test/parallel/test-fs-readv-promises.js +test/js/node/test/parallel/test-fs-readv-promisify.js +test/js/node/test/parallel/test-fs-readv-sync.js +test/js/node/test/parallel/test-fs-readv.js +test/js/node/test/parallel/test-fs-ready-event-stream.js +test/js/node/test/parallel/test-fs-realpath-buffer-encoding.js +test/js/node/test/parallel/test-fs-realpath-native.js +test/js/node/test/parallel/test-fs-realpath-on-substed-drive.js +test/js/node/test/parallel/test-fs-realpath-pipe.js +test/js/node/test/parallel/test-fs-realpath.js +test/js/node/test/parallel/test-fs-rename-type-check.js +test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js +test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js +test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js +test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js +test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js +test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js +test/js/node/test/parallel/test-fs-rmdir-recursive.js +test/js/node/test/parallel/test-fs-rmdir-type-check.js +test/js/node/test/parallel/test-fs-sir-writes-alot.js +test/js/node/test/parallel/test-fs-stat-bigint.js +test/js/node/test/parallel/test-fs-stat-date.mjs +test/js/node/test/parallel/test-fs-stat.js +test/js/node/test/parallel/test-fs-statfs.js +test/js/node/test/parallel/test-fs-stream-construct-compat-error-read.js +test/js/node/test/parallel/test-fs-stream-construct-compat-error-write.js +test/js/node/test/parallel/test-fs-stream-construct-compat-graceful-fs.js +test/js/node/test/parallel/test-fs-stream-construct-compat-old-node.js +test/js/node/test/parallel/test-fs-stream-destroy-emit-error.js +test/js/node/test/parallel/test-fs-stream-double-close.js +test/js/node/test/parallel/test-fs-stream-fs-options.js +test/js/node/test/parallel/test-fs-stream-options.js +test/js/node/test/parallel/test-fs-symlink-buffer-path.js +test/js/node/test/parallel/test-fs-symlink-dir-junction-relative.js +test/js/node/test/parallel/test-fs-symlink-dir-junction.js +test/js/node/test/parallel/test-fs-symlink-dir.js +test/js/node/test/parallel/test-fs-symlink-longpath.js +test/js/node/test/parallel/test-fs-symlink.js +test/js/node/test/parallel/test-fs-syncwritestream.js +test/js/node/test/parallel/test-fs-timestamp-parsing-error.js +test/js/node/test/parallel/test-fs-truncate-clear-file-zero.js +test/js/node/test/parallel/test-fs-truncate-fd.js +test/js/node/test/parallel/test-fs-truncate-sync.js +test/js/node/test/parallel/test-fs-truncate.js +test/js/node/test/parallel/test-fs-unlink-type-check.js +test/js/node/test/parallel/test-fs-utimes-y2K38.js +test/js/node/test/parallel/test-fs-watch-abort-signal.js +test/js/node/test/parallel/test-fs-watch-close-when-destroyed.js +test/js/node/test/parallel/test-fs-watch-encoding.js +test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js +test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js +test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js +test/js/node/test/parallel/test-fs-watch-recursive-add-file.js +test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js +test/js/node/test/parallel/test-fs-watch-recursive-assert-leaks.js +test/js/node/test/parallel/test-fs-watch-recursive-delete.js +test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js +test/js/node/test/parallel/test-fs-watch-recursive-validate.js +test/js/node/test/parallel/test-fs-watch-ref-unref.js +test/js/node/test/parallel/test-fs-watch-stop-sync.js +test/js/node/test/parallel/test-fs-watchfile-ref-unref.js +test/js/node/test/parallel/test-fs-whatwg-url.js +test/js/node/test/parallel/test-fs-write-buffer-large.js +test/js/node/test/parallel/test-fs-write-buffer.js +test/js/node/test/parallel/test-fs-write-file-buffer.js +test/js/node/test/parallel/test-fs-write-file-flush.js +test/js/node/test/parallel/test-fs-write-file-invalid-path.js +test/js/node/test/parallel/test-fs-write-file-sync.js +test/js/node/test/parallel/test-fs-write-file-typedarrays.js +test/js/node/test/parallel/test-fs-write-file.js +test/js/node/test/parallel/test-fs-write-negativeoffset.js +test/js/node/test/parallel/test-fs-write-no-fd.js +test/js/node/test/parallel/test-fs-write-optional-params.js +test/js/node/test/parallel/test-fs-write-reuse-callback.js +test/js/node/test/parallel/test-fs-write-sigxfsz.js +test/js/node/test/parallel/test-fs-write-stream-autoclose-option.js +test/js/node/test/parallel/test-fs-write-stream-change-open.js +test/js/node/test/parallel/test-fs-write-stream-close-without-callback.js +test/js/node/test/parallel/test-fs-write-stream-double-close.js +test/js/node/test/parallel/test-fs-write-stream-encoding.js +test/js/node/test/parallel/test-fs-write-stream-end.js +test/js/node/test/parallel/test-fs-write-stream-err.js +test/js/node/test/parallel/test-fs-write-stream-file-handle-2.js +test/js/node/test/parallel/test-fs-write-stream-file-handle.js +test/js/node/test/parallel/test-fs-write-stream-flush.js +test/js/node/test/parallel/test-fs-write-stream-fs.js +test/js/node/test/parallel/test-fs-write-stream-patch-open.js +test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js +test/js/node/test/parallel/test-fs-write-stream.js +test/js/node/test/parallel/test-fs-write-sync.js +test/js/node/test/parallel/test-fs-writefile-with-fd.js +test/js/node/test/parallel/test-fs-writestream-open-write.js +test/js/node/test/parallel/test-fs-writev-promises.js +test/js/node/test/parallel/test-fs-writev-sync.js +test/js/node/test/parallel/test-fs-writev.js +test/js/node/test/parallel/test-global-domexception.js +test/js/node/test/parallel/test-global-encoder.js +test/js/node/test/parallel/test-global-webcrypto.js +test/js/node/test/parallel/test-handle-wrap-close-abort.js +test/js/node/test/parallel/test-http-1.0-keep-alive.js +test/js/node/test/parallel/test-http-abort-before-end.js +test/js/node/test/parallel/test-http-abort-stream-end.js +test/js/node/test/parallel/test-http-aborted.js +test/js/node/test/parallel/test-http-agent-false.js +test/js/node/test/parallel/test-http-agent-getname.js +test/js/node/test/parallel/test-http-agent-keepalive-delay.js +test/js/node/test/parallel/test-http-agent-no-protocol.js +test/js/node/test/parallel/test-http-agent-null.js +test/js/node/test/parallel/test-http-agent-remove.js +test/js/node/test/parallel/test-http-agent-uninitialized-with-handle.js +test/js/node/test/parallel/test-http-agent-uninitialized.js +test/js/node/test/parallel/test-http-allow-content-length-304.js +test/js/node/test/parallel/test-http-allow-req-after-204-res.js +test/js/node/test/parallel/test-http-autoselectfamily.js +test/js/node/test/parallel/test-http-bind-twice.js +test/js/node/test/parallel/test-http-blank-header.js +test/js/node/test/parallel/test-http-buffer-sanity.js +test/js/node/test/parallel/test-http-byteswritten.js +test/js/node/test/parallel/test-http-catch-uncaughtexception.js +test/js/node/test/parallel/test-http-chunk-problem.js +test/js/node/test/parallel/test-http-chunked-smuggling.js +test/js/node/test/parallel/test-http-chunked.js +test/js/node/test/parallel/test-http-client-abort-event.js +test/js/node/test/parallel/test-http-client-abort-response-event.js +test/js/node/test/parallel/test-http-client-abort.js +test/js/node/test/parallel/test-http-client-abort2.js +test/js/node/test/parallel/test-http-client-agent-abort-close-event.js +test/js/node/test/parallel/test-http-client-check-http-token.js +test/js/node/test/parallel/test-http-client-close-with-default-agent.js +test/js/node/test/parallel/test-http-client-defaults.js +test/js/node/test/parallel/test-http-client-encoding.js +test/js/node/test/parallel/test-http-client-get-url.js +test/js/node/test/parallel/test-http-client-headers-host-array.js +test/js/node/test/parallel/test-http-client-input-function.js +test/js/node/test/parallel/test-http-client-insecure-http-parser-error.js +test/js/node/test/parallel/test-http-client-invalid-path.js +test/js/node/test/parallel/test-http-client-keep-alive-hint.js +test/js/node/test/parallel/test-http-client-keep-alive-release-before-finish.js +test/js/node/test/parallel/test-http-client-pipe-end.js +test/js/node/test/parallel/test-http-client-race-2.js +test/js/node/test/parallel/test-http-client-race.js +test/js/node/test/parallel/test-http-client-read-in-error.js +test/js/node/test/parallel/test-http-client-reject-unexpected-agent.js +test/js/node/test/parallel/test-http-client-req-error-dont-double-fire.js +test/js/node/test/parallel/test-http-client-request-options.js +test/js/node/test/parallel/test-http-client-res-destroyed.js +test/js/node/test/parallel/test-http-client-timeout-agent.js +test/js/node/test/parallel/test-http-client-timeout-connect-listener.js +test/js/node/test/parallel/test-http-client-timeout-event.js +test/js/node/test/parallel/test-http-client-timeout-option.js +test/js/node/test/parallel/test-http-client-timeout.js +test/js/node/test/parallel/test-http-client-unescaped-path.js +test/js/node/test/parallel/test-http-client-upload-buf.js +test/js/node/test/parallel/test-http-client-upload.js +test/js/node/test/parallel/test-http-client-with-create-connection.js +test/js/node/test/parallel/test-http-common.js +test/js/node/test/parallel/test-http-conn-reset.js +test/js/node/test/parallel/test-http-content-length-mismatch.js +test/js/node/test/parallel/test-http-contentLength0.js +test/js/node/test/parallel/test-http-date-header.js +test/js/node/test/parallel/test-http-decoded-auth.js +test/js/node/test/parallel/test-http-default-encoding.js +test/js/node/test/parallel/test-http-dns-error.js +test/js/node/test/parallel/test-http-double-content-length.js +test/js/node/test/parallel/test-http-dummy-characters-smuggling.js +test/js/node/test/parallel/test-http-early-hints-invalid-argument.js +test/js/node/test/parallel/test-http-end-throw-socket-handling.js +test/js/node/test/parallel/test-http-eof-on-connect.js +test/js/node/test/parallel/test-http-exceptions.js +test/js/node/test/parallel/test-http-expect-continue.js +test/js/node/test/parallel/test-http-expect-handling.js +test/js/node/test/parallel/test-http-extra-response.js +test/js/node/test/parallel/test-http-flush-headers.js +test/js/node/test/parallel/test-http-flush-response-headers.js +test/js/node/test/parallel/test-http-full-response.js +test/js/node/test/parallel/test-http-get-pipeline-problem.js +test/js/node/test/parallel/test-http-head-request.js +test/js/node/test/parallel/test-http-head-response-has-no-body-end-implicit-headers.js +test/js/node/test/parallel/test-http-head-response-has-no-body-end.js +test/js/node/test/parallel/test-http-head-response-has-no-body.js +test/js/node/test/parallel/test-http-head-throw-on-response-body-write.js +test/js/node/test/parallel/test-http-header-obstext.js +test/js/node/test/parallel/test-http-header-overflow.js +test/js/node/test/parallel/test-http-header-owstext.js +test/js/node/test/parallel/test-http-header-read.js +test/js/node/test/parallel/test-http-header-validators.js +test/js/node/test/parallel/test-http-hex-write.js +test/js/node/test/parallel/test-http-highwatermark.js +test/js/node/test/parallel/test-http-host-headers.js +test/js/node/test/parallel/test-http-hostname-typechecking.js +test/js/node/test/parallel/test-http-import-websocket.js +test/js/node/test/parallel/test-http-incoming-message-destroy.js +test/js/node/test/parallel/test-http-invalid-path-chars.js +test/js/node/test/parallel/test-http-invalid-te.js +test/js/node/test/parallel/test-http-invalid-urls.js +test/js/node/test/parallel/test-http-invalidheaderfield.js +test/js/node/test/parallel/test-http-invalidheaderfield2.js +test/js/node/test/parallel/test-http-keep-alive-drop-requests.js +test/js/node/test/parallel/test-http-keep-alive-pipeline-max-requests.js +test/js/node/test/parallel/test-http-keep-alive-timeout-custom.js +test/js/node/test/parallel/test-http-keep-alive-timeout-race-condition.js +test/js/node/test/parallel/test-http-listening.js +test/js/node/test/parallel/test-http-malformed-request.js +test/js/node/test/parallel/test-http-many-ended-pipelines.js +test/js/node/test/parallel/test-http-max-header-size.js +test/js/node/test/parallel/test-http-methods.js +test/js/node/test/parallel/test-http-missing-header-separator-cr.js +test/js/node/test/parallel/test-http-missing-header-separator-lf.js +test/js/node/test/parallel/test-http-no-content-length.js +test/js/node/test/parallel/test-http-outgoing-buffer.js +test/js/node/test/parallel/test-http-outgoing-destroy.js +test/js/node/test/parallel/test-http-outgoing-end-multiple.js +test/js/node/test/parallel/test-http-outgoing-end-types.js +test/js/node/test/parallel/test-http-outgoing-finish-writable.js +test/js/node/test/parallel/test-http-outgoing-finish.js +test/js/node/test/parallel/test-http-outgoing-finished.js +test/js/node/test/parallel/test-http-outgoing-first-chunk-singlebyte-encoding.js +test/js/node/test/parallel/test-http-outgoing-internal-headernames-getter.js +test/js/node/test/parallel/test-http-outgoing-internal-headernames-setter.js +test/js/node/test/parallel/test-http-outgoing-internal-headers.js +test/js/node/test/parallel/test-http-outgoing-message-write-callback.js +test/js/node/test/parallel/test-http-outgoing-proto.js +test/js/node/test/parallel/test-http-outgoing-settimeout.js +test/js/node/test/parallel/test-http-outgoing-writableFinished.js +test/js/node/test/parallel/test-http-outgoing-write-types.js +test/js/node/test/parallel/test-http-parser-bad-ref.js +test/js/node/test/parallel/test-http-parser-lazy-loaded.js +test/js/node/test/parallel/test-http-parser.js +test/js/node/test/parallel/test-http-pause-no-dump.js +test/js/node/test/parallel/test-http-pause-resume-one-end.js +test/js/node/test/parallel/test-http-pause.js +test/js/node/test/parallel/test-http-pipe-fs.js +test/js/node/test/parallel/test-http-pipeline-requests-connection-leak.js +test/js/node/test/parallel/test-http-pipeline-socket-parser-typeerror.js +test/js/node/test/parallel/test-http-proxy.js +test/js/node/test/parallel/test-http-readable-data-event.js +test/js/node/test/parallel/test-http-request-agent.js +test/js/node/test/parallel/test-http-request-arguments.js +test/js/node/test/parallel/test-http-request-end-twice.js +test/js/node/test/parallel/test-http-request-end.js +test/js/node/test/parallel/test-http-request-invalid-method-error.js +test/js/node/test/parallel/test-http-request-large-payload.js +test/js/node/test/parallel/test-http-request-method-delete-payload.js +test/js/node/test/parallel/test-http-request-methods.js +test/js/node/test/parallel/test-http-request-smuggling-content-length.js +test/js/node/test/parallel/test-http-res-write-after-end.js +test/js/node/test/parallel/test-http-res-write-end-dont-take-array.js +test/js/node/test/parallel/test-http-response-add-header-after-sent.js +test/js/node/test/parallel/test-http-response-close.js +test/js/node/test/parallel/test-http-response-cork.js +test/js/node/test/parallel/test-http-response-multi-content-length.js +test/js/node/test/parallel/test-http-response-readable.js +test/js/node/test/parallel/test-http-response-remove-header-after-sent.js +test/js/node/test/parallel/test-http-response-setheaders.js +test/js/node/test/parallel/test-http-response-splitting.js +test/js/node/test/parallel/test-http-response-status-message.js +test/js/node/test/parallel/test-http-response-statuscode.js +test/js/node/test/parallel/test-http-response-writehead-returns-this.js +test/js/node/test/parallel/test-http-server-async-dispose.js +test/js/node/test/parallel/test-http-server-capture-rejections.js +test/js/node/test/parallel/test-http-server-close-all.js +test/js/node/test/parallel/test-http-server-close-destroy-timeout.js +test/js/node/test/parallel/test-http-server-close-idle-wait-response.js +test/js/node/test/parallel/test-http-server-de-chunked-trailer.js +test/js/node/test/parallel/test-http-server-delete-parser.js +test/js/node/test/parallel/test-http-server-destroy-socket-on-client-error.js +test/js/node/test/parallel/test-http-server-keep-alive-defaults.js +test/js/node/test/parallel/test-http-server-keep-alive-max-requests-null.js +test/js/node/test/parallel/test-http-server-method.query.js +test/js/node/test/parallel/test-http-server-multiheaders.js +test/js/node/test/parallel/test-http-server-non-utf8-header.js +test/js/node/test/parallel/test-http-server-options-incoming-message.js +test/js/node/test/parallel/test-http-server-options-server-response.js +test/js/node/test/parallel/test-http-server-reject-chunked-with-content-length.js +test/js/node/test/parallel/test-http-server-stale-close.js +test/js/node/test/parallel/test-http-server-timeouts-validation.js +test/js/node/test/parallel/test-http-server-write-after-end.js +test/js/node/test/parallel/test-http-server-write-end-after-end.js +test/js/node/test/parallel/test-http-set-cookies.js +test/js/node/test/parallel/test-http-set-header-chain.js +test/js/node/test/parallel/test-http-set-max-idle-http-parser.js +test/js/node/test/parallel/test-http-socket-error-listeners.js +test/js/node/test/parallel/test-http-status-code.js +test/js/node/test/parallel/test-http-status-message.js +test/js/node/test/parallel/test-http-status-reason-invalid-chars.js +test/js/node/test/parallel/test-http-timeout-client-warning.js +test/js/node/test/parallel/test-http-timeout-overflow.js +test/js/node/test/parallel/test-http-timeout.js +test/js/node/test/parallel/test-http-uncaught-from-request-callback.js +test/js/node/test/parallel/test-http-upgrade-reconsume-stream.js +test/js/node/test/parallel/test-http-url.parse-auth-with-header-in-request.js +test/js/node/test/parallel/test-http-url.parse-auth.js +test/js/node/test/parallel/test-http-url.parse-basic.js +test/js/node/test/parallel/test-http-url.parse-only-support-http-https-protocol.js +test/js/node/test/parallel/test-http-url.parse-path.js +test/js/node/test/parallel/test-http-url.parse-post.js +test/js/node/test/parallel/test-http-url.parse-search.js +test/js/node/test/parallel/test-http-wget.js +test/js/node/test/parallel/test-http-write-callbacks.js +test/js/node/test/parallel/test-http-write-empty-string.js +test/js/node/test/parallel/test-http-write-head-2.js +test/js/node/test/parallel/test-http-write-head.js +test/js/node/test/parallel/test-http-zero-length-write.js +test/js/node/test/parallel/test-http-zerolengthbuffer.js +test/js/node/test/parallel/test-http2-altsvc.js +test/js/node/test/parallel/test-http2-cancel-while-client-reading.js +test/js/node/test/parallel/test-http2-clean-output.js +test/js/node/test/parallel/test-http2-client-port-80.js +test/js/node/test/parallel/test-http2-client-priority-before-connect.js +test/js/node/test/parallel/test-http2-client-request-listeners-warning.js +test/js/node/test/parallel/test-http2-client-request-options-errors.js +test/js/node/test/parallel/test-http2-client-rststream-before-connect.js +test/js/node/test/parallel/test-http2-client-setLocalWindowSize.js +test/js/node/test/parallel/test-http2-client-setNextStreamID-errors.js +test/js/node/test/parallel/test-http2-client-shutdown-before-connect.js +test/js/node/test/parallel/test-http2-client-stream-destroy-before-connect.js +test/js/node/test/parallel/test-http2-client-upload-reject.js +test/js/node/test/parallel/test-http2-client-upload.js +test/js/node/test/parallel/test-http2-client-write-before-connect.js +test/js/node/test/parallel/test-http2-client-write-empty-string.js +test/js/node/test/parallel/test-http2-close-while-writing.js +test/js/node/test/parallel/test-http2-compat-aborted.js +test/js/node/test/parallel/test-http2-compat-client-upload-reject.js +test/js/node/test/parallel/test-http2-compat-errors.js +test/js/node/test/parallel/test-http2-compat-expect-continue-check.js +test/js/node/test/parallel/test-http2-compat-expect-continue.js +test/js/node/test/parallel/test-http2-compat-expect-handling.js +test/js/node/test/parallel/test-http2-compat-method-connect.js +test/js/node/test/parallel/test-http2-compat-serverrequest-end.js +test/js/node/test/parallel/test-http2-compat-serverrequest-headers.js +test/js/node/test/parallel/test-http2-compat-serverrequest-host.js +test/js/node/test/parallel/test-http2-compat-serverrequest-pause.js +test/js/node/test/parallel/test-http2-compat-serverrequest-pipe.js +test/js/node/test/parallel/test-http2-compat-serverrequest-settimeout.js +test/js/node/test/parallel/test-http2-compat-serverrequest-trailers.js +test/js/node/test/parallel/test-http2-compat-serverrequest.js +test/js/node/test/parallel/test-http2-compat-serverresponse-close.js +test/js/node/test/parallel/test-http2-compat-serverresponse-destroy.js +test/js/node/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js +test/js/node/test/parallel/test-http2-compat-serverresponse-finished.js +test/js/node/test/parallel/test-http2-compat-serverresponse-flushheaders.js +test/js/node/test/parallel/test-http2-compat-serverresponse-headers-send-date.js +test/js/node/test/parallel/test-http2-compat-serverresponse-settimeout.js +test/js/node/test/parallel/test-http2-compat-serverresponse-statuscode.js +test/js/node/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js +test/js/node/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js +test/js/node/test/parallel/test-http2-compat-serverresponse-write.js +test/js/node/test/parallel/test-http2-compat-serverresponse.js +test/js/node/test/parallel/test-http2-compat-socket-destroy-delayed.js +test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-type.js +test/js/node/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js +test/js/node/test/parallel/test-http2-compat-write-early-hints.js +test/js/node/test/parallel/test-http2-compat-write-head-after-close.js +test/js/node/test/parallel/test-http2-compat-write-head-destroyed.js +test/js/node/test/parallel/test-http2-connect-tls-with-delay.js +test/js/node/test/parallel/test-http2-connect.js +test/js/node/test/parallel/test-http2-cookies.js +test/js/node/test/parallel/test-http2-create-client-connect.js +test/js/node/test/parallel/test-http2-create-client-session.js +test/js/node/test/parallel/test-http2-createsecureserver-options.js +test/js/node/test/parallel/test-http2-createserver-options.js +test/js/node/test/parallel/test-http2-createwritereq.js +test/js/node/test/parallel/test-http2-date-header.js +test/js/node/test/parallel/test-http2-destroy-after-write.js +test/js/node/test/parallel/test-http2-dont-override.js +test/js/node/test/parallel/test-http2-endafterheaders.js +test/js/node/test/parallel/test-http2-error-order.js +test/js/node/test/parallel/test-http2-forget-closed-streams.js +test/js/node/test/parallel/test-http2-goaway-opaquedata.js +test/js/node/test/parallel/test-http2-graceful-close.js +test/js/node/test/parallel/test-http2-head-request.js +test/js/node/test/parallel/test-http2-info-headers.js +test/js/node/test/parallel/test-http2-invalidargtypes-errors.js +test/js/node/test/parallel/test-http2-large-write-close.js +test/js/node/test/parallel/test-http2-large-write-destroy.js +test/js/node/test/parallel/test-http2-large-write-multiple-requests.js +test/js/node/test/parallel/test-http2-large-writes-session-memory-leak.js +test/js/node/test/parallel/test-http2-malformed-altsvc.js +test/js/node/test/parallel/test-http2-many-writes-and-destroy.js +test/js/node/test/parallel/test-http2-max-session-memory-leak.js +test/js/node/test/parallel/test-http2-methods.js +test/js/node/test/parallel/test-http2-misbehaving-flow-control-paused.js +test/js/node/test/parallel/test-http2-misbehaving-flow-control.js +test/js/node/test/parallel/test-http2-misused-pseudoheaders.js +test/js/node/test/parallel/test-http2-multiheaders-raw.js +test/js/node/test/parallel/test-http2-multiheaders.js +test/js/node/test/parallel/test-http2-multiplex.js +test/js/node/test/parallel/test-http2-multistream-destroy-on-read-tls.js +test/js/node/test/parallel/test-http2-no-more-streams.js +test/js/node/test/parallel/test-http2-no-wanttrailers-listener.js +test/js/node/test/parallel/test-http2-options-max-headers-block-length.js +test/js/node/test/parallel/test-http2-origin.js +test/js/node/test/parallel/test-http2-pipe-named-pipe.js +test/js/node/test/parallel/test-http2-pipe.js +test/js/node/test/parallel/test-http2-premature-close.js +test/js/node/test/parallel/test-http2-priority-cycle-.js +test/js/node/test/parallel/test-http2-request-remove-connect-listener.js +test/js/node/test/parallel/test-http2-request-response-proto.js +test/js/node/test/parallel/test-http2-res-corked.js +test/js/node/test/parallel/test-http2-respond-errors.js +test/js/node/test/parallel/test-http2-respond-file-204.js +test/js/node/test/parallel/test-http2-respond-file-304.js +test/js/node/test/parallel/test-http2-respond-file-404.js +test/js/node/test/parallel/test-http2-respond-file-compat.js +test/js/node/test/parallel/test-http2-respond-file-error-dir.js +test/js/node/test/parallel/test-http2-respond-file-error-pipe-offset.js +test/js/node/test/parallel/test-http2-respond-file-errors.js +test/js/node/test/parallel/test-http2-respond-file-fd-errors.js +test/js/node/test/parallel/test-http2-respond-file-fd-invalid.js +test/js/node/test/parallel/test-http2-respond-file-fd-range.js +test/js/node/test/parallel/test-http2-respond-file-fd.js +test/js/node/test/parallel/test-http2-respond-file-filehandle.js +test/js/node/test/parallel/test-http2-respond-file-range.js +test/js/node/test/parallel/test-http2-respond-file.js +test/js/node/test/parallel/test-http2-respond-no-data.js +test/js/node/test/parallel/test-http2-respond-with-file-connection-abort.js +test/js/node/test/parallel/test-http2-sent-headers.js +test/js/node/test/parallel/test-http2-serve-file.js +test/js/node/test/parallel/test-http2-server-async-dispose.js +test/js/node/test/parallel/test-http2-server-close-callback.js +test/js/node/test/parallel/test-http2-server-close-idle-connection.js +test/js/node/test/parallel/test-http2-server-errors.js +test/js/node/test/parallel/test-http2-server-rst-before-respond.js +test/js/node/test/parallel/test-http2-server-session-destroy.js +test/js/node/test/parallel/test-http2-server-setLocalWindowSize.js +test/js/node/test/parallel/test-http2-server-shutdown-options-errors.js +test/js/node/test/parallel/test-http2-session-gc-while-write-scheduled.js +test/js/node/test/parallel/test-http2-session-stream-state.js +test/js/node/test/parallel/test-http2-session-timeout.js +test/js/node/test/parallel/test-http2-single-headers.js +test/js/node/test/parallel/test-http2-socket-proxy-handler-for-has.js +test/js/node/test/parallel/test-http2-status-code.js +test/js/node/test/parallel/test-http2-stream-destroy-event-order.js +test/js/node/test/parallel/test-http2-timeouts.js +test/js/node/test/parallel/test-http2-tls-disconnect.js +test/js/node/test/parallel/test-http2-too-many-headers.js +test/js/node/test/parallel/test-http2-trailers-after-session-close.js +test/js/node/test/parallel/test-http2-trailers.js +test/js/node/test/parallel/test-http2-unbound-socket-proxy.js +test/js/node/test/parallel/test-http2-write-callbacks.js +test/js/node/test/parallel/test-http2-zero-length-header.js +test/js/node/test/parallel/test-http2-zero-length-write.js +test/js/node/test/parallel/test-https-agent-constructor.js +test/js/node/test/parallel/test-https-agent-session-eviction.js +test/js/node/test/parallel/test-https-agent.js +test/js/node/test/parallel/test-https-byteswritten.js +test/js/node/test/parallel/test-https-client-get-url.js +test/js/node/test/parallel/test-https-client-renegotiation-limit.js +test/js/node/test/parallel/test-https-close.js +test/js/node/test/parallel/test-https-connecting-to-http.js +test/js/node/test/parallel/test-https-eof-for-eom.js +test/js/node/test/parallel/test-https-foafssl.js +test/js/node/test/parallel/test-https-localaddress-bind-error.js +test/js/node/test/parallel/test-https-options-boolean-check.js +test/js/node/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js +test/js/node/test/parallel/test-https-server-async-dispose.js +test/js/node/test/parallel/test-https-server-close-destroy-timeout.js +test/js/node/test/parallel/test-https-server-headers-timeout.js +test/js/node/test/parallel/test-https-server-request-timeout.js +test/js/node/test/parallel/test-https-simple.js +test/js/node/test/parallel/test-https-socket-options.js +test/js/node/test/parallel/test-https-truncate.js +test/js/node/test/parallel/test-https-unix-socket-self-signed.js +test/js/node/test/parallel/test-icu-env.js +test/js/node/test/parallel/test-icu-punycode.js +test/js/node/test/parallel/test-icu-transcode.js +test/js/node/test/parallel/test-inspect-support-for-node_options.js +test/js/node/test/parallel/test-inspector-enabled.js +test/js/node/test/parallel/test-inspector-has-inspector-false.js +test/js/node/test/parallel/test-inspector-stops-no-file.js +test/js/node/test/parallel/test-inspector-workers-flat-list.js +test/js/node/test/parallel/test-instanceof.js +test/js/node/test/parallel/test-internal-module-require.js +test/js/node/test/parallel/test-internal-process-binding.js +test/js/node/test/parallel/test-intl-v8BreakIterator.js +test/js/node/test/parallel/test-kill-segfault-freebsd.js +test/js/node/test/parallel/test-listen-fd-detached-inherit.js +test/js/node/test/parallel/test-listen-fd-detached.js +test/js/node/test/parallel/test-math-random.js +test/js/node/test/parallel/test-memory-usage-emfile.js +test/js/node/test/parallel/test-memory-usage.js +test/js/node/test/parallel/test-messagechannel.js +test/js/node/test/parallel/test-messageevent-brandcheck.js +test/js/node/test/parallel/test-microtask-queue-integration.js +test/js/node/test/parallel/test-microtask-queue-run-immediate.js +test/js/node/test/parallel/test-microtask-queue-run.js +test/js/node/test/parallel/test-mime-api.js +test/js/node/test/parallel/test-mime-whatwg.js +test/js/node/test/parallel/test-module-builtin.js +test/js/node/test/parallel/test-module-cache.js +test/js/node/test/parallel/test-module-children.js +test/js/node/test/parallel/test-module-circular-dependency-warning.js +test/js/node/test/parallel/test-module-circular-symlinks.js +test/js/node/test/parallel/test-module-create-require.js +test/js/node/test/parallel/test-module-globalpaths-nodepath.js +test/js/node/test/parallel/test-module-loading-deprecated.js +test/js/node/test/parallel/test-module-loading-error.js +test/js/node/test/parallel/test-module-main-extension-lookup.js +test/js/node/test/parallel/test-module-main-fail.js +test/js/node/test/parallel/test-module-main-preserve-symlinks-fail.js +test/js/node/test/parallel/test-module-multi-extensions.js +test/js/node/test/parallel/test-module-nodemodulepaths.js +test/js/node/test/parallel/test-module-parent-deprecation.js +test/js/node/test/parallel/test-module-parent-setter-deprecation.js +test/js/node/test/parallel/test-module-prototype-mutation.js +test/js/node/test/parallel/test-module-readonly.js +test/js/node/test/parallel/test-module-relative-lookup.js +test/js/node/test/parallel/test-module-run-main-monkey-patch.js +test/js/node/test/parallel/test-module-stat.js +test/js/node/test/parallel/test-module-symlinked-peer-modules.js +test/js/node/test/parallel/test-module-version.js +test/js/node/test/parallel/test-module-wrap.js +test/js/node/test/parallel/test-module-wrapper.js +test/js/node/test/parallel/test-net-access-byteswritten.js +test/js/node/test/parallel/test-net-after-close.js +test/js/node/test/parallel/test-net-autoselectfamily-attempt-timeout-default-value.js +test/js/node/test/parallel/test-net-autoselectfamily-default.js +test/js/node/test/parallel/test-net-autoselectfamily-ipv4first.js +test/js/node/test/parallel/test-net-better-error-messages-listen-path.js +test/js/node/test/parallel/test-net-better-error-messages-listen.js +test/js/node/test/parallel/test-net-better-error-messages-port-hostname.js +test/js/node/test/parallel/test-net-bind-twice.js +test/js/node/test/parallel/test-net-blocklist.js +test/js/node/test/parallel/test-net-buffersize.js +test/js/node/test/parallel/test-net-bytes-written-large.js +test/js/node/test/parallel/test-net-can-reset-timeout.js +test/js/node/test/parallel/test-net-child-process-connect-reset.js +test/js/node/test/parallel/test-net-connect-abort-controller.js +test/js/node/test/parallel/test-net-connect-after-destroy.js +test/js/node/test/parallel/test-net-connect-buffer.js +test/js/node/test/parallel/test-net-connect-buffer2.js +test/js/node/test/parallel/test-net-connect-call-socket-connect.js +test/js/node/test/parallel/test-net-connect-destroy.js +test/js/node/test/parallel/test-net-connect-immediate-destroy.js +test/js/node/test/parallel/test-net-connect-immediate-finish.js +test/js/node/test/parallel/test-net-connect-no-arg.js +test/js/node/test/parallel/test-net-connect-nodelay.js +test/js/node/test/parallel/test-net-connect-options-invalid.js +test/js/node/test/parallel/test-net-connect-options-ipv6.js +test/js/node/test/parallel/test-net-connect-options-path.js +test/js/node/test/parallel/test-net-connect-options-port.js +test/js/node/test/parallel/test-net-connect-reset-before-connected.js +test/js/node/test/parallel/test-net-connect-reset.js +test/js/node/test/parallel/test-net-deprecated-setsimultaneousaccepts.js +test/js/node/test/parallel/test-net-dns-custom-lookup.js +test/js/node/test/parallel/test-net-dns-error.js +test/js/node/test/parallel/test-net-dns-lookup-skip.js +test/js/node/test/parallel/test-net-dns-lookup.js +test/js/node/test/parallel/test-net-during-close.js +test/js/node/test/parallel/test-net-eaddrinuse.js +test/js/node/test/parallel/test-net-end-without-connect.js +test/js/node/test/parallel/test-net-isip.js +test/js/node/test/parallel/test-net-isipv4.js +test/js/node/test/parallel/test-net-isipv6.js +test/js/node/test/parallel/test-net-keepalive.js +test/js/node/test/parallel/test-net-listen-after-destroying-stdin.js +test/js/node/test/parallel/test-net-listen-close-server-callback-is-not-function.js +test/js/node/test/parallel/test-net-listen-close-server.js +test/js/node/test/parallel/test-net-listen-error.js +test/js/node/test/parallel/test-net-listen-exclusive-random-ports.js +test/js/node/test/parallel/test-net-listen-handle-in-cluster-1.js +test/js/node/test/parallel/test-net-listen-invalid-port.js +test/js/node/test/parallel/test-net-listen-ipv6only.js +test/js/node/test/parallel/test-net-listening.js +test/js/node/test/parallel/test-net-local-address-port.js +test/js/node/test/parallel/test-net-localerror.js +test/js/node/test/parallel/test-net-options-lookup.js +test/js/node/test/parallel/test-net-persistent-keepalive.js +test/js/node/test/parallel/test-net-reconnect.js +test/js/node/test/parallel/test-net-remote-address-port.js +test/js/node/test/parallel/test-net-remote-address.js +test/js/node/test/parallel/test-net-reuseport.js +test/js/node/test/parallel/test-net-server-async-dispose.mjs +test/js/node/test/parallel/test-net-server-blocklist.js +test/js/node/test/parallel/test-net-server-call-listen-multiple-times.js +test/js/node/test/parallel/test-net-server-close-before-calling-lookup-callback.js +test/js/node/test/parallel/test-net-server-close-before-ipc-response.js +test/js/node/test/parallel/test-net-server-close.js +test/js/node/test/parallel/test-net-server-drop-connections.js +test/js/node/test/parallel/test-net-server-listen-options-signal.js +test/js/node/test/parallel/test-net-server-listen-remove-callback.js +test/js/node/test/parallel/test-net-server-max-connections-close-makes-more-available.js +test/js/node/test/parallel/test-net-server-max-connections.js +test/js/node/test/parallel/test-net-server-options.js +test/js/node/test/parallel/test-net-server-pause-on-connect.js +test/js/node/test/parallel/test-net-server-simultaneous-accepts-produce-warning-once.js +test/js/node/test/parallel/test-net-server-try-ports.js +test/js/node/test/parallel/test-net-server-unref-persistent.js +test/js/node/test/parallel/test-net-server-unref.js +test/js/node/test/parallel/test-net-settimeout.js +test/js/node/test/parallel/test-net-socket-byteswritten.js +test/js/node/test/parallel/test-net-socket-close-after-end.js +test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamily.js +test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamilyattempttimeout.js +test/js/node/test/parallel/test-net-socket-connect-without-cb.js +test/js/node/test/parallel/test-net-socket-connecting.js +test/js/node/test/parallel/test-net-socket-constructor.js +test/js/node/test/parallel/test-net-socket-destroy-send.js +test/js/node/test/parallel/test-net-socket-destroy-twice.js +test/js/node/test/parallel/test-net-socket-end-before-connect.js +test/js/node/test/parallel/test-net-socket-end-callback.js +test/js/node/test/parallel/test-net-socket-local-address.js +test/js/node/test/parallel/test-net-socket-no-halfopen-enforcer.js +test/js/node/test/parallel/test-net-socket-ready-without-cb.js +test/js/node/test/parallel/test-net-socket-reset-twice.js +test/js/node/test/parallel/test-net-socket-timeout-unref.js +test/js/node/test/parallel/test-net-socket-timeout.js +test/js/node/test/parallel/test-net-socket-write-error.js +test/js/node/test/parallel/test-net-sync-cork.js +test/js/node/test/parallel/test-net-throttle.js +test/js/node/test/parallel/test-net-timeout-no-handle.js +test/js/node/test/parallel/test-net-writable.js +test/js/node/test/parallel/test-net-write-arguments.js +test/js/node/test/parallel/test-net-write-cb-on-destroy-before-connect.js +test/js/node/test/parallel/test-net-write-connect-write.js +test/js/node/test/parallel/test-net-write-fully-async-buffer.js +test/js/node/test/parallel/test-net-write-fully-async-hex-string.js +test/js/node/test/parallel/test-net-write-slow.js +test/js/node/test/parallel/test-next-tick-doesnt-hang.js +test/js/node/test/parallel/test-next-tick-domain.js +test/js/node/test/parallel/test-next-tick-errors.js +test/js/node/test/parallel/test-next-tick-fixed-queue-regression.js +test/js/node/test/parallel/test-next-tick-intentional-starvation.js +test/js/node/test/parallel/test-next-tick-ordering.js +test/js/node/test/parallel/test-next-tick-ordering2.js +test/js/node/test/parallel/test-next-tick-when-exiting.js +test/js/node/test/parallel/test-next-tick.js +test/js/node/test/parallel/test-no-addons-resolution-condition.js +test/js/node/test/parallel/test-no-node-snapshot.js +test/js/node/test/parallel/test-os-eol.js +test/js/node/test/parallel/test-os-homedir-no-envvar.js +test/js/node/test/parallel/test-os-process-priority.js +test/js/node/test/parallel/test-os-userinfo-handles-getter-errors.js +test/js/node/test/parallel/test-os.js +test/js/node/test/parallel/test-outgoing-message-destroy.js +test/js/node/test/parallel/test-outgoing-message-pipe.js +test/js/node/test/parallel/test-parse-args.mjs +test/js/node/test/parallel/test-path-basename.js +test/js/node/test/parallel/test-path-dirname.js +test/js/node/test/parallel/test-path-extname.js +test/js/node/test/parallel/test-path-glob.js +test/js/node/test/parallel/test-path-isabsolute.js +test/js/node/test/parallel/test-path-join.js +test/js/node/test/parallel/test-path-makelong.js +test/js/node/test/parallel/test-path-normalize.js +test/js/node/test/parallel/test-path-parse-format.js +test/js/node/test/parallel/test-path-posix-exists.js +test/js/node/test/parallel/test-path-posix-relative-on-windows.js +test/js/node/test/parallel/test-path-relative.js +test/js/node/test/parallel/test-path-resolve.js +test/js/node/test/parallel/test-path-win32-exists.js +test/js/node/test/parallel/test-path-zero-length-strings.js +test/js/node/test/parallel/test-path.js +test/js/node/test/parallel/test-perf-gc-crash.js +test/js/node/test/parallel/test-performance-measure.js +test/js/node/test/parallel/test-performanceobserver-gc.js +test/js/node/test/parallel/test-permission-fs-supported.js +test/js/node/test/parallel/test-pipe-abstract-socket-http.js +test/js/node/test/parallel/test-pipe-address.js +test/js/node/test/parallel/test-pipe-file-to-http.js +test/js/node/test/parallel/test-pipe-head.js +test/js/node/test/parallel/test-pipe-outgoing-message-data-emitted-after-ended.js +test/js/node/test/parallel/test-pipe-return-val.js +test/js/node/test/parallel/test-pipe-writev.js +test/js/node/test/parallel/test-preload-print-process-argv.js +test/js/node/test/parallel/test-preload-self-referential.js +test/js/node/test/parallel/test-process-abort.js +test/js/node/test/parallel/test-process-argv-0.js +test/js/node/test/parallel/test-process-assert.js +test/js/node/test/parallel/test-process-available-memory.js +test/js/node/test/parallel/test-process-beforeexit-throw-exit.js +test/js/node/test/parallel/test-process-beforeexit.js +test/js/node/test/parallel/test-process-binding-util.js +test/js/node/test/parallel/test-process-chdir-errormessage.js +test/js/node/test/parallel/test-process-chdir.js +test/js/node/test/parallel/test-process-config.js +test/js/node/test/parallel/test-process-constants-noatime.js +test/js/node/test/parallel/test-process-constrained-memory.js +test/js/node/test/parallel/test-process-cpuUsage.js +test/js/node/test/parallel/test-process-default.js +test/js/node/test/parallel/test-process-dlopen-error-message-crash.js +test/js/node/test/parallel/test-process-dlopen-undefined-exports.js +test/js/node/test/parallel/test-process-domain-segfault.js +test/js/node/test/parallel/test-process-emit.js +test/js/node/test/parallel/test-process-emitwarning.js +test/js/node/test/parallel/test-process-env-windows-error-reset.js +test/js/node/test/parallel/test-process-euid-egid.js +test/js/node/test/parallel/test-process-exception-capture-errors.js +test/js/node/test/parallel/test-process-exception-capture-should-abort-on-uncaught.js +test/js/node/test/parallel/test-process-exception-capture.js +test/js/node/test/parallel/test-process-execpath.js +test/js/node/test/parallel/test-process-exit-code-validation.js +test/js/node/test/parallel/test-process-exit-from-before-exit.js +test/js/node/test/parallel/test-process-exit-handler.js +test/js/node/test/parallel/test-process-exit-recursive.js +test/js/node/test/parallel/test-process-exit.js +test/js/node/test/parallel/test-process-external-stdio-close-spawn.js +test/js/node/test/parallel/test-process-external-stdio-close.js +test/js/node/test/parallel/test-process-features.js +test/js/node/test/parallel/test-process-getgroups.js +test/js/node/test/parallel/test-process-hrtime-bigint.js +test/js/node/test/parallel/test-process-hrtime.js +test/js/node/test/parallel/test-process-kill-null.js +test/js/node/test/parallel/test-process-kill-pid.js +test/js/node/test/parallel/test-process-next-tick.js +test/js/node/test/parallel/test-process-no-deprecation.js +test/js/node/test/parallel/test-process-ppid.js +test/js/node/test/parallel/test-process-really-exit.js +test/js/node/test/parallel/test-process-release.js +test/js/node/test/parallel/test-process-remove-all-signal-listeners.js +test/js/node/test/parallel/test-process-setgroups.js +test/js/node/test/parallel/test-process-setsourcemapsenabled.js +test/js/node/test/parallel/test-process-title-cli.js +test/js/node/test/parallel/test-process-uid-gid.js +test/js/node/test/parallel/test-process-umask-mask.js +test/js/node/test/parallel/test-process-umask.js +test/js/node/test/parallel/test-process-uptime.js +test/js/node/test/parallel/test-process-warning.js +test/js/node/test/parallel/test-promise-handled-rejection-no-warning.js +test/js/node/test/parallel/test-promise-unhandled-default.js +test/js/node/test/parallel/test-promise-unhandled-error.js +test/js/node/test/parallel/test-promise-unhandled-flag.js +test/js/node/test/parallel/test-promise-unhandled-issue-43655.js +test/js/node/test/parallel/test-promise-unhandled-silent-no-hook.js +test/js/node/test/parallel/test-promise-unhandled-silent.js +test/js/node/test/parallel/test-promise-unhandled-throw-handler.js +test/js/node/test/parallel/test-promise-unhandled-throw.js +test/js/node/test/parallel/test-promise-unhandled-warn-no-hook.js +test/js/node/test/parallel/test-promises-unhandled-proxy-rejections.js +test/js/node/test/parallel/test-promises-unhandled-rejections.js +test/js/node/test/parallel/test-promises-unhandled-symbol-rejections.js +test/js/node/test/parallel/test-promises-warning-on-unhandled-rejection.js +test/js/node/test/parallel/test-punycode.js +test/js/node/test/parallel/test-querystring-escape.js +test/js/node/test/parallel/test-querystring-maxKeys-non-finite.js +test/js/node/test/parallel/test-querystring-multichar-separator.js +test/js/node/test/parallel/test-querystring.js +test/js/node/test/parallel/test-queue-microtask.js +test/js/node/test/parallel/test-quic-internal-endpoint-listen-defaults.js +test/js/node/test/parallel/test-quic-internal-endpoint-options.js +test/js/node/test/parallel/test-quic-internal-endpoint-stats-state.js +test/js/node/test/parallel/test-quic-internal-setcallbacks.js +test/js/node/test/parallel/test-readable-from-iterator-closing.js +test/js/node/test/parallel/test-readable-from-web-enqueue-then-close.js +test/js/node/test/parallel/test-readable-from.js +test/js/node/test/parallel/test-readable-large-hwm.js +test/js/node/test/parallel/test-readable-single-end.js +test/js/node/test/parallel/test-readline-async-iterators-backpressure.js +test/js/node/test/parallel/test-readline-async-iterators-destroy.js +test/js/node/test/parallel/test-readline-async-iterators.js +test/js/node/test/parallel/test-readline-carriage-return-between-chunks.js +test/js/node/test/parallel/test-readline-csi.js +test/js/node/test/parallel/test-readline-emit-keypress-events.js +test/js/node/test/parallel/test-readline-input-onerror.js +test/js/node/test/parallel/test-readline-interface-escapecodetimeout.js +test/js/node/test/parallel/test-readline-interface-no-trailing-newline.js +test/js/node/test/parallel/test-readline-interface-recursive-writes.js +test/js/node/test/parallel/test-readline-keys.js +test/js/node/test/parallel/test-readline-position.js +test/js/node/test/parallel/test-readline-promises-csi.mjs +test/js/node/test/parallel/test-readline-promises-tab-complete.js +test/js/node/test/parallel/test-readline-reopen.js +test/js/node/test/parallel/test-readline-set-raw-mode.js +test/js/node/test/parallel/test-readline-tab-complete.js +test/js/node/test/parallel/test-readline-undefined-columns.js +test/js/node/test/parallel/test-readline.js +test/js/node/test/parallel/test-ref-unref-return.js +test/js/node/test/parallel/test-repl-clear-immediate-crash.js +test/js/node/test/parallel/test-repl-close.js +test/js/node/test/parallel/test-repl-dynamic-import.js +test/js/node/test/parallel/test-repl-preview-without-inspector.js +test/js/node/test/parallel/test-repl-syntax-error-handling.js +test/js/node/test/parallel/test-require-cache.js +test/js/node/test/parallel/test-require-delete-array-iterator.js +test/js/node/test/parallel/test-require-dot.js +test/js/node/test/parallel/test-require-empty-main.js +test/js/node/test/parallel/test-require-enoent-dir.js +test/js/node/test/parallel/test-require-exceptions.js +test/js/node/test/parallel/test-require-extension-over-directory.js +test/js/node/test/parallel/test-require-extensions-main.js +test/js/node/test/parallel/test-require-extensions-same-filename-as-dir-trailing-slash.js +test/js/node/test/parallel/test-require-invalid-main-no-exports.js +test/js/node/test/parallel/test-require-invalid-package.js +test/js/node/test/parallel/test-require-json.js +test/js/node/test/parallel/test-require-long-path.js +test/js/node/test/parallel/test-require-node-prefix.js +test/js/node/test/parallel/test-require-nul.js +test/js/node/test/parallel/test-require-process.js +test/js/node/test/parallel/test-require-resolve.js +test/js/node/test/parallel/test-require-symlink.js +test/js/node/test/parallel/test-require-unicode.js +test/js/node/test/parallel/test-resource-usage.js +test/js/node/test/parallel/test-runner-filter-warning.js +test/js/node/test/parallel/test-runner-subtest-after-hook.js +test/js/node/test/parallel/test-set-process-debug-port.js +test/js/node/test/parallel/test-shadow-realm-gc-module.js +test/js/node/test/parallel/test-shadow-realm-module.js +test/js/node/test/parallel/test-shadow-realm-preload-module.js +test/js/node/test/parallel/test-shadow-realm-prepare-stack-trace.js +test/js/node/test/parallel/test-shadow-realm.js +test/js/node/test/parallel/test-sigint-infinite-loop.js +test/js/node/test/parallel/test-signal-args.js +test/js/node/test/parallel/test-signal-handler-remove-on-exit.js +test/js/node/test/parallel/test-signal-handler.js +test/js/node/test/parallel/test-signal-unregister.js +test/js/node/test/parallel/test-socket-address.js +test/js/node/test/parallel/test-socket-options-invalid.js +test/js/node/test/parallel/test-socket-write-after-fin-error.js +test/js/node/test/parallel/test-spawn-cmd-named-pipe.js +test/js/node/test/parallel/test-stdin-child-proc.js +test/js/node/test/parallel/test-stdin-from-file-spawn.js +test/js/node/test/parallel/test-stdin-from-file.js +test/js/node/test/parallel/test-stdin-hang.js +test/js/node/test/parallel/test-stdin-pause-resume-sync.js +test/js/node/test/parallel/test-stdin-pause-resume.js +test/js/node/test/parallel/test-stdin-pipe-large.js +test/js/node/test/parallel/test-stdin-pipe-resume.js +test/js/node/test/parallel/test-stdin-resume-pause.js +test/js/node/test/parallel/test-stdin-script-child-option.js +test/js/node/test/parallel/test-stdin-script-child.js +test/js/node/test/parallel/test-stdio-closed.js +test/js/node/test/parallel/test-stdio-pipe-access.js +test/js/node/test/parallel/test-stdio-pipe-stderr.js +test/js/node/test/parallel/test-stdio-undestroy.js +test/js/node/test/parallel/test-stdout-cannot-be-closed-child-process-pipe.js +test/js/node/test/parallel/test-stdout-pipeline-destroy.js +test/js/node/test/parallel/test-stdout-stderr-reading.js +test/js/node/test/parallel/test-stdout-stderr-write.js +test/js/node/test/parallel/test-stdout-to-file.js +test/js/node/test/parallel/test-stream-aliases-legacy.js +test/js/node/test/parallel/test-stream-auto-destroy.js +test/js/node/test/parallel/test-stream-await-drain-writers-in-synchronously-recursion-write.js +test/js/node/test/parallel/test-stream-backpressure.js +test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js +test/js/node/test/parallel/test-stream-big-packet.js +test/js/node/test/parallel/test-stream-big-push.js +test/js/node/test/parallel/test-stream-catch-rejections.js +test/js/node/test/parallel/test-stream-compose-operator.js +test/js/node/test/parallel/test-stream-compose.js +test/js/node/test/parallel/test-stream-construct.js +test/js/node/test/parallel/test-stream-consumers.js +test/js/node/test/parallel/test-stream-decoder-objectmode.js +test/js/node/test/parallel/test-stream-destroy-event-order.js +test/js/node/test/parallel/test-stream-drop-take.js +test/js/node/test/parallel/test-stream-duplex-destroy.js +test/js/node/test/parallel/test-stream-duplex-end.js +test/js/node/test/parallel/test-stream-duplex-from.js +test/js/node/test/parallel/test-stream-duplex-props.js +test/js/node/test/parallel/test-stream-duplex-readable-end.js +test/js/node/test/parallel/test-stream-duplex-readable-writable.js +test/js/node/test/parallel/test-stream-duplex-writable-finished.js +test/js/node/test/parallel/test-stream-duplex.js +test/js/node/test/parallel/test-stream-duplexpair.js +test/js/node/test/parallel/test-stream-end-of-streams.js +test/js/node/test/parallel/test-stream-end-paused.js +test/js/node/test/parallel/test-stream-err-multiple-callback-construction.js +test/js/node/test/parallel/test-stream-error-once.js +test/js/node/test/parallel/test-stream-event-names.js +test/js/node/test/parallel/test-stream-events-prepend.js +test/js/node/test/parallel/test-stream-filter.js +test/js/node/test/parallel/test-stream-finished.js +test/js/node/test/parallel/test-stream-flatMap.js +test/js/node/test/parallel/test-stream-forEach.js +test/js/node/test/parallel/test-stream-inheritance.js +test/js/node/test/parallel/test-stream-ispaused.js +test/js/node/test/parallel/test-stream-iterator-helpers-test262-tests.mjs +test/js/node/test/parallel/test-stream-map.js +test/js/node/test/parallel/test-stream-objectmode-undefined.js +test/js/node/test/parallel/test-stream-once-readable-pipe.js +test/js/node/test/parallel/test-stream-passthrough-drain.js +test/js/node/test/parallel/test-stream-pipe-after-end.js +test/js/node/test/parallel/test-stream-pipe-await-drain-manual-resume.js +test/js/node/test/parallel/test-stream-pipe-await-drain-push-while-write.js +test/js/node/test/parallel/test-stream-pipe-await-drain.js +test/js/node/test/parallel/test-stream-pipe-cleanup-pause.js +test/js/node/test/parallel/test-stream-pipe-cleanup.js +test/js/node/test/parallel/test-stream-pipe-deadlock.js +test/js/node/test/parallel/test-stream-pipe-error-handling.js +test/js/node/test/parallel/test-stream-pipe-error-unhandled.js +test/js/node/test/parallel/test-stream-pipe-event.js +test/js/node/test/parallel/test-stream-pipe-flow-after-unpipe.js +test/js/node/test/parallel/test-stream-pipe-flow.js +test/js/node/test/parallel/test-stream-pipe-manual-resume.js +test/js/node/test/parallel/test-stream-pipe-multiple-pipes.js +test/js/node/test/parallel/test-stream-pipe-needDrain.js +test/js/node/test/parallel/test-stream-pipe-same-destination-twice.js +test/js/node/test/parallel/test-stream-pipe-unpipe-streams.js +test/js/node/test/parallel/test-stream-pipe-without-listenerCount.js +test/js/node/test/parallel/test-stream-pipeline-async-iterator.js +test/js/node/test/parallel/test-stream-pipeline-duplex.js +test/js/node/test/parallel/test-stream-pipeline-listeners.js +test/js/node/test/parallel/test-stream-pipeline-process.js +test/js/node/test/parallel/test-stream-pipeline-queued-end-in-destroy.js +test/js/node/test/parallel/test-stream-pipeline-uncaught.js +test/js/node/test/parallel/test-stream-pipeline-with-empty-string.js +test/js/node/test/parallel/test-stream-preprocess.js +test/js/node/test/parallel/test-stream-promises.js +test/js/node/test/parallel/test-stream-push-order.js +test/js/node/test/parallel/test-stream-push-strings.js +test/js/node/test/parallel/test-stream-readable-aborted.js +test/js/node/test/parallel/test-stream-readable-add-chunk-during-data.js +test/js/node/test/parallel/test-stream-readable-constructor-set-methods.js +test/js/node/test/parallel/test-stream-readable-data.js +test/js/node/test/parallel/test-stream-readable-default-encoding.js +test/js/node/test/parallel/test-stream-readable-destroy.js +test/js/node/test/parallel/test-stream-readable-didRead.js +test/js/node/test/parallel/test-stream-readable-dispose.js +test/js/node/test/parallel/test-stream-readable-emit-readable-short-stream.js +test/js/node/test/parallel/test-stream-readable-emittedReadable.js +test/js/node/test/parallel/test-stream-readable-end-destroyed.js +test/js/node/test/parallel/test-stream-readable-ended.js +test/js/node/test/parallel/test-stream-readable-error-end.js +test/js/node/test/parallel/test-stream-readable-event.js +test/js/node/test/parallel/test-stream-readable-flow-recursion.js +test/js/node/test/parallel/test-stream-readable-from-web-termination.js +test/js/node/test/parallel/test-stream-readable-hwm-0-async.js +test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js +test/js/node/test/parallel/test-stream-readable-hwm-0.js +test/js/node/test/parallel/test-stream-readable-infinite-read.js +test/js/node/test/parallel/test-stream-readable-invalid-chunk.js +test/js/node/test/parallel/test-stream-readable-needReadable.js +test/js/node/test/parallel/test-stream-readable-next-no-null.js +test/js/node/test/parallel/test-stream-readable-no-unneeded-readable.js +test/js/node/test/parallel/test-stream-readable-object-multi-push-async.js +test/js/node/test/parallel/test-stream-readable-pause-and-resume.js +test/js/node/test/parallel/test-stream-readable-readable-then-resume.js +test/js/node/test/parallel/test-stream-readable-readable.js +test/js/node/test/parallel/test-stream-readable-reading-readingMore.js +test/js/node/test/parallel/test-stream-readable-resume-hwm.js +test/js/node/test/parallel/test-stream-readable-resumeScheduled.js +test/js/node/test/parallel/test-stream-readable-setEncoding-existing-buffers.js +test/js/node/test/parallel/test-stream-readable-setEncoding-null.js +test/js/node/test/parallel/test-stream-readable-strategy-option.js +test/js/node/test/parallel/test-stream-readable-to-web-termination.js +test/js/node/test/parallel/test-stream-readable-to-web.js +test/js/node/test/parallel/test-stream-readable-unpipe-resume.js +test/js/node/test/parallel/test-stream-readable-unshift.js +test/js/node/test/parallel/test-stream-readable-with-unimplemented-_read.js +test/js/node/test/parallel/test-stream-readableListening-state.js +test/js/node/test/parallel/test-stream-reduce.js +test/js/node/test/parallel/test-stream-set-default-hwm.js +test/js/node/test/parallel/test-stream-some-find-every.mjs +test/js/node/test/parallel/test-stream-toArray.js +test/js/node/test/parallel/test-stream-toWeb-allows-server-response.js +test/js/node/test/parallel/test-stream-transform-callback-twice.js +test/js/node/test/parallel/test-stream-transform-constructor-set-methods.js +test/js/node/test/parallel/test-stream-transform-destroy.js +test/js/node/test/parallel/test-stream-transform-final-sync.js +test/js/node/test/parallel/test-stream-transform-final.js +test/js/node/test/parallel/test-stream-transform-flush-data.js +test/js/node/test/parallel/test-stream-transform-hwm0.js +test/js/node/test/parallel/test-stream-transform-objectmode-falsey-value.js +test/js/node/test/parallel/test-stream-transform-split-highwatermark.js +test/js/node/test/parallel/test-stream-transform-split-objectmode.js +test/js/node/test/parallel/test-stream-typedarray.js +test/js/node/test/parallel/test-stream-uint8array.js +test/js/node/test/parallel/test-stream-unpipe-event.js +test/js/node/test/parallel/test-stream-unshift-empty-chunk.js +test/js/node/test/parallel/test-stream-unshift-read-race.js +test/js/node/test/parallel/test-stream-writable-aborted.js +test/js/node/test/parallel/test-stream-writable-change-default-encoding.js +test/js/node/test/parallel/test-stream-writable-clear-buffer.js +test/js/node/test/parallel/test-stream-writable-constructor-set-methods.js +test/js/node/test/parallel/test-stream-writable-decoded-encoding.js +test/js/node/test/parallel/test-stream-writable-destroy.js +test/js/node/test/parallel/test-stream-writable-end-cb-error.js +test/js/node/test/parallel/test-stream-writable-end-cb-uncaught.js +test/js/node/test/parallel/test-stream-writable-end-multiple.js +test/js/node/test/parallel/test-stream-writable-ended-state.js +test/js/node/test/parallel/test-stream-writable-final-async.js +test/js/node/test/parallel/test-stream-writable-final-destroy.js +test/js/node/test/parallel/test-stream-writable-final-throw.js +test/js/node/test/parallel/test-stream-writable-finish-destroyed.js +test/js/node/test/parallel/test-stream-writable-finished-state.js +test/js/node/test/parallel/test-stream-writable-finished.js +test/js/node/test/parallel/test-stream-writable-invalid-chunk.js +test/js/node/test/parallel/test-stream-writable-needdrain-state.js +test/js/node/test/parallel/test-stream-writable-null.js +test/js/node/test/parallel/test-stream-writable-properties.js +test/js/node/test/parallel/test-stream-writable-writable.js +test/js/node/test/parallel/test-stream-writable-write-cb-error.js +test/js/node/test/parallel/test-stream-writable-write-cb-twice.js +test/js/node/test/parallel/test-stream-writable-write-error.js +test/js/node/test/parallel/test-stream-writable-write-writev-finish.js +test/js/node/test/parallel/test-stream-writableState-ending.js +test/js/node/test/parallel/test-stream-writableState-uncorked-bufferedRequestCount.js +test/js/node/test/parallel/test-stream-write-destroy.js +test/js/node/test/parallel/test-stream-write-drain.js +test/js/node/test/parallel/test-stream-write-final.js +test/js/node/test/parallel/test-stream-writev.js +test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js +test/js/node/test/parallel/test-stream2-basic.js +test/js/node/test/parallel/test-stream2-compatibility.js +test/js/node/test/parallel/test-stream2-decode-partial.js +test/js/node/test/parallel/test-stream2-finish-pipe-error.js +test/js/node/test/parallel/test-stream2-finish-pipe.js +test/js/node/test/parallel/test-stream2-large-read-stall.js +test/js/node/test/parallel/test-stream2-objects.js +test/js/node/test/parallel/test-stream2-pipe-error-handling.js +test/js/node/test/parallel/test-stream2-pipe-error-once-listener.js +test/js/node/test/parallel/test-stream2-push.js +test/js/node/test/parallel/test-stream2-read-sync-stack.js +test/js/node/test/parallel/test-stream2-readable-empty-buffer-no-eof.js +test/js/node/test/parallel/test-stream2-readable-legacy-drain.js +test/js/node/test/parallel/test-stream2-readable-non-empty-end.js +test/js/node/test/parallel/test-stream2-readable-wrap-destroy.js +test/js/node/test/parallel/test-stream2-readable-wrap-empty.js +test/js/node/test/parallel/test-stream2-readable-wrap-error.js +test/js/node/test/parallel/test-stream2-readable-wrap.js +test/js/node/test/parallel/test-stream2-set-encoding.js +test/js/node/test/parallel/test-stream2-transform.js +test/js/node/test/parallel/test-stream2-unpipe-drain.js +test/js/node/test/parallel/test-stream2-unpipe-leak.js +test/js/node/test/parallel/test-stream2-writable.js +test/js/node/test/parallel/test-stream3-cork-end.js +test/js/node/test/parallel/test-stream3-cork-uncork.js +test/js/node/test/parallel/test-stream3-pause-then-read.js +test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js +test/js/node/test/parallel/test-streams-highwatermark.js +test/js/node/test/parallel/test-string-decoder-end.js +test/js/node/test/parallel/test-string-decoder.js +test/js/node/test/parallel/test-stringbytes-external.js +test/js/node/test/parallel/test-sync-fileread.js +test/js/node/test/parallel/test-sys.js +test/js/node/test/parallel/test-timers-api-refs.js +test/js/node/test/parallel/test-timers-args.js +test/js/node/test/parallel/test-timers-clear-null-does-not-throw-error.js +test/js/node/test/parallel/test-timers-clear-object-does-not-throw-error.js +test/js/node/test/parallel/test-timers-clear-timeout-interval-equivalent.js +test/js/node/test/parallel/test-timers-clearImmediate-als.js +test/js/node/test/parallel/test-timers-clearImmediate.js +test/js/node/test/parallel/test-timers-destroyed.js +test/js/node/test/parallel/test-timers-dispose.js +test/js/node/test/parallel/test-timers-immediate-promisified.js +test/js/node/test/parallel/test-timers-immediate-queue-throw.js +test/js/node/test/parallel/test-timers-immediate-queue.js +test/js/node/test/parallel/test-timers-immediate-unref-nested-once.js +test/js/node/test/parallel/test-timers-immediate-unref-simple.js +test/js/node/test/parallel/test-timers-immediate-unref.js +test/js/node/test/parallel/test-timers-immediate.js +test/js/node/test/parallel/test-timers-interval-promisified.js +test/js/node/test/parallel/test-timers-interval-throw.js +test/js/node/test/parallel/test-timers-linked-list.js +test/js/node/test/parallel/test-timers-max-duration-warning.js +test/js/node/test/parallel/test-timers-nan-duration-emit-once-per-process.js +test/js/node/test/parallel/test-timers-nan-duration-warning-promises.js +test/js/node/test/parallel/test-timers-nan-duration-warning.js +test/js/node/test/parallel/test-timers-negative-duration-warning-emit-once-per-process.js +test/js/node/test/parallel/test-timers-negative-duration-warning.js +test/js/node/test/parallel/test-timers-nested.js +test/js/node/test/parallel/test-timers-next-tick.js +test/js/node/test/parallel/test-timers-non-integer-delay.js +test/js/node/test/parallel/test-timers-not-emit-duration-zero.js +test/js/node/test/parallel/test-timers-now.js +test/js/node/test/parallel/test-timers-ordering.js +test/js/node/test/parallel/test-timers-process-tampering.js +test/js/node/test/parallel/test-timers-promises-scheduler.js +test/js/node/test/parallel/test-timers-promises.js +test/js/node/test/parallel/test-timers-refresh-in-callback.js +test/js/node/test/parallel/test-timers-refresh.js +test/js/node/test/parallel/test-timers-same-timeout-wrong-list-deleted.js +test/js/node/test/parallel/test-timers-setimmediate-infinite-loop.js +test/js/node/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js +test/js/node/test/parallel/test-timers-this.js +test/js/node/test/parallel/test-timers-throw-when-cb-not-function.js +test/js/node/test/parallel/test-timers-timeout-promisified.js +test/js/node/test/parallel/test-timers-timeout-to-interval.js +test/js/node/test/parallel/test-timers-timeout-with-non-integer.js +test/js/node/test/parallel/test-timers-to-primitive.js +test/js/node/test/parallel/test-timers-uncaught-exception.js +test/js/node/test/parallel/test-timers-unenroll-unref-interval.js +test/js/node/test/parallel/test-timers-unref-throw-then-ref.js +test/js/node/test/parallel/test-timers-unref.js +test/js/node/test/parallel/test-timers-unrefd-interval-still-fires.js +test/js/node/test/parallel/test-timers-unrefed-in-beforeexit.js +test/js/node/test/parallel/test-timers-unrefed-in-callback.js +test/js/node/test/parallel/test-timers-user-call.js +test/js/node/test/parallel/test-timers-zero-timeout.js +test/js/node/test/parallel/test-timers.js +test/js/node/test/parallel/test-tls-0-dns-altname.js +test/js/node/test/parallel/test-tls-add-context.js +test/js/node/test/parallel/test-tls-alert-handling.js +test/js/node/test/parallel/test-tls-alert.js +test/js/node/test/parallel/test-tls-ca-concat.js +test/js/node/test/parallel/test-tls-cert-ext-encoding.js +test/js/node/test/parallel/test-tls-cert-regression.js +test/js/node/test/parallel/test-tls-check-server-identity.js +test/js/node/test/parallel/test-tls-client-abort.js +test/js/node/test/parallel/test-tls-client-abort2.js +test/js/node/test/parallel/test-tls-client-destroy-soon.js +test/js/node/test/parallel/test-tls-client-renegotiation-limit.js +test/js/node/test/parallel/test-tls-client-verify.js +test/js/node/test/parallel/test-tls-close-error.js +test/js/node/test/parallel/test-tls-close-event-after-write.js +test/js/node/test/parallel/test-tls-connect-abort-controller.js +test/js/node/test/parallel/test-tls-connect-address-family.js +test/js/node/test/parallel/test-tls-connect-hints-option.js +test/js/node/test/parallel/test-tls-connect-hwm-option.js +test/js/node/test/parallel/test-tls-connect-no-host.js +test/js/node/test/parallel/test-tls-connect-pipe.js +test/js/node/test/parallel/test-tls-connect-secure-context.js +test/js/node/test/parallel/test-tls-connect-simple.js +test/js/node/test/parallel/test-tls-destroy-whilst-write.js +test/js/node/test/parallel/test-tls-dhe.js +test/js/node/test/parallel/test-tls-ecdh-auto.js +test/js/node/test/parallel/test-tls-ecdh-multiple.js +test/js/node/test/parallel/test-tls-ecdh.js +test/js/node/test/parallel/test-tls-econnreset.js +test/js/node/test/parallel/test-tls-env-extra-ca-no-crypto.js +test/js/node/test/parallel/test-tls-fast-writing.js +test/js/node/test/parallel/test-tls-friendly-error-message.js +test/js/node/test/parallel/test-tls-get-ca-certificates-bundled-subset.js +test/js/node/test/parallel/test-tls-get-ca-certificates-bundled.js +test/js/node/test/parallel/test-tls-get-ca-certificates-default.js +test/js/node/test/parallel/test-tls-get-ca-certificates-error.js +test/js/node/test/parallel/test-tls-get-ca-certificates-extra-empty.js +test/js/node/test/parallel/test-tls-get-ca-certificates-extra-subset.js +test/js/node/test/parallel/test-tls-get-ca-certificates-extra.js +test/js/node/test/parallel/test-tls-handshake-error.js +test/js/node/test/parallel/test-tls-inception.js +test/js/node/test/parallel/test-tls-interleave.js +test/js/node/test/parallel/test-tls-invoke-queued.js +test/js/node/test/parallel/test-tls-junk-closes-server.js +test/js/node/test/parallel/test-tls-keyengine-invalid-arg-type.js +test/js/node/test/parallel/test-tls-legacy-pfx.js +test/js/node/test/parallel/test-tls-multiple-cas-as-string.js +test/js/node/test/parallel/test-tls-net-connect-prefer-path.js +test/js/node/test/parallel/test-tls-no-rsa-key.js +test/js/node/test/parallel/test-tls-no-sslv3.js +test/js/node/test/parallel/test-tls-ocsp-callback.js +test/js/node/test/parallel/test-tls-on-empty-socket.js +test/js/node/test/parallel/test-tls-options-boolean-check.js +test/js/node/test/parallel/test-tls-peer-certificate-encoding.js +test/js/node/test/parallel/test-tls-peer-certificate-multi-keys.js +test/js/node/test/parallel/test-tls-psk-server.js +test/js/node/test/parallel/test-tls-request-timeout.js +test/js/node/test/parallel/test-tls-reuse-host-from-socket.js +test/js/node/test/parallel/test-tls-root-certificates.js +test/js/node/test/parallel/test-tls-secure-context-usage-order.js +test/js/node/test/parallel/test-tls-securepair-server.js +test/js/node/test/parallel/test-tls-server-connection-server.js +test/js/node/test/parallel/test-tls-server-verify.js +test/js/node/test/parallel/test-tls-session-cache.js +test/js/node/test/parallel/test-tls-set-ciphers-error.js +test/js/node/test/parallel/test-tls-set-ciphers.js +test/js/node/test/parallel/test-tls-set-encoding.js +test/js/node/test/parallel/test-tls-sni-server-client.js +test/js/node/test/parallel/test-tls-socket-allow-half-open-option.js +test/js/node/test/parallel/test-tls-startcom-wosign-whitelist.js +test/js/node/test/parallel/test-tls-timeout-server-2.js +test/js/node/test/parallel/test-tls-tlswrap-segfault-2.js +test/js/node/test/parallel/test-tls-tlswrap-segfault.js +test/js/node/test/parallel/test-tls-translate-peer-certificate.js +test/js/node/test/parallel/test-tls-transport-destroy-after-own-gc.js +test/js/node/test/parallel/test-tls-use-after-free-regression.js +test/js/node/test/parallel/test-tls-wrap-econnreset-localaddress.js +test/js/node/test/parallel/test-tls-wrap-econnreset-socket.js +test/js/node/test/parallel/test-tls-wrap-econnreset.js +test/js/node/test/parallel/test-tls-write-error.js +test/js/node/test/parallel/test-tls-zero-clear-in.js +test/js/node/test/parallel/test-tty-backwards-api.js +test/js/node/test/parallel/test-tty-stdin-end.js +test/js/node/test/parallel/test-tty-stdin-pipe.js +test/js/node/test/parallel/test-tz-version.js +test/js/node/test/parallel/test-unhandled-exception-with-worker-inuse.js +test/js/node/test/parallel/test-url-canParse-whatwg.js +test/js/node/test/parallel/test-url-domain-ascii-unicode.js +test/js/node/test/parallel/test-url-format-invalid-input.js +test/js/node/test/parallel/test-url-format-whatwg.js +test/js/node/test/parallel/test-url-format.js +test/js/node/test/parallel/test-url-parse-format.js +test/js/node/test/parallel/test-url-parse-invalid-input.js +test/js/node/test/parallel/test-url-parse-query.js +test/js/node/test/parallel/test-url-relative.js +test/js/node/test/parallel/test-url-revokeobjecturl.js +test/js/node/test/parallel/test-url-urltooptions.js +test/js/node/test/parallel/test-utf8-scripts.js +test/js/node/test/parallel/test-util-callbackify.js +test/js/node/test/parallel/test-util-deprecate-invalid-code.js +test/js/node/test/parallel/test-util-deprecate.js +test/js/node/test/parallel/test-util-inherits.js +test/js/node/test/parallel/test-util-inspect-getters-accessing-this.js +test/js/node/test/parallel/test-util-inspect-long-running.js +test/js/node/test/parallel/test-util-inspect-proxy.js +test/js/node/test/parallel/test-util-internal.js +test/js/node/test/parallel/test-util-parse-env.js +test/js/node/test/parallel/test-util-primordial-monkeypatching.js +test/js/node/test/parallel/test-util-promisify-custom-names.mjs +test/js/node/test/parallel/test-util-promisify.js +test/js/node/test/parallel/test-util-sigint-watchdog.js +test/js/node/test/parallel/test-util-sleep.js +test/js/node/test/parallel/test-util-stripvtcontrolcharacters.js +test/js/node/test/parallel/test-util-styletext.js +test/js/node/test/parallel/test-util-text-decoder.js +test/js/node/test/parallel/test-util-types-exists.js +test/js/node/test/parallel/test-util-types.js +test/js/node/test/parallel/test-util.js +test/js/node/test/parallel/test-v8-deserialize-buffer.js +test/js/node/test/parallel/test-v8-flag-pool-size-0.js +test/js/node/test/parallel/test-v8-getheapsnapshot-twice.js +test/js/node/test/parallel/test-v8-global-setter.js +test/js/node/test/parallel/test-v8-serialize-leak.js +test/js/node/test/parallel/test-vm-access-process-env.js +test/js/node/test/parallel/test-vm-api-handles-getter-errors.js +test/js/node/test/parallel/test-vm-attributes-property-not-on-sandbox.js +test/js/node/test/parallel/test-vm-basic.js +test/js/node/test/parallel/test-vm-cached-data.js +test/js/node/test/parallel/test-vm-context-async-script.js +test/js/node/test/parallel/test-vm-context-property-forwarding.js +test/js/node/test/parallel/test-vm-context.js +test/js/node/test/parallel/test-vm-create-and-run-in-context.js +test/js/node/test/parallel/test-vm-create-context-accessors.js +test/js/node/test/parallel/test-vm-create-context-arg.js +test/js/node/test/parallel/test-vm-create-context-circular-reference.js +test/js/node/test/parallel/test-vm-createcacheddata.js +test/js/node/test/parallel/test-vm-cross-context.js +test/js/node/test/parallel/test-vm-data-property-writable.js +test/js/node/test/parallel/test-vm-deleting-property.js +test/js/node/test/parallel/test-vm-function-declaration.js +test/js/node/test/parallel/test-vm-function-redefinition.js +test/js/node/test/parallel/test-vm-getters.js +test/js/node/test/parallel/test-vm-global-assignment.js +test/js/node/test/parallel/test-vm-global-configurable-properties.js +test/js/node/test/parallel/test-vm-global-define-property.js +test/js/node/test/parallel/test-vm-global-get-own.js +test/js/node/test/parallel/test-vm-global-non-writable-properties.js +test/js/node/test/parallel/test-vm-global-property-enumerator.js +test/js/node/test/parallel/test-vm-global-property-interceptors.js +test/js/node/test/parallel/test-vm-global-property-prototype.js +test/js/node/test/parallel/test-vm-global-setter.js +test/js/node/test/parallel/test-vm-harmony-symbols.js +test/js/node/test/parallel/test-vm-indexed-properties.js +test/js/node/test/parallel/test-vm-inherited_properties.js +test/js/node/test/parallel/test-vm-is-context.js +test/js/node/test/parallel/test-vm-low-stack-space.js +test/js/node/test/parallel/test-vm-module-basic.js +test/js/node/test/parallel/test-vm-module-cached-data.js +test/js/node/test/parallel/test-vm-module-errors.js +test/js/node/test/parallel/test-vm-module-import-meta.js +test/js/node/test/parallel/test-vm-module-link.js +test/js/node/test/parallel/test-vm-module-reevaluate.js +test/js/node/test/parallel/test-vm-module-synthetic.js +test/js/node/test/parallel/test-vm-new-script-context.js +test/js/node/test/parallel/test-vm-new-script-new-context.js +test/js/node/test/parallel/test-vm-new-script-this-context.js +test/js/node/test/parallel/test-vm-not-strict.js +test/js/node/test/parallel/test-vm-options-validation.js +test/js/node/test/parallel/test-vm-ownkeys.js +test/js/node/test/parallel/test-vm-ownpropertynames.js +test/js/node/test/parallel/test-vm-ownpropertysymbols.js +test/js/node/test/parallel/test-vm-parse-abort-on-uncaught-exception.js +test/js/node/test/parallel/test-vm-preserves-property.js +test/js/node/test/parallel/test-vm-proxies.js +test/js/node/test/parallel/test-vm-proxy-failure-CP.js +test/js/node/test/parallel/test-vm-run-in-new-context.js +test/js/node/test/parallel/test-vm-script-throw-in-tostring.js +test/js/node/test/parallel/test-vm-set-property-proxy.js +test/js/node/test/parallel/test-vm-set-proto-null-on-globalthis.js +test/js/node/test/parallel/test-vm-sigint-existing-handler.js +test/js/node/test/parallel/test-vm-sigint.js +test/js/node/test/parallel/test-vm-static-this.js +test/js/node/test/parallel/test-vm-strict-assign.js +test/js/node/test/parallel/test-vm-strict-mode.js +test/js/node/test/parallel/test-vm-symbols.js +test/js/node/test/parallel/test-vm-syntax-error-message.js +test/js/node/test/parallel/test-vm-syntax-error-stderr.js +test/js/node/test/parallel/test-vm-timeout-escape-promise-module.js +test/js/node/test/parallel/test-vm-timeout-escape-promise.js +test/js/node/test/parallel/test-vm-timeout.js +test/js/node/test/parallel/test-vm-util-lazy-properties.js +test/js/node/test/parallel/test-warn-stream-wrap.js +test/js/node/test/parallel/test-weakref.js +test/js/node/test/parallel/test-webcrypto-cryptokey-workers.js +test/js/node/test/parallel/test-webcrypto-derivekey.js +test/js/node/test/parallel/test-webcrypto-digest.js +test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +test/js/node/test/parallel/test-webcrypto-encrypt-decrypt.js +test/js/node/test/parallel/test-webcrypto-getRandomValues.js +test/js/node/test/parallel/test-webcrypto-random.js +test/js/node/test/parallel/test-webcrypto-sign-verify.js +test/js/node/test/parallel/test-websocket.js +test/js/node/test/parallel/test-webstream-string-tag.js +test/js/node/test/parallel/test-whatwg-encoding-custom-api-basics.js +test/js/node/test/parallel/test-whatwg-encoding-custom-fatal-streaming.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-api-invalid-label.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-fatal.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-ignorebom.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-invalid-arg.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-streaming.js +test/js/node/test/parallel/test-whatwg-encoding-custom-textdecoder-utf16-surrogates.js +test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-passive.js +test/js/node/test/parallel/test-whatwg-events-add-event-listener-options-signal.js +test/js/node/test/parallel/test-whatwg-events-customevent.js +test/js/node/test/parallel/test-whatwg-events-event-constructors.js +test/js/node/test/parallel/test-whatwg-events-eventtarget-this-of-listener.js +test/js/node/test/parallel/test-whatwg-readablebytestream.js +test/js/node/test/parallel/test-whatwg-readablebytestreambyob.js +test/js/node/test/parallel/test-whatwg-readablestream.mjs +test/js/node/test/parallel/test-whatwg-url-canparse.js +test/js/node/test/parallel/test-whatwg-url-custom-deepequal.js +test/js/node/test/parallel/test-whatwg-url-custom-domainto.js +test/js/node/test/parallel/test-whatwg-url-custom-global.js +test/js/node/test/parallel/test-whatwg-url-custom-href-side-effect.js +test/js/node/test/parallel/test-whatwg-url-custom-inspect.js +test/js/node/test/parallel/test-whatwg-url-custom-parsing.js +test/js/node/test/parallel/test-whatwg-url-custom-properties.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-append.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-delete.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-entries.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-foreach.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-get.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-getall.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-has.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-keys.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-set.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-sort.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-stringifier.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams-values.js +test/js/node/test/parallel/test-whatwg-url-custom-searchparams.js +test/js/node/test/parallel/test-whatwg-url-custom-setters.js +test/js/node/test/parallel/test-whatwg-url-custom-tostringtag.js +test/js/node/test/parallel/test-whatwg-url-invalidthis.js +test/js/node/test/parallel/test-whatwg-url-override-hostname.js +test/js/node/test/parallel/test-whatwg-url-toascii.js +test/js/node/test/parallel/test-windows-abort-exitcode.js +test/js/node/test/parallel/test-windows-failed-heap-allocation.js +test/js/node/test/parallel/test-worker-abort-on-uncaught-exception.js +test/js/node/test/parallel/test-worker-arraybuffer-zerofill.js +test/js/node/test/parallel/test-worker-cjs-workerdata.js +test/js/node/test/parallel/test-worker-cleanexit-with-js.js +test/js/node/test/parallel/test-worker-cleanexit-with-moduleload.js +test/js/node/test/parallel/test-worker-console-listeners.js +test/js/node/test/parallel/test-worker-dns-terminate-during-query.js +test/js/node/test/parallel/test-worker-environmentdata.js +test/js/node/test/parallel/test-worker-esm-exit.js +test/js/node/test/parallel/test-worker-esm-missing-main.js +test/js/node/test/parallel/test-worker-esmodule.js +test/js/node/test/parallel/test-worker-event.js +test/js/node/test/parallel/test-worker-exit-event-error.js +test/js/node/test/parallel/test-worker-exit-from-uncaught-exception.js +test/js/node/test/parallel/test-worker-exit-heapsnapshot.js +test/js/node/test/parallel/test-worker-fs-stat-watcher.js +test/js/node/test/parallel/test-worker-heap-snapshot.js +test/js/node/test/parallel/test-worker-http2-generic-streams-terminate.js +test/js/node/test/parallel/test-worker-invalid-workerdata.js +test/js/node/test/parallel/test-worker-load-file-with-extension-other-than-js.js +test/js/node/test/parallel/test-worker-memory.js +test/js/node/test/parallel/test-worker-message-channel-sharedarraybuffer.js +test/js/node/test/parallel/test-worker-message-event.js +test/js/node/test/parallel/test-worker-message-port-constructor.js +test/js/node/test/parallel/test-worker-message-port-infinite-message-loop.js +test/js/node/test/parallel/test-worker-message-port-receive-message.js +test/js/node/test/parallel/test-worker-message-port-terminate-transfer-list.js +test/js/node/test/parallel/test-worker-message-port-transfer-duplicate.js +test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js +test/js/node/test/parallel/test-worker-message-port-wasm-module.js +test/js/node/test/parallel/test-worker-message-port-wasm-threads.js +test/js/node/test/parallel/test-worker-mjs-workerdata.js +test/js/node/test/parallel/test-worker-nested-on-process-exit.js +test/js/node/test/parallel/test-worker-nested-uncaught.js +test/js/node/test/parallel/test-worker-no-sab.js +test/js/node/test/parallel/test-worker-non-fatal-uncaught-exception.js +test/js/node/test/parallel/test-worker-on-process-exit.js +test/js/node/test/parallel/test-worker-onmessage-not-a-function.js +test/js/node/test/parallel/test-worker-onmessage.js +test/js/node/test/parallel/test-worker-parent-port-ref.js +test/js/node/test/parallel/test-worker-process-argv.js +test/js/node/test/parallel/test-worker-ref-onexit.js +test/js/node/test/parallel/test-worker-ref.js +test/js/node/test/parallel/test-worker-relative-path-double-dot.js +test/js/node/test/parallel/test-worker-relative-path.js +test/js/node/test/parallel/test-worker-safe-getters.js +test/js/node/test/parallel/test-worker-sharedarraybuffer-from-worker-thread.js +test/js/node/test/parallel/test-worker-terminate-http2-respond-with-file.js +test/js/node/test/parallel/test-worker-terminate-nested.js +test/js/node/test/parallel/test-worker-terminate-null-handler.js +test/js/node/test/parallel/test-worker-terminate-timers.js +test/js/node/test/parallel/test-worker-type-check.js +test/js/node/test/parallel/test-worker-workerdata-sharedarraybuffer.js +test/js/node/test/parallel/test-worker.js +test/js/node/test/parallel/test-worker.mjs +test/js/node/test/parallel/test-zlib-brotli-16GB.js +test/js/node/test/parallel/test-zlib-brotli-flush.js +test/js/node/test/parallel/test-zlib-brotli-from-brotli.js +test/js/node/test/parallel/test-zlib-brotli-from-string.js +test/js/node/test/parallel/test-zlib-brotli-kmaxlength-rangeerror.js +test/js/node/test/parallel/test-zlib-brotli.js +test/js/node/test/parallel/test-zlib-close-after-error.js +test/js/node/test/parallel/test-zlib-close-after-write.js +test/js/node/test/parallel/test-zlib-close-in-ondata.js +test/js/node/test/parallel/test-zlib-const.js +test/js/node/test/parallel/test-zlib-convenience-methods.js +test/js/node/test/parallel/test-zlib-crc32.js +test/js/node/test/parallel/test-zlib-create-raw.js +test/js/node/test/parallel/test-zlib-deflate-constructors.js +test/js/node/test/parallel/test-zlib-deflate-raw-inherits.js +test/js/node/test/parallel/test-zlib-destroy-pipe.js +test/js/node/test/parallel/test-zlib-destroy.js +test/js/node/test/parallel/test-zlib-dictionary-fail.js +test/js/node/test/parallel/test-zlib-dictionary.js +test/js/node/test/parallel/test-zlib-empty-buffer.js +test/js/node/test/parallel/test-zlib-failed-init.js +test/js/node/test/parallel/test-zlib-flush-drain-longblock.js +test/js/node/test/parallel/test-zlib-flush-drain.js +test/js/node/test/parallel/test-zlib-flush-flags.js +test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js +test/js/node/test/parallel/test-zlib-flush.js +test/js/node/test/parallel/test-zlib-from-concatenated-gzip.js +test/js/node/test/parallel/test-zlib-from-gzip-with-trailing-garbage.js +test/js/node/test/parallel/test-zlib-from-gzip.js +test/js/node/test/parallel/test-zlib-from-string.js +test/js/node/test/parallel/test-zlib-invalid-arg-value-brotli-compress.js +test/js/node/test/parallel/test-zlib-invalid-input.js +test/js/node/test/parallel/test-zlib-kmaxlength-rangeerror.js +test/js/node/test/parallel/test-zlib-maxOutputLength.js +test/js/node/test/parallel/test-zlib-not-string-or-buffer.js +test/js/node/test/parallel/test-zlib-object-write.js +test/js/node/test/parallel/test-zlib-params.js +test/js/node/test/parallel/test-zlib-premature-end.js +test/js/node/test/parallel/test-zlib-random-byte-pipes.js +test/js/node/test/parallel/test-zlib-reset-before-write.js +test/js/node/test/parallel/test-zlib-sync-no-event.js +test/js/node/test/parallel/test-zlib-truncated.js +test/js/node/test/parallel/test-zlib-unzip-one-byte-chunks.js +test/js/node/test/parallel/test-zlib-write-after-close.js +test/js/node/test/parallel/test-zlib-write-after-end.js +test/js/node/test/parallel/test-zlib-write-after-flush.js +test/js/node/test/parallel/test-zlib-zero-byte.js +test/js/node/test/parallel/test-zlib-zero-windowBits.js +test/js/node/test/parallel/test-zlib-zstd-flush.js +test/js/node/test/parallel/test-zlib-zstd-from-string.js +test/js/node/test/parallel/test-zlib-zstd-from-zstd.js +test/js/node/test/parallel/test-zlib-zstd-kmaxlength-rangeerror.js +test/js/node/test/parallel/test-zlib-zstd-pledged-src-size.js +test/js/node/test/parallel/test-zlib-zstd.js +test/js/node/test/parallel/test-zlib.js +test/js/node/test/sequential/test-buffer-creation-regression.js +test/js/node/test/sequential/test-child-process-emfile.js +test/js/node/test/sequential/test-child-process-execsync.js +test/js/node/test/sequential/test-child-process-exit.js +test/js/node/test/sequential/test-crypto-timing-safe-equal.js +test/js/node/test/sequential/test-debug-prompt.js +test/js/node/test/sequential/test-dgram-implicit-bind-failure.js +test/js/node/test/sequential/test-dgram-pingpong.js +test/js/node/test/sequential/test-fs-opendir-recursive.js +test/js/node/test/sequential/test-fs-readdir-recursive.js +test/js/node/test/sequential/test-fs-stat-sync-overflow.js +test/js/node/test/sequential/test-http-econnrefused.js +test/js/node/test/sequential/test-http-keep-alive-large-write.js +test/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.js +test/js/node/test/sequential/test-http2-large-file.js +test/js/node/test/sequential/test-init.js +test/js/node/test/sequential/test-net-better-error-messages-port.js +test/js/node/test/sequential/test-net-connect-econnrefused.js +test/js/node/test/sequential/test-net-connect-handle-econnrefused.js +test/js/node/test/sequential/test-net-GH-5504.js +test/js/node/test/sequential/test-net-reconnect-error.js +test/js/node/test/sequential/test-net-response-size.js +test/js/node/test/sequential/test-net-server-address.js +test/js/node/test/sequential/test-net-server-bind.js +test/js/node/test/sequential/test-require-cache-without-stat.js +test/js/node/test/sequential/test-single-executable-application-assets-raw.js +test/js/node/test/sequential/test-single-executable-application-assets.js +test/js/node/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js +test/js/node/test/sequential/test-single-executable-application-empty.js +test/js/node/test/sequential/test-single-executable-application-snapshot-and-code-cache.js +test/js/node/test/sequential/test-single-executable-application-snapshot-worker.js +test/js/node/test/sequential/test-single-executable-application-snapshot.js +test/js/node/test/sequential/test-single-executable-application-use-code-cache.js +test/js/node/test/sequential/test-single-executable-application.js +test/js/node/test/sequential/test-stream2-fs.js +test/js/node/test/sequential/test-timers-block-eventloop.js +test/js/node/test/sequential/test-timers-set-interval-excludes-callback-duration.js +test/js/node/test/sequential/test-tls-connect.js +test/js/node/test/sequential/test-tls-lookup.js +test/js/node/test/sequential/test-tls-psk-client.js +test/js/node/test/sequential/test-tls-securepair-client.js +test/js/node/timers.promises/timers.promises.test.ts +test/js/node/timers/node-timers.test.ts +test/js/node/tls/fetch-tls-cert.test.ts +test/js/node/tls/node-tls-cert.test.ts +test/js/node/tls/node-tls-connect.test.ts +test/js/node/tls/node-tls-context.test.ts +test/js/node/tls/node-tls-create-secure-context-args.test.ts +test/js/node/tls/node-tls-no-cipher-match-error.test.ts +test/js/node/tls/node-tls-server.test.ts +test/js/node/tls/node-tls-upgrade.test.ts +test/js/node/tls/renegotiation.test.ts +test/js/node/url/url-parse-format.test.js +test/js/node/url/url-parse-ipv6.test.ts +test/js/node/url/url-relative.test.js +test/js/node/url/url.test.ts +test/js/node/util/bun-inspect.test.ts +test/js/node/util/custom-inspect.test.js +test/js/node/util/mime-api.test.ts +test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js +test/js/node/util/parse_args/default-args.test.mjs +test/js/node/util/test-util-types.test.js +test/js/node/util/util-callbackify.test.js +test/js/node/util/util-promisify.test.js +test/js/node/util/util.test.js +test/js/node/v8/capture-stack-trace.test.js +test/js/node/vm/happy-dom-vm-16277.test.ts +test/js/node/vm/sourcetextmodule-leak.test.ts +test/js/node/vm/vm-sourceUrl.test.ts +test/js/node/vm/vm.test.ts +test/js/node/watch/fs.watchFile.test.ts +test/js/node/worker_threads/15787.test.ts +test/js/node/zlib/zlib.kMaxLength.global.test.js +test/js/node/zlib/zlib.test.js +test/js/sql/local-sql.test.ts +test/js/sql/sql.test.ts +test/js/third_party/@azure/service-bus/azure-service-bus.test.ts +test/js/third_party/@duckdb/node-api/duckdb.test.ts +test/js/third_party/@fastify/websocket/fastity-test-websocket.test.js +test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts +test/js/third_party/body-parser/express-body-parser-test.test.ts +test/js/third_party/body-parser/express-bun-build-compile.test.ts +test/js/third_party/body-parser/express-memory-leak.test.ts +test/js/third_party/comlink/comlink.test.ts +test/js/third_party/duckdb/duckdb-basic-usage.test.ts +test/js/third_party/esbuild/esbuild-child_process.test.ts +test/js/third_party/express/app.router.test.ts +test/js/third_party/express/express.json.test.ts +test/js/third_party/express/express.test.ts +test/js/third_party/express/express.text.test.ts +test/js/third_party/express/res.json.test.ts +test/js/third_party/express/res.location.test.ts +test/js/third_party/express/res.redirect.test.ts +test/js/third_party/express/res.send.test.ts +test/js/third_party/express/res.sendFile.test.ts +test/js/third_party/grpc-js/test-call-credentials.test.ts +test/js/third_party/grpc-js/test-call-propagation.test.ts +test/js/third_party/grpc-js/test-certificate-provider.test.ts +test/js/third_party/grpc-js/test-channel-credentials.test.ts +test/js/third_party/grpc-js/test-channelz.test.ts +test/js/third_party/grpc-js/test-client.test.ts +test/js/third_party/grpc-js/test-confg-parsing.test.ts +test/js/third_party/grpc-js/test-deadline.test.ts +test/js/third_party/grpc-js/test-duration.test.ts +test/js/third_party/grpc-js/test-end-to-end.test.ts +test/js/third_party/grpc-js/test-global-subchannel-pool.test.ts +test/js/third_party/grpc-js/test-idle-timer.test.ts +test/js/third_party/grpc-js/test-local-subchannel-pool.test.ts +test/js/third_party/grpc-js/test-logging.test.ts +test/js/third_party/grpc-js/test-metadata.test.ts +test/js/third_party/grpc-js/test-outlier-detection.test.ts +test/js/third_party/grpc-js/test-pick-first.test.ts +test/js/third_party/grpc-js/test-prototype-pollution.test.ts +test/js/third_party/grpc-js/test-resolver.test.ts +test/js/third_party/grpc-js/test-retry-config.test.ts +test/js/third_party/grpc-js/test-retry.test.ts +test/js/third_party/grpc-js/test-server-credentials.test.ts +test/js/third_party/grpc-js/test-server-deadlines.test.ts +test/js/third_party/grpc-js/test-server-errors.test.ts +test/js/third_party/grpc-js/test-server-interceptors.test.ts +test/js/third_party/grpc-js/test-server.test.ts +test/js/third_party/grpc-js/test-status-builder.test.ts +test/js/third_party/grpc-js/test-uri-parser.test.ts +test/js/third_party/http2-wrapper/http2-wrapper.test.ts +test/js/third_party/jsonwebtoken/async_sign.test.js +test/js/third_party/jsonwebtoken/buffer.test.js +test/js/third_party/jsonwebtoken/claim-aud.test.js +test/js/third_party/jsonwebtoken/claim-exp.test.js +test/js/third_party/jsonwebtoken/claim-iat.test.js +test/js/third_party/jsonwebtoken/claim-iss.test.js +test/js/third_party/jsonwebtoken/claim-jti.test.js +test/js/third_party/jsonwebtoken/claim-nbf.test.js +test/js/third_party/jsonwebtoken/claim-private.test.js +test/js/third_party/jsonwebtoken/claim-sub.test.js +test/js/third_party/jsonwebtoken/decoding.test.js +test/js/third_party/jsonwebtoken/encoding.test.js +test/js/third_party/jsonwebtoken/expires_format.test.js +test/js/third_party/jsonwebtoken/header-kid.test.js +test/js/third_party/jsonwebtoken/invalid_exp.test.js +test/js/third_party/jsonwebtoken/issue_147.test.js +test/js/third_party/jsonwebtoken/issue_304.test.js +test/js/third_party/jsonwebtoken/issue_70.test.js +test/js/third_party/jsonwebtoken/jwt.asymmetric_signing.test.js +test/js/third_party/jsonwebtoken/jwt.hs.test.js +test/js/third_party/jsonwebtoken/jwt.malicious.test.js +test/js/third_party/jsonwebtoken/non_object_values.test.js +test/js/third_party/jsonwebtoken/noTimestamp.test.js +test/js/third_party/jsonwebtoken/option-complete.test.js +test/js/third_party/jsonwebtoken/option-maxAge.test.js +test/js/third_party/jsonwebtoken/option-nonce.test.js +test/js/third_party/jsonwebtoken/rsa-public-key.test.js +test/js/third_party/jsonwebtoken/schema.test.js +test/js/third_party/jsonwebtoken/set_headers.test.js +test/js/third_party/jsonwebtoken/undefined_secretOrPublickey.test.js +test/js/third_party/jsonwebtoken/validateAsymmetricKey.test.js +test/js/third_party/jsonwebtoken/verify.test.js +test/js/third_party/jsonwebtoken/wrong_alg.test.js +test/js/third_party/mongodb/mongodb.test.ts +test/js/third_party/msw/msw.test.ts +test/js/third_party/nodemailer/nodemailer.test.ts +test/js/third_party/pg-gateway/pglite.test.ts +test/js/third_party/pg/pg.test.ts +test/js/third_party/pino/pino.test.js +test/js/third_party/postgres/postgres.test.ts +test/js/third_party/prisma/prisma.test.ts +test/js/third_party/prompts/prompts.test.ts +test/js/third_party/remix/remix.test.ts +test/js/third_party/resvg/bbox.test.js +test/js/third_party/rollup-v4/rollup-v4.test.ts +test/js/third_party/socket.io/socket.io-close.test.ts +test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts +test/js/third_party/socket.io/socket.io-handshake.test.ts +test/js/third_party/socket.io/socket.io-messaging-many.test.ts +test/js/third_party/socket.io/socket.io-middleware.test.ts +test/js/third_party/socket.io/socket.io-namespaces.test.ts +test/js/third_party/socket.io/socket.io-server-attachment.test.ts +test/js/third_party/socket.io/socket.io-socket-middleware.test.ts +test/js/third_party/socket.io/socket.io-socket-timeout.test.ts +test/js/third_party/socket.io/socket.io-utility-methods.test.ts +test/js/third_party/socket.io/socket.io.test.ts +test/js/third_party/solc/solc.test.ts +test/js/third_party/st/st.test.ts +test/js/third_party/stripe/stripe.test.ts +test/js/third_party/svelte/svelte.test.ts +test/js/web/abort/abort.test.ts +test/js/web/broadcastchannel/broadcast-channel.test.ts +test/js/web/console/console-timeLog.test.ts +test/js/web/crypto/web-crypto.test.ts +test/js/web/encoding/encode-bad-chunks.test.ts +test/js/web/encoding/text-decoder-stream.test.ts +test/js/web/encoding/text-decoder.test.js +test/js/web/encoding/text-encoder-stream.test.ts +test/js/web/encoding/text-encoder.test.js +test/js/web/fetch/abort-signal-leak.test.ts +test/js/web/fetch/blob-oom.test.ts +test/js/web/fetch/blob.test.ts +test/js/web/fetch/body-clone.test.ts +test/js/web/fetch/body-stream-excess.test.ts +test/js/web/fetch/body-stream.test.ts +test/js/web/fetch/body.test.ts +test/js/web/fetch/chunked-trailing.test.js +test/js/web/fetch/client-fetch.test.ts +test/js/web/fetch/content-length.test.js +test/js/web/fetch/cookies.test.ts +test/js/web/fetch/fetch_headers.test.js +test/js/web/fetch/fetch-args.test.ts +test/js/web/fetch/fetch-gzip.test.ts +test/js/web/fetch/fetch-preconnect.test.ts +test/js/web/fetch/fetch-redirect.test.ts +test/js/web/fetch/fetch-tcp-stress.test.ts +test/js/web/fetch/fetch-url-after-redirect.test.ts +test/js/web/fetch/fetch.brotli.test.ts +test/js/web/fetch/fetch.stream.test.ts +test/js/web/fetch/fetch.test.ts +test/js/web/fetch/fetch.tls.test.ts +test/js/web/fetch/fetch.unix.test.ts +test/js/web/fetch/headers.test.ts +test/js/web/fetch/headers.undici.test.ts +test/js/web/fetch/stream-fast-path.test.ts +test/js/web/fetch/utf8-bom.test.ts +test/js/web/html/FormData.test.ts +test/js/web/request/request-clone-leak.test.ts +test/js/web/request/request-subclass.test.ts +test/js/web/request/request.test.ts +test/js/web/streams/streams.test.js +test/js/web/timers/microtask.test.js +test/js/web/timers/setInterval.test.js +test/js/web/timers/setTimeout.test.js +test/js/web/websocket/websocket-client-short-read.test.ts +test/js/web/websocket/websocket-client.test.ts +test/js/web/websocket/websocket.test.js +test/js/web/workers/message-channel.test.ts +test/js/web/workers/message-event.test.ts +test/js/web/workers/structured-clone.test.ts +test/js/web/workers/worker_blob.test.ts +test/js/web/workers/worker.test.ts +test/js/workerd/html-rewriter.test.js +test/napi/node-napi.test.ts +test/napi/uv_stub.test.ts +test/napi/uv.test.ts +test/regression/issue/012040.test.ts +test/regression/issue/014187.test.ts +test/regression/issue/01466.test.ts +test/regression/issue/014865.test.ts +test/regression/issue/02368.test.ts +test/regression/issue/02499/02499.test.ts +test/regression/issue/04298/04298.test.ts +test/regression/issue/04947.test.js +test/regression/issue/06946/06946.test.ts +test/regression/issue/07001.test.ts +test/regression/issue/07261.test.ts +test/regression/issue/07827.test.ts +test/regression/issue/07917/7917.test.ts +test/regression/issue/08093.test.ts +test/regression/issue/08794.test.ts +test/regression/issue/09041.test.ts +test/regression/issue/09340.test.ts +test/regression/issue/09469.test.ts +test/regression/issue/09555.test.ts +test/regression/issue/09559.test.ts +test/regression/issue/09778.test.ts +test/regression/issue/10132.test.ts +test/regression/issue/10139.test.ts +test/regression/issue/10170.test.ts +test/regression/issue/11297/11297.test.ts +test/regression/issue/11664.test.ts +test/regression/issue/12910/12910.test.ts +test/regression/issue/14477/14477.test.ts +test/regression/issue/14515.test.tsx +test/regression/issue/14976/14976.test.ts +test/regression/issue/14982/14982.test.ts +test/regression/issue/16312.test.ts +test/regression/issue/16474.test.ts +test/regression/issue/17605.test.ts +test/regression/issue/17766.test.ts +test/regression/issue/18159/18159.test.ts +test/regression/issue/18239/18239.test.ts +test/regression/issue/18547.test.ts +test/regression/issue/18595.test.ts +test/regression/issue/19661.test.ts +test/regression/issue/20144/20144.test.ts +test/regression/issue/crypto-names.test.ts +test/v8/v8.test.ts +vendor/elysia/test/a.test.ts +vendor/elysia/test/adapter/web-standard/cookie-to-header.test.ts +vendor/elysia/test/adapter/web-standard/map-compact-response.test.ts +vendor/elysia/test/adapter/web-standard/map-early-response.test.ts +vendor/elysia/test/adapter/web-standard/map-response.test.ts +vendor/elysia/test/adapter/web-standard/set-cookie.test.ts +vendor/elysia/test/aot/analysis.test.ts +vendor/elysia/test/aot/generation.test.ts +vendor/elysia/test/aot/has-transform.test.ts +vendor/elysia/test/aot/has-type.test.ts +vendor/elysia/test/aot/response.test.ts +vendor/elysia/test/bun/router.test.ts +vendor/elysia/test/cookie/explicit.test.ts +vendor/elysia/test/cookie/implicit.test.ts +vendor/elysia/test/cookie/response.test.ts +vendor/elysia/test/cookie/signature.test.ts +vendor/elysia/test/core/as.test.ts +vendor/elysia/test/core/config.test.ts +vendor/elysia/test/core/context.test.ts +vendor/elysia/test/core/dynamic.test.ts +vendor/elysia/test/core/elysia.test.ts +vendor/elysia/test/core/formdata.test.ts +vendor/elysia/test/core/handle-error.test.ts +vendor/elysia/test/core/modules.test.ts +vendor/elysia/test/core/mount.test.ts +vendor/elysia/test/core/native-static.test.ts +vendor/elysia/test/core/normalize.test.ts +vendor/elysia/test/core/path.test.ts +vendor/elysia/test/core/redirect.test.ts +vendor/elysia/test/core/sanitize.test.ts +vendor/elysia/test/core/stop.test.ts +vendor/elysia/test/extends/decorators.test.ts +vendor/elysia/test/extends/error.test.ts +vendor/elysia/test/extends/models.test.ts +vendor/elysia/test/extends/store.test.ts +vendor/elysia/test/hoc/index.test.ts +vendor/elysia/test/lifecycle/after-handle.test.ts +vendor/elysia/test/lifecycle/before-handle.test.ts +vendor/elysia/test/lifecycle/derive.test.ts +vendor/elysia/test/lifecycle/error.test.ts +vendor/elysia/test/lifecycle/hook-types.test.ts +vendor/elysia/test/lifecycle/map-derive.test.ts +vendor/elysia/test/lifecycle/map-resolve.test.ts +vendor/elysia/test/lifecycle/map-response.test.ts +vendor/elysia/test/lifecycle/parser.test.ts +vendor/elysia/test/lifecycle/request.test.ts +vendor/elysia/test/lifecycle/resolve.test.ts +vendor/elysia/test/lifecycle/response.test.ts +vendor/elysia/test/lifecycle/transform.test.ts +vendor/elysia/test/macro/macro.test.ts +vendor/elysia/test/path/group.test.ts +vendor/elysia/test/path/guard.test.ts +vendor/elysia/test/path/path.test.ts +vendor/elysia/test/plugins/affix.test.ts +vendor/elysia/test/plugins/checksum.test.ts +vendor/elysia/test/plugins/error-propagation.test.ts +vendor/elysia/test/plugins/plugin.test.ts +vendor/elysia/test/production/index.test.ts +vendor/elysia/test/response/custom-response.test.ts +vendor/elysia/test/response/headers.test.ts +vendor/elysia/test/response/redirect.test.ts +vendor/elysia/test/response/static.test.ts +vendor/elysia/test/response/stream.test.ts +vendor/elysia/test/sucrose/query.test.ts +vendor/elysia/test/sucrose/sucrose.test.ts +vendor/elysia/test/tracer/aot.test.ts +vendor/elysia/test/tracer/detail.test.ts +vendor/elysia/test/tracer/timing.test.ts +vendor/elysia/test/tracer/trace.test.ts +vendor/elysia/test/type-system/array-string.test.ts +vendor/elysia/test/type-system/boolean-string.test.ts +vendor/elysia/test/type-system/coercion-number.test.ts +vendor/elysia/test/type-system/date.test.ts +vendor/elysia/test/type-system/form.test.ts +vendor/elysia/test/type-system/object-string.test.ts +vendor/elysia/test/type-system/string-format.test.ts +vendor/elysia/test/type-system/union-enum.test.ts +vendor/elysia/test/units/deduplicate-checksum.test.ts +vendor/elysia/test/units/has-ref.test.ts +vendor/elysia/test/units/has-transform.test.ts +vendor/elysia/test/units/merge-deep.test.ts +vendor/elysia/test/units/merge-object-schemas.test.ts +vendor/elysia/test/units/replace-schema-type.test.ts +vendor/elysia/test/validator/body.test.ts +vendor/elysia/test/validator/encode.test.ts +vendor/elysia/test/validator/exact-mirror.test.ts +vendor/elysia/test/validator/header.test.ts +vendor/elysia/test/validator/params.test.ts +vendor/elysia/test/validator/query.test.ts +vendor/elysia/test/validator/response.test.ts +vendor/elysia/test/validator/standalone.test.ts +vendor/elysia/test/validator/validator.test.ts +vendor/elysia/test/ws/aot.test.ts +vendor/elysia/test/ws/connection.test.ts +vendor/elysia/test/ws/destructuring.test.ts +vendor/elysia/test/ws/message.test.ts From 3aedf0692c9cbe1081387f61fd741daec7a3d185 Mon Sep 17 00:00:00 2001 From: Michael H Date: Fri, 20 Jun 2025 05:55:32 +1000 Subject: [PATCH 006/147] make options argument not required for `fs.promises.glob` (#20480) --- src/js/internal/fs/glob.ts | 8 ++++---- test/js/node/fs/glob.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/js/internal/fs/glob.ts b/src/js/internal/fs/glob.ts index 294313fcbc..03d2601f94 100644 --- a/src/js/internal/fs/glob.ts +++ b/src/js/internal/fs/glob.ts @@ -17,9 +17,9 @@ interface ExtendedGlobOptions extends GlobScanOptions { exclude(ent: string): boolean; } -async function* glob(pattern: string | string[], options: GlobOptions): AsyncGenerator { +async function* glob(pattern: string | string[], options?: GlobOptions): AsyncGenerator { pattern = validatePattern(pattern); - const globOptions = mapOptions(options); + const globOptions = mapOptions(options || {}); let it = new Bun.Glob(pattern).scan(globOptions); const exclude = globOptions.exclude; @@ -29,9 +29,9 @@ async function* glob(pattern: string | string[], options: GlobOptions): AsyncGen } } -function* globSync(pattern: string | string[], options: GlobOptions): Generator { +function* globSync(pattern: string | string[], options?: GlobOptions): Generator { pattern = validatePattern(pattern); - const globOptions = mapOptions(options); + const globOptions = mapOptions(options || {}); const g = new Bun.Glob(pattern); const exclude = globOptions.exclude; for (const ent of g.scanSync(globOptions)) { diff --git a/test/js/node/fs/glob.test.ts b/test/js/node/fs/glob.test.ts index 180f79edb7..dc5d70a243 100644 --- a/test/js/node/fs/glob.test.ts +++ b/test/js/node/fs/glob.test.ts @@ -102,6 +102,18 @@ describe("fs.globSync", () => { expect(fs.globSync("a/*", { cwd: tmp, exclude })).toStrictEqual(expected); }); + it("works without providing options", () => { + const oldProcessCwd = process.cwd; + try { + process.cwd = () => tmp; + + const paths = fs.globSync("*.txt"); + expect(paths).toContain("foo.txt"); + } finally { + process.cwd = oldProcessCwd; + } + }); + describe("invalid arguments", () => { // TODO: GlobSet it("does not support arrays of patterns yet", () => { @@ -129,4 +141,23 @@ describe("fs.promises.glob", () => { expect(path).toMatch(/\.txt$/); } }); + + it("works without providing options", async () => { + const oldProcessCwd = process.cwd; + try { + process.cwd = () => tmp; + + const iter = fs.promises.glob("*.txt"); + expect(iter[Symbol.asyncIterator]).toBeDefined(); + + const paths = []; + for await (const path of iter) { + paths.push(path); + } + + expect(paths).toContain("foo.txt"); + } finally { + process.cwd = oldProcessCwd; + } + }); }); // From 43777cffeee8e939dd744967bbeacd1cd46e01dd Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 19 Jun 2025 11:56:27 -0800 Subject: [PATCH 007/147] fix passing nested object to macro (#20467) Co-authored-by: nektro <5464072+nektro@users.noreply.github.com> --- src/bun.js/bindings/JSValue.zig | 6 +++++ src/bun.js/bindings/bindings.cpp | 13 ++++++++++ src/js_ast.zig | 7 ++--- test/bundler/bun-build-api.test.ts | 41 +++++++++++++++++++++++++++++- test/harness.ts | 6 +++++ 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 3a89ee7770..09a92f0032 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -397,6 +397,12 @@ pub const JSValue = enum(i64) { JSC__JSValue__putMayBeIndex(this, globalObject, key, value); } + extern fn JSC__JSValue__putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) void; + pub fn putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) error{JSError}!void { + JSC__JSValue__putToPropertyKey(target, globalObject, key, value); + if (globalObject.hasException()) return error.JSError; + } + extern fn JSC__JSValue__putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void; pub fn putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void { JSC__JSValue__putIndex(value, globalObject, i, out); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 34db22da41..ea4b74cf94 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3664,6 +3664,19 @@ void JSC__JSValue__put(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, object->putDirect(arg1->vm(), Zig::toIdentifier(*arg2, arg1), JSC::JSValue::decode(JSValue3)); } +void JSC__JSValue__putToPropertyKey(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3) +{ + auto& vm = JSC::getVM(arg1); + auto scope = DECLARE_THROW_SCOPE(vm); + auto obj = JSValue::decode(JSValue0); + auto key = JSValue::decode(arg2); + auto value = JSValue::decode(arg3); + auto object = obj.asCell()->getObject(); + auto pkey = key.toPropertyKey(arg1); + RETURN_IF_EXCEPTION(scope, ); + object->putDirectMayBeIndex(arg1, pkey, value); +} + extern "C" void JSC__JSValue__putMayBeIndex(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const BunString* key, JSC::EncodedJSValue value) { auto& vm = JSC::getVM(globalObject); diff --git a/src/js_ast.zig b/src/js_ast.zig index edeb32c2a1..b609627083 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1967,11 +1967,12 @@ pub const E = struct { defer obj.unprotect(); const props: []const G.Property = this.properties.slice(); for (props) |prop| { - if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.key.?.data != .e_string or prop.value == null) { + if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) { return error.@"Cannot convert argument type to JS"; } - var key = prop.key.?.data.e_string.toZigString(allocator); - obj.put(globalObject, &key, try prop.value.?.toJS(allocator, globalObject)); + const key = try prop.key.?.data.toJS(allocator, globalObject); + const value = try prop.value.?.toJS(allocator, globalObject); + try obj.putToPropertyKey(globalObject, key, value); } return obj; diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 4ad15e2595..fae46580c4 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -1,8 +1,9 @@ import assert from "assert"; import { describe, expect, test } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, tempDirWithFiles, tempDirWithFilesAnon } from "harness"; import path, { join } from "path"; +import { cwd } from "process"; import { buildNoThrow } from "./buildNoThrow"; describe("Bun.build", () => { @@ -632,3 +633,41 @@ test("onEnd Plugin does not crash", async () => { })(), ).rejects.toThrow("On-end callbacks is not implemented yet. See https://github.com/oven-sh/bun/issues/2771"); }); + +test("macro with nested object", async () => { + const dir = tempDirWithFilesAnon({ + "index.ts": ` +import { testMacro } from "./macro" assert { type: "macro" }; + +export const testConfig = testMacro({ + borderRadius: { + 1: "4px", + 2: "8px", + }, +}); + `, + "macro.ts": ` +export function testMacro(val: any) { + return val; +} + `, + }); + + const build = await Bun.build({ + entrypoints: [join(dir, "index.ts")], + }); + + expect(build.outputs).toHaveLength(1); + expect(build.outputs[0].kind).toBe("entry-point"); + expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(`// ${path.relative(cwd(), dir)}/index.ts +var testConfig = { + borderRadius: { + "1": "4px", + "2": "8px" + } +}; +export { + testConfig +}; +`); +}); diff --git a/test/harness.ts b/test/harness.ts index ee3cb2c0cb..001d6b39e0 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -262,6 +262,12 @@ export function tempDirWithFiles( return base; } +export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string { + const base = tmpdirSync(); + makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom); + return base; +} + export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv) { var path = require("path"); const result = Bun.spawnSync([bunExe(), file], { From d4ccba67f20746f7210fa6f64fd1e4b042080a05 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 19 Jun 2025 12:14:46 -0800 Subject: [PATCH 008/147] Revert "fix passing nested object to macro" (#20495) --- src/bun.js/bindings/JSValue.zig | 6 ----- src/bun.js/bindings/bindings.cpp | 13 ---------- src/js_ast.zig | 7 +++-- test/bundler/bun-build-api.test.ts | 41 +----------------------------- test/harness.ts | 6 ----- 5 files changed, 4 insertions(+), 69 deletions(-) diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 09a92f0032..3a89ee7770 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -397,12 +397,6 @@ pub const JSValue = enum(i64) { JSC__JSValue__putMayBeIndex(this, globalObject, key, value); } - extern fn JSC__JSValue__putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) void; - pub fn putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) error{JSError}!void { - JSC__JSValue__putToPropertyKey(target, globalObject, key, value); - if (globalObject.hasException()) return error.JSError; - } - extern fn JSC__JSValue__putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void; pub fn putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void { JSC__JSValue__putIndex(value, globalObject, i, out); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index ea4b74cf94..34db22da41 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3664,19 +3664,6 @@ void JSC__JSValue__put(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, object->putDirect(arg1->vm(), Zig::toIdentifier(*arg2, arg1), JSC::JSValue::decode(JSValue3)); } -void JSC__JSValue__putToPropertyKey(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3) -{ - auto& vm = JSC::getVM(arg1); - auto scope = DECLARE_THROW_SCOPE(vm); - auto obj = JSValue::decode(JSValue0); - auto key = JSValue::decode(arg2); - auto value = JSValue::decode(arg3); - auto object = obj.asCell()->getObject(); - auto pkey = key.toPropertyKey(arg1); - RETURN_IF_EXCEPTION(scope, ); - object->putDirectMayBeIndex(arg1, pkey, value); -} - extern "C" void JSC__JSValue__putMayBeIndex(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const BunString* key, JSC::EncodedJSValue value) { auto& vm = JSC::getVM(globalObject); diff --git a/src/js_ast.zig b/src/js_ast.zig index b609627083..edeb32c2a1 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1967,12 +1967,11 @@ pub const E = struct { defer obj.unprotect(); const props: []const G.Property = this.properties.slice(); for (props) |prop| { - if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) { + if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.key.?.data != .e_string or prop.value == null) { return error.@"Cannot convert argument type to JS"; } - const key = try prop.key.?.data.toJS(allocator, globalObject); - const value = try prop.value.?.toJS(allocator, globalObject); - try obj.putToPropertyKey(globalObject, key, value); + var key = prop.key.?.data.e_string.toZigString(allocator); + obj.put(globalObject, &key, try prop.value.?.toJS(allocator, globalObject)); } return obj; diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index fae46580c4..4ad15e2595 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -1,9 +1,8 @@ import assert from "assert"; import { describe, expect, test } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDirWithFiles, tempDirWithFilesAnon } from "harness"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; import path, { join } from "path"; -import { cwd } from "process"; import { buildNoThrow } from "./buildNoThrow"; describe("Bun.build", () => { @@ -633,41 +632,3 @@ test("onEnd Plugin does not crash", async () => { })(), ).rejects.toThrow("On-end callbacks is not implemented yet. See https://github.com/oven-sh/bun/issues/2771"); }); - -test("macro with nested object", async () => { - const dir = tempDirWithFilesAnon({ - "index.ts": ` -import { testMacro } from "./macro" assert { type: "macro" }; - -export const testConfig = testMacro({ - borderRadius: { - 1: "4px", - 2: "8px", - }, -}); - `, - "macro.ts": ` -export function testMacro(val: any) { - return val; -} - `, - }); - - const build = await Bun.build({ - entrypoints: [join(dir, "index.ts")], - }); - - expect(build.outputs).toHaveLength(1); - expect(build.outputs[0].kind).toBe("entry-point"); - expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(`// ${path.relative(cwd(), dir)}/index.ts -var testConfig = { - borderRadius: { - "1": "4px", - "2": "8px" - } -}; -export { - testConfig -}; -`); -}); diff --git a/test/harness.ts b/test/harness.ts index 001d6b39e0..ee3cb2c0cb 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -262,12 +262,6 @@ export function tempDirWithFiles( return base; } -export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string { - const base = tmpdirSync(); - makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom); - return base; -} - export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv) { var path = require("path"); const result = Bun.spawnSync([bunExe(), file], { From b62f70c23aaa024512a0aa07edae68930cdb4e83 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 19 Jun 2025 15:02:38 -0700 Subject: [PATCH 009/147] inspect-error-leak.test.js: add a slightly higher asan margin --- test/js/bun/util/inspect-error-leak.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/util/inspect-error-leak.test.js b/test/js/bun/util/inspect-error-leak.test.js index 6634edf6e0..aab13c8d37 100644 --- a/test/js/bun/util/inspect-error-leak.test.js +++ b/test/js/bun/util/inspect-error-leak.test.js @@ -20,5 +20,5 @@ test("Printing errors does not leak", () => { const after = Math.floor(process.memoryUsage.rss() / 1024); const diff = ((after - baseline) / 1024) | 0; console.log(`RSS increased by ${diff} MB`); - expect(diff, `RSS grew by ${diff} MB after ${perBatch * repeat} iterations`).toBeLessThan(isASAN ? 16 : 10); + expect(diff, `RSS grew by ${diff} MB after ${perBatch * repeat} iterations`).toBeLessThan(isASAN ? 20 : 10); }, 10_000); From 197443b2db63f9ddbea1783fe4b5f648dadfbe8b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 19 Jun 2025 15:04:27 -0700 Subject: [PATCH 010/147] fix(bundler): correct `import_records` for TLA detection (#20358) --- src/bundler/LinkerContext.zig | 38 +++++++++++-------- .../linker_context/scanImportsAndExports.zig | 2 +- test/bundler/cli.test.ts | 9 ++++- test/bundler/esbuild/default.test.ts | 23 +++++++++++ 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 89f238947d..0a80ad2677 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -890,13 +890,13 @@ pub const LinkerContext = struct { pub fn validateTLA( c: *LinkerContext, source_index: Index.Int, - tla_keywords: []Logger.Range, + tla_keywords: []const Logger.Range, tla_checks: []js_ast.TlaCheck, - input_files: []Logger.Source, - import_records: []ImportRecord, + input_files: []const Logger.Source, + import_records: []const ImportRecord, meta_flags: []JSMeta.Flags, - ast_import_records: []bun.BabyList(ImportRecord), - ) js_ast.TlaCheck { + ast_import_records: []const bun.BabyList(ImportRecord), + ) bun.OOM!js_ast.TlaCheck { var result_tla_check: *js_ast.TlaCheck = &tla_checks[source_index]; if (result_tla_check.depth == 0) { @@ -907,7 +907,15 @@ pub const LinkerContext = struct { for (import_records, 0..) |record, import_record_index| { if (Index.isValid(record.source_index) and (record.kind == .require or record.kind == .stmt)) { - const parent = c.validateTLA(record.source_index.get(), tla_keywords, tla_checks, input_files, import_records, meta_flags, ast_import_records); + const parent = try c.validateTLA( + record.source_index.get(), + tla_keywords, + tla_checks, + input_files, + ast_import_records[record.source_index.get()].slice(), + meta_flags, + ast_import_records, + ); if (Index.isInvalid(Index.init(parent.parent))) { continue; } @@ -944,31 +952,31 @@ pub const LinkerContext = struct { } if (!Index.isValid(Index.init(parent_tla_check.parent))) { - notes.append(Logger.Data{ + try notes.append(Logger.Data{ .text = "unexpected invalid index", - }) catch bun.outOfMemory(); + }); break; } other_source_index = parent_tla_check.parent; - notes.append(Logger.Data{ - .text = std.fmt.allocPrint(c.allocator, "The file {s} imports the file {s} here:", .{ + try notes.append(Logger.Data{ + .text = try std.fmt.allocPrint(c.allocator, "The file {s} imports the file {s} here:", .{ input_files[parent_source_index].path.pretty, input_files[other_source_index].path.pretty, - }) catch bun.outOfMemory(), + }), .location = .initOrNull(&input_files[parent_source_index], ast_import_records[parent_source_index].slice()[tla_checks[parent_source_index].import_record_index].range), - }) catch bun.outOfMemory(); + }); } const source: *const Logger.Source = &input_files[source_index]; const imported_pretty_path = source.path.pretty; const text: string = if (strings.eql(imported_pretty_path, tla_pretty_path)) - std.fmt.allocPrint(c.allocator, "This require call is not allowed because the imported file \"{s}\" contains a top-level await", .{imported_pretty_path}) catch bun.outOfMemory() + try std.fmt.allocPrint(c.allocator, "This require call is not allowed because the imported file \"{s}\" contains a top-level await", .{imported_pretty_path}) else - std.fmt.allocPrint(c.allocator, "This require call is not allowed because the transitive dependency \"{s}\" contains a top-level await", .{tla_pretty_path}) catch bun.outOfMemory(); + try std.fmt.allocPrint(c.allocator, "This require call is not allowed because the transitive dependency \"{s}\" contains a top-level await", .{tla_pretty_path}); - c.log.addRangeErrorWithNotes(source, record.range, text, notes.items) catch bun.outOfMemory(); + try c.log.addRangeErrorWithNotes(source, record.range, text, notes.items); } } } diff --git a/src/bundler/linker_context/scanImportsAndExports.zig b/src/bundler/linker_context/scanImportsAndExports.zig index c510cdde3d..2d53e3fabe 100644 --- a/src/bundler/linker_context/scanImportsAndExports.zig +++ b/src/bundler/linker_context/scanImportsAndExports.zig @@ -80,7 +80,7 @@ pub fn scanImportsAndExports(this: *LinkerContext) !void { continue; } - _ = this.validateTLA(id, tla_keywords, tla_checks, input_files, import_records, flags, import_records_list); + _ = try this.validateTLA(id, tla_keywords, tla_checks, input_files, import_records, flags, import_records_list); for (import_records) |record| { if (!record.source_index.isValid()) { diff --git a/test/bundler/cli.test.ts b/test/bundler/cli.test.ts index 43d8cbd1d9..2999dbcf17 100644 --- a/test/bundler/cli.test.ts +++ b/test/bundler/cli.test.ts @@ -201,7 +201,14 @@ test("you can use --outfile=... and --sourcemap", () => { const outputContent = fs.readFileSync(outFile, "utf8"); expect(outputContent).toContain("//# sourceMappingURL=out.js.map"); - expect(stdout.toString()).toMatchInlineSnapshot(); + expect(stdout.toString().replace(/\d{1,}ms/, "0.000000001ms")).toMatchInlineSnapshot(` + "Bundled 1 module in 0.000000001ms + + out.js 120 bytes (entry point) + out.js.map 213 bytes (source map) + + " + `); }); test("some log cases", () => { diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts index 21e213bfe2..4491d1edef 100644 --- a/test/bundler/esbuild/default.test.ts +++ b/test/bundler/esbuild/default.test.ts @@ -1581,6 +1581,29 @@ describe("bundler", () => { "/entry.js": ["Top-level return cannot be used inside an ECMAScript module"], }, }); + itBundled("default/CircularTLADependency", { + files: { + "/entry.js": /* js */ ` + const { A } = await import('./a.js'); + console.log(A); + `, + "/a.js": /* js */ ` + import { B } from './b.js'; + export const A = 'hi'; + `, + "/b.js": /* js */ ` + import { A } from './a.js'; + + // TLA that should mark the wrapper closure for a.js as async + await 1; + + export const B = 'hello'; + `, + }, + run: { + stdout: "hi\n", + }, + }); itBundled("default/ThisOutsideFunctionRenamedToExports", { files: { "/entry.js": /* js */ ` From bb55b2596d27073c3263fb09f60f93a13c7c1987 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 19 Jun 2025 16:13:27 -0800 Subject: [PATCH 011/147] fix passing nested object to macro" (#20497) Co-authored-by: nektro <5464072+nektro@users.noreply.github.com> --- src/bun.js/bindings/JSValue.zig | 5 +++++ src/bun.js/bindings/bindings.cpp | 13 ++++++++++++ src/bun.js/jsc/host_fn.zig | 15 ++++++++++++++ src/js_ast.zig | 7 ++++--- test/bundler/bun-build-api.test.ts | 33 +++++++++++++++++++++++++++++- test/harness.ts | 6 ++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 3a89ee7770..b4e9519be8 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -397,6 +397,11 @@ pub const JSValue = enum(i64) { JSC__JSValue__putMayBeIndex(this, globalObject, key, value); } + extern fn JSC__JSValue__putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) void; + pub fn putToPropertyKey(target: JSValue, globalObject: *JSGlobalObject, key: JSC.JSValue, value: JSC.JSValue) bun.JSError!void { + return bun.jsc.host_fn.fromJSHostCallVoid(globalObject, @src(), JSC__JSValue__putToPropertyKey, .{ target, globalObject, key, value }); + } + extern fn JSC__JSValue__putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void; pub fn putIndex(value: JSValue, globalObject: *JSGlobalObject, i: u32, out: JSValue) void { JSC__JSValue__putIndex(value, globalObject, i, out); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 34db22da41..ea4b74cf94 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3664,6 +3664,19 @@ void JSC__JSValue__put(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, object->putDirect(arg1->vm(), Zig::toIdentifier(*arg2, arg1), JSC::JSValue::decode(JSValue3)); } +void JSC__JSValue__putToPropertyKey(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3) +{ + auto& vm = JSC::getVM(arg1); + auto scope = DECLARE_THROW_SCOPE(vm); + auto obj = JSValue::decode(JSValue0); + auto key = JSValue::decode(arg2); + auto value = JSValue::decode(arg3); + auto object = obj.asCell()->getObject(); + auto pkey = key.toPropertyKey(arg1); + RETURN_IF_EXCEPTION(scope, ); + object->putDirectMayBeIndex(arg1, pkey, value); +} + extern "C" void JSC__JSValue__putMayBeIndex(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const BunString* key, JSC::EncodedJSValue value) { auto& vm = JSC::getVM(globalObject); diff --git a/src/bun.js/jsc/host_fn.zig b/src/bun.js/jsc/host_fn.zig index 9bba8d08f5..da0f830cfd 100644 --- a/src/bun.js/jsc/host_fn.zig +++ b/src/bun.js/jsc/host_fn.zig @@ -121,6 +121,21 @@ pub fn fromJSHostCall( return if (value == .zero) error.JSError else value; } +pub fn fromJSHostCallVoid( + globalThis: *JSGlobalObject, + /// For attributing thrown exceptions + src: std.builtin.SourceLocation, + comptime function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) bun.JSError!void { + var scope: jsc.CatchScope = undefined; + scope.init(globalThis, src, .assertions_only); + defer scope.deinit(); + + @call(.auto, function, args); + try scope.returnIfException(); +} + const ParsedHostFunctionErrorSet = struct { OutOfMemory: bool = false, JSError: bool = false, diff --git a/src/js_ast.zig b/src/js_ast.zig index edeb32c2a1..b609627083 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1967,11 +1967,12 @@ pub const E = struct { defer obj.unprotect(); const props: []const G.Property = this.properties.slice(); for (props) |prop| { - if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.key.?.data != .e_string or prop.value == null) { + if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) { return error.@"Cannot convert argument type to JS"; } - var key = prop.key.?.data.e_string.toZigString(allocator); - obj.put(globalObject, &key, try prop.value.?.toJS(allocator, globalObject)); + const key = try prop.key.?.data.toJS(allocator, globalObject); + const value = try prop.value.?.toJS(allocator, globalObject); + try obj.putToPropertyKey(globalObject, key, value); } return obj; diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 4ad15e2595..1292c07e59 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { describe, expect, test } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, tempDirWithFiles, tempDirWithFilesAnon } from "harness"; import path, { join } from "path"; import { buildNoThrow } from "./buildNoThrow"; @@ -632,3 +632,34 @@ test("onEnd Plugin does not crash", async () => { })(), ).rejects.toThrow("On-end callbacks is not implemented yet. See https://github.com/oven-sh/bun/issues/2771"); }); + +test("macro with nested object", async () => { + const dir = tempDirWithFilesAnon({ + "index.ts": ` +import { testMacro } from "./macro" assert { type: "macro" }; + +export const testConfig = testMacro({ + borderRadius: { + 1: "4px", + 2: "8px", + }, +}); + `, + "macro.ts": ` +export function testMacro(val: any) { + return val; +} + `, + }); + + const build = await Bun.build({ + entrypoints: [join(dir, "index.ts")], + minify: true, + }); + + expect(build.outputs).toHaveLength(1); + expect(build.outputs[0].kind).toBe("entry-point"); + expect(await build.outputs[0].text()).toEqualIgnoringWhitespace( + `var t={borderRadius:{"1":"4px","2":"8px"}};export{t as testConfig};\n`, + ); +}); diff --git a/test/harness.ts b/test/harness.ts index ee3cb2c0cb..001d6b39e0 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -262,6 +262,12 @@ export function tempDirWithFiles( return base; } +export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string { + const base = tmpdirSync(); + makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom); + return base; +} + export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv) { var path = require("path"); const result = Bun.spawnSync([bunExe(), file], { From 41d10ed01e824f3a8bfb38a0daf9b25819eeb109 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:47:00 -0700 Subject: [PATCH 012/147] Refactor shell and fix some bugs (#20476) --- cmake/sources/ZigSources.txt | 2 + src/bun.js/api/bun/process.zig | 20 +- src/bun.js/event_loop/Task.zig | 6 + src/bun.js/webcore/FileSink.zig | 93 ++--- src/io/io.zig | 3 + src/io/openForWriting.zig | 139 +++++++ src/ptr/CowSlice.zig | 4 +- src/ptr/ref_count.zig | 21 +- src/shell/Builtin.zig | 117 ++++-- src/shell/IO.zig | 18 + src/shell/IOReader.zig | 35 +- src/shell/IOWriter.zig | 505 +++++++++++++++++++----- src/shell/Yield.zig | 164 ++++++++ src/shell/builtin/basename.zig | 43 +- src/shell/builtin/cat.zig | 96 ++--- src/shell/builtin/cd.zig | 54 ++- src/shell/builtin/cp.zig | 81 ++-- src/shell/builtin/dirname.zig | 30 +- src/shell/builtin/echo.zig | 33 +- src/shell/builtin/exit.zig | 37 +- src/shell/builtin/export.zig | 26 +- src/shell/builtin/false.zig | 15 +- src/shell/builtin/ls.zig | 65 ++- src/shell/builtin/mkdir.zig | 62 ++- src/shell/builtin/mv.zig | 41 +- src/shell/builtin/pwd.zig | 33 +- src/shell/builtin/rm.zig | 108 ++--- src/shell/builtin/seq.zig | 42 +- src/shell/builtin/touch.zig | 61 ++- src/shell/builtin/true.zig | 15 +- src/shell/builtin/which.zig | 48 +-- src/shell/builtin/yes.zig | 22 +- src/shell/interpreter.zig | 204 +++++----- src/shell/shell.zig | 16 + src/shell/states/Assigns.zig | 19 +- src/shell/states/Async.zig | 18 +- src/shell/states/Base.zig | 1 + src/shell/states/Binary.zig | 23 +- src/shell/states/Cmd.zig | 188 +++++---- src/shell/states/CondExpr.zig | 73 ++-- src/shell/states/Expansion.zig | 62 ++- src/shell/states/If.zig | 37 +- src/shell/states/Pipeline.zig | 116 +++--- src/shell/states/Script.zig | 26 +- src/shell/states/Stmt.zig | 28 +- src/shell/states/Subshell.zig | 47 ++- src/shell/subproc.zig | 33 +- src/sys.zig | 64 ++- test/js/bun/shell/bunshell.test.ts | 139 ++++++- test/js/bun/shell/commands/echo.test.ts | 77 ++++ test/js/bun/shell/file-io.test.ts | 143 +++++++ test/js/bun/shell/leak.test.ts | 212 +++++++++- test/js/bun/shell/yield.test.ts | 11 + test/js/bun/symbols.test.ts | 2 +- test/no-validate-exceptions.txt | 3 + 55 files changed, 2381 insertions(+), 1200 deletions(-) create mode 100644 src/io/openForWriting.zig create mode 100644 src/shell/Yield.zig create mode 100644 test/js/bun/shell/commands/echo.test.ts create mode 100644 test/js/bun/shell/file-io.test.ts create mode 100644 test/js/bun/shell/yield.test.ts diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index f8e597503c..a131be306f 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -556,6 +556,7 @@ src/install/windows-shim/bun_shim_impl.zig src/io/heap.zig src/io/io.zig src/io/MaxBuf.zig +src/io/openForWriting.zig src/io/PipeReader.zig src/io/pipes.zig src/io/PipeWriter.zig @@ -670,6 +671,7 @@ src/shell/states/Stmt.zig src/shell/states/Subshell.zig src/shell/subproc.zig src/shell/util.zig +src/shell/Yield.zig src/sourcemap/CodeCoverage.zig src/sourcemap/LineOffsetTable.zig src/sourcemap/sourcemap.zig diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index 1d079e128b..435a802826 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -8,6 +8,7 @@ const uv = bun.windows.libuv; const pid_t = if (Environment.isPosix) std.posix.pid_t else uv.uv_pid_t; const fd_t = if (Environment.isPosix) std.posix.fd_t else i32; const Maybe = JSC.Maybe; +const log = bun.Output.scoped(.PROCESS, false); const win_rusage = struct { utime: struct { @@ -990,6 +991,13 @@ pub const PosixSpawnOptions = struct { /// and posix_spawnp(2) will behave as a more /// featureful execve(2). use_execve_on_macos: bool = false, + /// If we need to call `socketpair()`, this + /// sets SO_NOSIGPIPE when true. + /// + /// If false, this avoids setting SO_NOSIGPIPE + /// for stdout. This is used to preserve + /// consistent shell semantics. + no_sigpipe: bool = true, pub const Stdio = union(enum) { path: []const u8, @@ -1347,7 +1355,17 @@ pub fn spawnProcessPosix( } const fds: [2]bun.FileDescriptor = brk: { - const pair = try bun.sys.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, .blocking).unwrap(); + const pair = if (!options.no_sigpipe) try bun.sys.socketpairForShell( + std.posix.AF.UNIX, + std.posix.SOCK.STREAM, + 0, + .blocking, + ).unwrap() else try bun.sys.socketpair( + std.posix.AF.UNIX, + std.posix.SOCK.STREAM, + 0, + .blocking, + ).unwrap(); break :brk .{ pair[if (i == 0) 1 else 0], pair[if (i == 0) 0 else 1] }; }; diff --git a/src/bun.js/event_loop/Task.zig b/src/bun.js/event_loop/Task.zig index b35e534d62..533c9ac879 100644 --- a/src/bun.js/event_loop/Task.zig +++ b/src/bun.js/event_loop/Task.zig @@ -71,6 +71,7 @@ pub const Task = TaggedPointerUnion(.{ ShellGlobTask, ShellIOReaderAsyncDeinit, ShellIOWriterAsyncDeinit, + ShellIOWriter, ShellLsTask, ShellMkdirTask, ShellMvBatchedTask, @@ -144,6 +145,10 @@ pub fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u3 var shell_ls_task: *ShellIOWriterAsyncDeinit = task.get(ShellIOWriterAsyncDeinit).?; shell_ls_task.runFromMainThread(); }, + @field(Task.Tag, @typeName(ShellIOWriter)) => { + var shell_io_writer: *ShellIOWriter = task.get(ShellIOWriter).?; + shell_io_writer.runFromMainThread(); + }, @field(Task.Tag, @typeName(ShellIOReaderAsyncDeinit)) => { var shell_ls_task: *ShellIOReaderAsyncDeinit = task.get(ShellIOReaderAsyncDeinit).?; shell_ls_task.runFromMainThread(); @@ -583,6 +588,7 @@ const ShellAsync = shell.Interpreter.Async; // const ShellIOReaderAsyncDeinit = shell.Interpreter.IOReader.AsyncDeinit; const ShellIOReaderAsyncDeinit = shell.Interpreter.AsyncDeinitReader; const ShellIOWriterAsyncDeinit = shell.Interpreter.AsyncDeinitWriter; +const ShellIOWriter = shell.Interpreter.IOWriter; const TimeoutObject = Timer.TimeoutObject; const ImmediateObject = Timer.ImmediateObject; const ProcessWaiterThreadTask = if (Environment.isPosix) bun.spawn.process.WaiterThread.ProcessQueue.ResultTask else opaque {}; diff --git a/src/bun.js/webcore/FileSink.zig b/src/bun.js/webcore/FileSink.zig index 8230ac6bb5..837cfe7b8a 100644 --- a/src/bun.js/webcore/FileSink.zig +++ b/src/bun.js/webcore/FileSink.zig @@ -225,80 +225,35 @@ pub fn create( } pub fn setup(this: *FileSink, options: *const FileSink.Options) JSC.Maybe(void) { - // TODO: this should be concurrent. - var isatty = false; - var is_nonblocking = false; - const fd = switch (switch (options.input_path) { - .path => |path| brk: { - is_nonblocking = true; - break :brk bun.sys.openA(path.slice(), options.flags(), options.mode); - }, - .fd => |fd_| brk: { - const duped = bun.sys.dupWithFlags(fd_, 0); + const result = bun.io.openForWriting( + bun.FileDescriptor.cwd(), + options.input_path, + options.flags(), + options.mode, + &this.pollable, + &this.is_socket, + this.force_sync, + &this.nonblocking, + *FileSink, + this, + struct { + fn onForceSyncOrIsaTTY(fs: *FileSink) void { + if (comptime bun.Environment.isPosix) { + fs.force_sync = true; + fs.writer.force_sync = true; + } + } + }.onForceSyncOrIsaTTY, + bun.sys.isPollable, + ); - break :brk duped; + const fd = switch (result) { + .err => |err| { + return .{ .err = err }; }, - }) { - .err => |err| return .{ .err = err }, .result => |fd| fd, }; - if (comptime Environment.isPosix) { - switch (bun.sys.fstat(fd)) { - .err => |err| { - fd.close(); - return .{ .err = err }; - }, - .result => |stat| { - this.pollable = bun.sys.isPollable(stat.mode); - if (!this.pollable) { - isatty = std.posix.isatty(fd.native()); - } - - if (isatty) { - this.pollable = true; - } - - this.fd = fd; - this.is_socket = std.posix.S.ISSOCK(stat.mode); - - if (this.force_sync or isatty) { - // Prevents interleaved or dropped stdout/stderr output for terminals. - // As noted in the following reference, local TTYs tend to be quite fast and - // this behavior has become expected due historical functionality on OS X, - // even though it was originally intended to change in v1.0.2 (Libuv 1.2.1). - // Ref: https://github.com/nodejs/node/pull/1771#issuecomment-119351671 - _ = bun.sys.updateNonblocking(fd, false); - is_nonblocking = false; - this.force_sync = true; - this.writer.force_sync = true; - } else if (!is_nonblocking) { - const flags = switch (bun.sys.getFcntlFlags(fd)) { - .result => |flags| flags, - .err => |err| { - fd.close(); - return .{ .err = err }; - }, - }; - is_nonblocking = (flags & @as(@TypeOf(flags), bun.O.NONBLOCK)) != 0; - - if (!is_nonblocking) { - if (bun.sys.setNonblocking(fd) == .result) { - is_nonblocking = true; - } - } - } - - this.nonblocking = is_nonblocking and this.pollable; - }, - } - } else if (comptime Environment.isWindows) { - this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0 and !this.force_sync; - this.fd = fd; - } else { - @compileError("TODO: implement for this platform"); - } - if (comptime Environment.isWindows) { if (this.force_sync) { switch (this.writer.startSync( diff --git a/src/io/io.zig b/src/io/io.zig index 1aba6f7775..69c6dae204 100644 --- a/src/io/io.zig +++ b/src/io/io.zig @@ -11,6 +11,9 @@ const Environment = bun.Environment; pub const heap = @import("./heap.zig"); const JSC = bun.JSC; +pub const openForWriting = @import("./openForWriting.zig").openForWriting; +pub const openForWritingImpl = @import("./openForWriting.zig").openForWritingImpl; + const log = bun.Output.scoped(.loop, false); const posix = std.posix; diff --git a/src/io/openForWriting.zig b/src/io/openForWriting.zig new file mode 100644 index 0000000000..700d329939 --- /dev/null +++ b/src/io/openForWriting.zig @@ -0,0 +1,139 @@ +pub fn openForWriting( + dir: bun.FileDescriptor, + input_path: anytype, + input_flags: i32, + mode: bun.Mode, + pollable: *bool, + is_socket: *bool, + force_sync: bool, + out_nonblocking: *bool, + comptime Ctx: type, + ctx: Ctx, + comptime onForceSyncOrIsaTTY: *const fn (Ctx) void, + comptime isPollable: *const fn (mode: bun.Mode) bool, +) JSC.Maybe(bun.FileDescriptor) { + return openForWritingImpl( + dir, + input_path, + input_flags, + mode, + pollable, + is_socket, + force_sync, + out_nonblocking, + Ctx, + ctx, + onForceSyncOrIsaTTY, + isPollable, + bun.sys.openat, + ); +} + +pub fn openForWritingImpl( + dir: bun.FileDescriptor, + input_path: anytype, + input_flags: i32, + mode: bun.Mode, + pollable: *bool, + is_socket: *bool, + force_sync: bool, + out_nonblocking: *bool, + comptime Ctx: type, + ctx: Ctx, + comptime onForceSyncOrIsaTTY: *const fn (Ctx) void, + comptime isPollable: *const fn (mode: bun.Mode) bool, + comptime openat: *const fn (dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, mode: bun.Mode) JSC.Maybe(bun.FileDescriptor), +) JSC.Maybe(bun.FileDescriptor) { + const PathT = @TypeOf(input_path); + if (PathT != bun.webcore.PathOrFileDescriptor and PathT != [:0]const u8 and PathT != [:0]u8) { + @compileError("Only string or PathOrFileDescriptor is supported but got: " ++ @typeName(PathT)); + } + + // TODO: this should be concurrent. + var isatty = false; + var is_nonblocking = false; + const result = + switch (PathT) { + bun.webcore.PathOrFileDescriptor => switch (input_path) { + .path => |path| brk: { + is_nonblocking = true; + break :brk bun.sys.openatA(dir, path.slice(), input_flags, mode); + }, + .fd => |fd_| brk: { + const duped = bun.sys.dupWithFlags(fd_, 0); + + break :brk duped; + }, + }, + [:0]const u8, [:0]u8 => openat(dir, input_path, input_flags, mode), + else => unreachable, + }; + const fd = switch (result) { + .err => |err| return .{ .err = err }, + .result => |fd| fd, + }; + + if (comptime Environment.isPosix) { + switch (bun.sys.fstat(fd)) { + .err => |err| { + fd.close(); + return .{ .err = err }; + }, + .result => |stat| { + // pollable.* = bun.sys.isPollable(stat.mode); + pollable.* = isPollable(stat.mode); + if (!pollable.*) { + isatty = std.posix.isatty(fd.native()); + } + + if (isatty) { + pollable.* = true; + } + + is_socket.* = std.posix.S.ISSOCK(stat.mode); + + if (force_sync or isatty) { + // Prevents interleaved or dropped stdout/stderr output for terminals. + // As noted in the following reference, local TTYs tend to be quite fast and + // this behavior has become expected due historical functionality on OS X, + // even though it was originally intended to change in v1.0.2 (Libuv 1.2.1). + // Ref: https://github.com/nodejs/node/pull/1771#issuecomment-119351671 + _ = bun.sys.updateNonblocking(fd, false); + is_nonblocking = false; + // this.force_sync = true; + // this.writer.force_sync = true; + onForceSyncOrIsaTTY(ctx); + } else if (!is_nonblocking) { + const flags = switch (bun.sys.getFcntlFlags(fd)) { + .result => |flags| flags, + .err => |err| { + fd.close(); + return .{ .err = err }; + }, + }; + is_nonblocking = (flags & @as(@TypeOf(flags), bun.O.NONBLOCK)) != 0; + + if (!is_nonblocking) { + if (bun.sys.setNonblocking(fd) == .result) { + is_nonblocking = true; + } + } + } + + out_nonblocking.* = is_nonblocking and pollable.*; + }, + } + + return .{ .result = fd }; + } + + if (comptime Environment.isWindows) { + pollable.* = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0 and !force_sync; + return .{ .result = fd }; + } +} + +const std = @import("std"); +const bun = @import("bun"); +const Environment = bun.Environment; +const JSC = bun.JSC; diff --git a/src/ptr/CowSlice.zig b/src/ptr/CowSlice.zig index 33411b21ce..d971e8fbbb 100644 --- a/src/ptr/CowSlice.zig +++ b/src/ptr/CowSlice.zig @@ -221,8 +221,8 @@ pub fn CowSliceZ(T: type, comptime sentinel: ?T) type { } } - pub fn format(str: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { - return std.fmt.formatType(str.slice(), fmt, options, writer, 1); + pub fn format(str: Self, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + return try writer.writeAll(str.slice()); } /// Free this `Cow`'s allocation if it is owned. diff --git a/src/ptr/ref_count.zig b/src/ptr/ref_count.zig index 127dbfe1c9..932f94ca18 100644 --- a/src/ptr/ref_count.zig +++ b/src/ptr/ref_count.zig @@ -75,6 +75,7 @@ pub fn RefCount(T: type, field_name: []const u8, destructor_untyped: anytype, op const debug_name = options.debug_name orelse bun.meta.typeBaseName(@typeName(T)); pub const scope = bun.Output.Scoped(debug_name, true); + const DEBUG_STACK_TRACE = false; const Destructor = if (options.destructor_ctx) |ctx| fn (*T, ctx) void else fn (*T) void; const destructor: Destructor = destructor_untyped; @@ -106,10 +107,12 @@ pub fn RefCount(T: type, field_name: []const u8, destructor_untyped: anytype, op counter.active_counts, counter.active_counts + 1, }); - bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{ - .frame_count = 2, - .skip_file_patterns = &.{"ptr/ref_count.zig"}, - }); + if (DEBUG_STACK_TRACE) { + bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{ + .frame_count = 2, + .skip_file_patterns = &.{"ptr/ref_count.zig"}, + }); + } } counter.assertNonThreadSafeCountIsSingleThreaded(); counter.active_counts += 1; @@ -130,10 +133,12 @@ pub fn RefCount(T: type, field_name: []const u8, destructor_untyped: anytype, op counter.active_counts, counter.active_counts - 1, }); - bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{ - .frame_count = 2, - .skip_file_patterns = &.{"ptr/ref_count.zig"}, - }); + if (DEBUG_STACK_TRACE) { + bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{ + .frame_count = 2, + .skip_file_patterns = &.{"ptr/ref_count.zig"}, + }); + } } counter.assertNonThreadSafeCountIsSingleThreaded(); counter.active_counts -= 1; diff --git a/src/shell/Builtin.zig b/src/shell/Builtin.zig index 44cb842c15..950e7bf818 100644 --- a/src/shell/Builtin.zig +++ b/src/shell/Builtin.zig @@ -156,6 +156,8 @@ pub const BuiltinIO = struct { this.fd.writer.deref(); }, .blob => this.blob.deref(), + // FIXME: should this be here?? + .arraybuf => this.arraybuf.buf.deinit(), else => {}, } } @@ -182,12 +184,16 @@ pub const BuiltinIO = struct { comptime fmt_: []const u8, args: anytype, _: OutputNeedsIOSafeGuard, - ) void { - this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args); + ) Yield { + return this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args); } - pub fn enqueue(this: *@This(), ptr: anytype, buf: []const u8, _: OutputNeedsIOSafeGuard) void { - this.fd.writer.enqueue(ptr, this.fd.captured, buf); + pub fn enqueue(this: *@This(), ptr: anytype, buf: []const u8, _: OutputNeedsIOSafeGuard) Yield { + return this.fd.writer.enqueue(ptr, this.fd.captured, buf); + } + + pub fn enqueueFmt(this: *@This(), ptr: anytype, comptime fmt: []const u8, args: anytype, _: OutputNeedsIOSafeGuard) Yield { + return this.fd.writer.enqueueFmt(ptr, this.fd.captured, fmt, args); } }; @@ -314,7 +320,7 @@ pub fn init( cmd_local_env: *EnvMap, cwd: bun.FileDescriptor, io: *IO, -) CoroutineResult { +) ?Yield { const stdin: BuiltinIO.Input = switch (io.stdin) { .fd => |fd| .{ .fd = fd.refSelf() }, .ignore => .ignore, @@ -366,40 +372,90 @@ pub fn init( }, } + return initRedirections(cmd, kind, node, interpreter); +} + +fn initRedirections( + cmd: *Cmd, + kind: Kind, + node: *const ast.Cmd, + interpreter: *Interpreter, +) ?Yield { if (node.redirect_file) |file| { switch (file) { .atom => { if (cmd.redirection_file.items.len == 0) { - cmd.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)}); - return .yield; + return cmd.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)}); } - // Regular files are not pollable on linux - const is_pollable: bool = if (bun.Environment.isLinux) false else true; + // Regular files are not pollable on linux and macos + const is_pollable: bool = if (bun.Environment.isPosix) false else true; const path = cmd.redirection_file.items[0..cmd.redirection_file.items.len -| 1 :0]; log("EXPANDED REDIRECT: {s}\n", .{cmd.redirection_file.items[0..]}); const perm = 0o666; - const is_nonblocking = false; - const flags = node.redirect.toFlags(); - const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) { - .err => |e| { - cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); - return .yield; - }, - .result => |f| f, + + var pollable = false; + var is_socket = false; + var is_nonblocking = false; + + const redirfd = redirfd: { + if (node.redirect.stdin) { + break :redirfd switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, node.redirect.toFlags(), perm)) { + .err => |e| { + return cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); + }, + .result => |f| f, + }; + } + + const result = bun.io.openForWritingImpl( + cmd.base.shell.cwd_fd, + path, + node.redirect.toFlags(), + perm, + &pollable, + &is_socket, + false, + &is_nonblocking, + void, + {}, + struct { + fn onForceSyncOrIsaTTY(_: void) void {} + }.onForceSyncOrIsaTTY, + shell.interpret.isPollableFromMode, + ShellSyscall.openat, + ); + + break :redirfd switch (result) { + .err => |e| { + return cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); + }, + .result => |f| { + if (bun.Environment.isWindows) { + switch (f.makeLibUVOwnedForSyscall(.open, .close_on_fail)) { + .err => |e| { + return cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); + }, + .result => |f2| break :redirfd f2, + } + } + break :redirfd f; + }, + }; }; + if (node.redirect.stdin) { cmd.exec.bltn.stdin.deref(); cmd.exec.bltn.stdin = .{ .fd = IOReader.init(redirfd, cmd.base.eventLoop()) }; } if (node.redirect.stdout) { cmd.exec.bltn.stdout.deref(); - cmd.exec.bltn.stdout = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } }; + cmd.exec.bltn.stdout = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking, .is_socket = is_socket }, cmd.base.eventLoop()) } }; } if (node.redirect.stderr) { cmd.exec.bltn.stderr.deref(); - cmd.exec.bltn.stderr = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } }; + cmd.exec.bltn.stderr = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking, .is_socket = is_socket }, cmd.base.eventLoop()) } }; } }, .jsbuf => |val| { @@ -428,7 +484,7 @@ pub fn init( if ((node.redirect.stdout or node.redirect.stderr) and !(body.* == .Blob and !body.Blob.needsToReadFile())) { // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary. cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {}; - return .yield; + return .failed; } var original_blob = body.use(); @@ -457,7 +513,7 @@ pub fn init( if ((node.redirect.stdout or node.redirect.stderr) and !blob.needsToReadFile()) { // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary. cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {}; - return .yield; + return .failed; } const theblob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{ @@ -478,7 +534,7 @@ pub fn init( } else { const jsval = cmd.base.interpreter.jsobjs[val.idx]; cmd.base.interpreter.event_loop.js.global.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(globalObject)}) catch {}; - return .yield; + return .failed; } }, } @@ -494,7 +550,7 @@ pub fn init( } } - return .cont; + return null; } pub inline fn eventLoop(this: *const Builtin) JSC.EventLoopHandle { @@ -515,7 +571,7 @@ pub inline fn parentCmdMut(this: *Builtin) *Cmd { return @fieldParentPtr("exec", union_ptr); } -pub fn done(this: *Builtin, exit_code: anytype) void { +pub fn done(this: *Builtin, exit_code: anytype) Yield { const code: ExitCode = switch (@TypeOf(exit_code)) { bun.sys.E => @intFromEnum(exit_code), u1, u8, u16 => exit_code, @@ -537,16 +593,11 @@ pub fn done(this: *Builtin, exit_code: anytype) void { cmd.base.shell.buffered_stderr().append(bun.default_allocator, this.stderr.buf.items[0..]) catch bun.outOfMemory(); } - cmd.parent.childDone(cmd, this.exit_code.?); + return cmd.parent.childDone(cmd, this.exit_code.?); } -pub fn start(this: *Builtin) Maybe(void) { - switch (this.callImpl(Maybe(void), "start", .{})) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } - - return Maybe(void).success; +pub fn start(this: *Builtin) Yield { + return this.callImpl(Yield, "start", .{}); } pub fn deinit(this: *Builtin) void { @@ -685,6 +736,7 @@ pub const Mv = @import("./builtin/mv.zig"); const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = shell.interpret.Interpreter; @@ -704,4 +756,3 @@ const ShellSyscall = shell.interpret.ShellSyscall; const Allocator = std.mem.Allocator; const ast = shell.AST; const IO = shell.Interpreter.IO; -const CoroutineResult = shell.interpret.CoroutineResult; diff --git a/src/shell/IO.zig b/src/shell/IO.zig index e5119d7515..6dfb808c7a 100644 --- a/src/shell/IO.zig +++ b/src/shell/IO.zig @@ -5,6 +5,10 @@ stdin: InKind, stdout: OutKind, stderr: OutKind, +pub fn format(this: IO, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("stdin: {}\nstdout: {}\nstderr: {}", .{ this.stdin, this.stdout, this.stderr }); +} + pub fn deinit(this: *IO) void { this.stdin.close(); this.stdout.close(); @@ -33,6 +37,13 @@ pub const InKind = union(enum) { fd: *Interpreter.IOReader, ignore, + pub fn format(this: InKind, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this) { + .fd => try writer.print("fd: {}", .{this.fd.fd}), + .ignore => try writer.print("ignore", .{}), + } + } + pub fn ref(this: InKind) InKind { switch (this) { .fd => this.fd.ref(), @@ -79,6 +90,13 @@ pub const OutKind = union(enum) { ignore, // fn dupeForSubshell(this: *ShellState, + pub fn format(this: OutKind, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this) { + .fd => try writer.print("fd: {}", .{this.fd.writer.fd}), + .pipe => try writer.print("pipe", .{}), + .ignore => try writer.print("ignore", .{}), + } + } pub fn ref(this: @This()) @This() { switch (this) { diff --git a/src/shell/IOReader.zig b/src/shell/IOReader.zig index 4ee9662a24..5fb7d2b885 100644 --- a/src/shell/IOReader.zig +++ b/src/shell/IOReader.zig @@ -67,21 +67,23 @@ pub fn init(fd: bun.FileDescriptor, evtloop: JSC.EventLoopHandle) *IOReader { } /// Idempotent function to start the reading -pub fn start(this: *IOReader) void { +pub fn start(this: *IOReader) Yield { if (bun.Environment.isPosix) { if (this.reader.handle == .closed or !this.reader.handle.poll.isRegistered()) { if (this.reader.start(this.fd, true).asErr()) |e| { this.onReaderError(e); } } - return; + return .suspended; } - if (this.is_reading) return; + if (this.is_reading) return .suspended; this.is_reading = true; if (this.reader.startWithCurrentPipe().asErr()) |e| { this.onReaderError(e); + return .failed; } + return .suspended; } /// Only does things on windows @@ -128,13 +130,12 @@ pub fn onReadChunk(ptr: *anyopaque, chunk: []const u8, has_more: bun.io.ReadStat var i: usize = 0; while (i < this.readers.len()) { var r = this.readers.get(i); - switch (r.onReadChunk(chunk)) { - .cont => { - i += 1; - }, - .stop_listening => { - this.readers.swapRemove(i); - }, + var remove = false; + r.onReadChunk(chunk, &remove).run(); + if (remove) { + this.readers.swapRemove(i); + } else { + i += 1; } } @@ -164,7 +165,7 @@ pub fn onReaderError(this: *IOReader, err: bun.sys.Error) void { r.onReaderDone(if (this.err) |*e| brk: { e.ref(); break :brk e.*; - } else null); + } else null).run(); } } @@ -175,7 +176,7 @@ pub fn onReaderDone(this: *IOReader) void { r.onReaderDone(if (this.err) |*err| brk: { err.ref(); break :brk err.*; - } else null); + } else null).run(); } } @@ -223,12 +224,12 @@ pub const IOReaderChildPtr = struct { } /// Return true if the child should be deleted - pub fn onReadChunk(this: IOReaderChildPtr, chunk: []const u8) ReadChunkAction { - return this.ptr.call("onIOReaderChunk", .{chunk}, ReadChunkAction); + pub fn onReadChunk(this: IOReaderChildPtr, chunk: []const u8, remove: *bool) Yield { + return this.ptr.call("onIOReaderChunk", .{ chunk, remove }, Yield); } - pub fn onReaderDone(this: IOReaderChildPtr, err: ?JSC.SystemError) void { - return this.ptr.call("onIOReaderDone", .{err}, void); + pub fn onReaderDone(this: IOReaderChildPtr, err: ?JSC.SystemError) Yield { + return this.ptr.call("onIOReaderDone", .{err}, Yield); } }; @@ -262,11 +263,11 @@ pub const AsyncDeinitReader = struct { }; const SmolList = bun.shell.SmolList; -const ReadChunkAction = bun.shell.interpret.ReadChunkAction; const std = @import("std"); const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const Interpreter = bun.shell.Interpreter; const log = bun.shell.interpret.log; diff --git a/src/shell/IOWriter.zig b/src/shell/IOWriter.zig index a7eb64fd1d..bdb28bb6f6 100644 --- a/src/shell/IOWriter.zig +++ b/src/shell/IOWriter.zig @@ -25,15 +25,16 @@ buf: std.ArrayListUnmanaged(u8) = .{}, /// quick hack to get windows working /// ideally this should be removed winbuf: if (bun.Environment.isWindows) std.ArrayListUnmanaged(u8) else u0 = if (bun.Environment.isWindows) .empty else 0, -__idx: usize = 0, +writer_idx: usize = 0, total_bytes_written: usize = 0, err: ?JSC.SystemError = null, evtloop: JSC.EventLoopHandle, concurrent_task: JSC.EventLoopTask, -is_writing: if (bun.Environment.isWindows) bool else u0 = if (bun.Environment.isWindows) false else 0, +concurrent_task2: JSC.EventLoopTask, +is_writing: bool = false, async_deinit: AsyncDeinitWriter = .{}, started: bool = false, -flags: InitFlags = .{}, +flags: Flags = .{}, const debug = bun.Output.scoped(.IOWriter, true); @@ -44,10 +45,15 @@ pub const ChildPtr = IOWriterChildPtr; /// but if this never happens, we shrink `buf` when it exceeds this threshold const SHRINK_THRESHOLD = 1024 * 128; +const CallstackChild = struct { + child: ChildPtr, + completed: bool = false, +}; + pub const auto_poll = false; pub const WriterImpl = bun.io.BufferedWriter(IOWriter, struct { - pub const onWrite = IOWriter.onWrite; + pub const onWrite = IOWriter.onWritePollable; pub const onError = IOWriter.onError; pub const onClose = IOWriter.onClose; pub const getBuffer = IOWriter.getBuffer; @@ -63,19 +69,21 @@ pub fn refSelf(this: *IOWriter) *IOWriter { return this; } -pub const InitFlags = packed struct(u8) { +pub const Flags = packed struct(u8) { pollable: bool = false, nonblocking: bool = false, is_socket: bool = false, - __unused: u5 = 0, + broken_pipe: bool = false, + __unused: u4 = 0, }; -pub fn init(fd: bun.FileDescriptor, flags: InitFlags, evtloop: JSC.EventLoopHandle) *IOWriter { +pub fn init(fd: bun.FileDescriptor, flags: Flags, evtloop: JSC.EventLoopHandle) *IOWriter { const this = bun.new(IOWriter, .{ .ref_count = .init(), .fd = fd, .evtloop = evtloop, .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop), + .concurrent_task2 = JSC.EventLoopTask.fromEventLoop(evtloop), }); this.writer.parent = this; @@ -156,38 +164,49 @@ pub fn eventLoop(this: *IOWriter) JSC.EventLoopHandle { } /// Idempotent write call -pub fn write(this: *IOWriter) void { +fn write(this: *IOWriter) enum { + suspended, + failed, + is_actually_file, +} { + if (bun.Environment.isPosix) + bun.assert(this.flags.pollable); + if (!this.started) { log("IOWriter(0x{x}, fd={}) starting", .{ @intFromPtr(this), this.fd }); if (this.__start().asErr()) |e| { this.onError(e); - return; + return .failed; } this.started = true; if (comptime bun.Environment.isPosix) { - if (this.writer.handle == .fd) {} else return; - } else return; + // if `handle == .fd` it means it's a file which does not + // support polling for writeability and we should just + // write to it + if (this.writer.handle == .fd) { + bun.assert(!this.flags.pollable); + return .is_actually_file; + } + return .suspended; + } + return .suspended; } + if (bun.Environment.isWindows) { log("IOWriter(0x{x}, fd={}) write() is_writing={any}", .{ @intFromPtr(this), this.fd, this.is_writing }); - if (this.is_writing) return; + if (this.is_writing) return .suspended; this.is_writing = true; if (this.writer.startWithCurrentPipe().asErr()) |e| { this.onError(e); - return; + return .failed; } - return; + return .suspended; } - if (this.writer.handle == .poll) { - if (!this.writer.handle.poll.isWatching()) { - log("IOWriter(0x{x}, fd={}) calling this.writer.write()", .{ @intFromPtr(this), this.fd }); - this.writer.write(); - } else log("IOWriter(0x{x}, fd={}) poll already watching", .{ @intFromPtr(this), this.fd }); - } else { - log("IOWriter(0x{x}, fd={}) no poll, calling write", .{ @intFromPtr(this), this.fd }); - this.writer.write(); - } + bun.assert(this.writer.handle == .poll); + if (this.writer.handle.poll.isWatching()) return .suspended; + this.writer.start(this.fd, this.flags.pollable).assert(); + return .suspended; } /// Cancel the chunks enqueued by the given writer by @@ -198,7 +217,7 @@ pub fn cancelChunks(this: *IOWriter, ptr_: anytype) void { else => ChildPtr.init(ptr_), }; if (this.writers.len() == 0) return; - const idx = this.__idx; + const idx = this.writer_idx; const slice: []Writer = this.writers.sliceMutable(); if (idx >= slice.len) return; for (slice[idx..]) |*w| { @@ -214,6 +233,10 @@ const Writer = struct { written: usize = 0, bytelist: ?*bun.ByteList = null, + pub fn wroteEverything(this: *const Writer) bool { + return this.written >= this.len; + } + pub fn rawPtr(this: Writer) ?*anyopaque { return this.ptr.ptr.ptr(); } @@ -223,7 +246,7 @@ const Writer = struct { } pub fn setDead(this: *Writer) void { - this.ptr.ptr = ChildPtr.ChildPtrRaw.Null; + this.ptr.ptr = ChildPtrRaw.Null; } }; @@ -233,9 +256,9 @@ pub const Writers = SmolList(Writer, 2); /// amount they would have written so the buf is skipped as well pub fn skipDead(this: *IOWriter) void { const slice = this.writers.slice(); - for (slice[this.__idx..]) |*w| { + for (slice[this.writer_idx..]) |*w| { if (w.isDead()) { - this.__idx += 1; + this.writer_idx += 1; this.total_bytes_written += w.len - w.written; continue; } @@ -244,13 +267,66 @@ pub fn skipDead(this: *IOWriter) void { return; } -pub fn onWrite(this: *IOWriter, amount: usize, status: bun.io.WriteStatus) void { +pub fn doFileWrite(this: *IOWriter) Yield { + assert(bun.Environment.isPosix); + assert(!this.flags.pollable); + assert(this.writer_idx < this.writers.len()); + + defer this.setWriting(false); + this.skipDead(); + + const child = this.writers.get(this.writer_idx); + assert(!child.isDead()); + + const buf = this.getBuffer(); + assert(buf.len > 0); + + var done = false; + const writeResult = drainBufferedData(this, buf, std.math.maxInt(u32), false); + const amt = switch (writeResult) { + .done => |amt| amt: { + done = true; + break :amt amt; + }, + // .wrote can be returned if an error was encountered but there we wrote + // some data before it happened. In that case, onError will also be + // called so we should just return. + .wrote => |amt| amt: { + if (this.err != null) return .done; + break :amt amt; + }, + // This is returned when we hit EAGAIN which should not be the case + // when writing to files unless we opened the file with non-blocking + // mode + .pending => bun.unreachablePanic("drainBufferedData returning .pending in IOWriter.doFileWrite should not happen", .{}), + .err => |e| { + this.onError(e); + return .done; + }, + }; + if (child.bytelist) |bl| { + const written_slice = this.buf.items[this.total_bytes_written .. this.total_bytes_written + amt]; + bl.append(bun.default_allocator, written_slice) catch bun.outOfMemory(); + } + child.written += amt; + if (!child.wroteEverything()) { + bun.assert(writeResult == .done); + // This should never happen if we are here. The only case where we get + // partial writes is when an error is encountered + bun.unreachablePanic("IOWriter.doFileWrite: child.wroteEverything() is false. This is unexpected behavior and indicates a bug in Bun. Please file a GitHub issue.", .{}); + } + return this.bump(child); +} + +pub fn onWritePollable(this: *IOWriter, amount: usize, status: bun.io.WriteStatus) void { + if (bun.Environment.isPosix) bun.assert(this.flags.pollable); + this.setWriting(false); debug("IOWriter(0x{x}, fd={}) onWrite({d}, {})", .{ @intFromPtr(this), this.fd, amount, status }); - if (this.__idx >= this.writers.len()) return; - const child = this.writers.get(this.__idx); + if (this.writer_idx >= this.writers.len()) return; + const child = this.writers.get(this.writer_idx); if (child.isDead()) { - this.bump(child); + this.bump(child).run(); } else { if (child.bytelist) |bl| { const written_slice = this.buf.items[this.total_bytes_written .. this.total_bytes_written + amount]; @@ -259,35 +335,79 @@ pub fn onWrite(this: *IOWriter, amount: usize, status: bun.io.WriteStatus) void this.total_bytes_written += amount; child.written += amount; if (status == .end_of_file) { - const not_fully_written = !this.isLastIdx(this.__idx) or child.written < child.len; - if (bun.Environment.allow_assert and not_fully_written) { - bun.Output.debugWarn("IOWriter(0x{x}, fd={}) received done without fully writing data, check that onError is thrown", .{ @intFromPtr(this), this.fd }); - } + const not_fully_written = if (this.isLastIdx(this.writer_idx)) true else child.written < child.len; + // We wrote everything + if (!not_fully_written) return; + + // We did not write everything. + // This seems to happen in a pipeline where the command which + // _reads_ the output of the previous command closes before the + // previous command. + // + // Example: `ls . | echo hi` + // + // 1. We call `socketpair()` and give `ls .` a socket to _write_ to and `echo hi` a socket to _read_ from + // 2. `ls .` executes first, but has to do some async work and so is suspended + // 3. `echo hi` then executes and finishes first (since it does less work) and closes its socket + // 4. `ls .` does its thing and then tries to write to its socket + // 5. Because `echo hi` closed its socket, when `ls .` does `send(...)` it will return EPIPE + // 6. Inside our PipeWriter abstraction this gets returned as bun.io.WriteStatus.end_of_file + // + // So what should we do? In a normal shell, `ls .` would receive the SIGPIPE signal and exit. + // We don't support signals right now. In fact we don't even have a way to kill the shell. + // + // So for a quick hack we're just going to have all writes return an error. + bun.assert(this.flags.is_socket); + bun.Output.debugWarn("IOWriter(0x{x}, fd={}) received done without fully writing data", .{ @intFromPtr(this), this.fd }); + this.flags.broken_pipe = true; + this.brokenPipeForWriters(); return; } if (child.written >= child.len) { - this.bump(child); + this.bump(child).run(); } } - const wrote_everything: bool = this.total_bytes_written >= this.buf.items.len; + const wrote_everything: bool = this.wroteEverything(); - log("IOWriter(0x{x}, fd={}) wrote_everything={}, idx={d} writers={d} next_len={d}", .{ @intFromPtr(this), this.fd, wrote_everything, this.__idx, this.writers.len(), if (this.writers.len() >= 1) this.writers.get(0).len else 0 }); - if (!wrote_everything and this.__idx < this.writers.len()) { + log("IOWriter(0x{x}, fd={}) wrote_everything={}, idx={d} writers={d} next_len={d}", .{ @intFromPtr(this), this.fd, wrote_everything, this.writer_idx, this.writers.len(), if (this.writers.len() >= 1) this.writers.get(0).len else 0 }); + if (!wrote_everything and this.writer_idx < this.writers.len()) { debug("IOWriter(0x{x}, fd={}) poll again", .{ @intFromPtr(this), this.fd }); if (comptime bun.Environment.isWindows) { this.setWriting(true); this.writer.write(); } else { - if (this.writer.handle == .poll) - this.writer.registerPoll() - else - this.writer.write(); + bun.assert(this.writer.handle == .poll); + this.writer.registerPoll(); } } } +pub fn brokenPipeForWriters(this: *IOWriter) void { + bun.assert(this.flags.broken_pipe); + var offset: usize = 0; + for (this.writers.sliceMutable()) |*w| { + if (w.isDead()) { + offset += w.len; + continue; + } + log("IOWriter(0x{x}, fd={}) brokenPipeForWriters {s}(0x{x})", .{ @intFromPtr(this), this.fd, @tagName(w.ptr.ptr.tag()), @intFromPtr(w.ptr.ptr.ptr()) }); + const err: JSC.SystemError = bun.sys.Error.fromCode(.PIPE, .write).toSystemError(); + w.ptr.onIOWriterChunk(0, err).run(); + offset += w.len; + } + + this.total_bytes_written = 0; + this.writers.clearRetainingCapacity(); + this.buf.clearRetainingCapacity(); + this.writer_idx = 0; +} + +pub fn wroteEverything(this: *IOWriter) bool { + return this.total_bytes_written >= this.buf.items.len; +} + pub fn onClose(this: *IOWriter) void { this.setWriting(false); } @@ -313,11 +433,19 @@ pub fn onError(this: *IOWriter, err__: bun.sys.Error) void { continue :writer_loop; } - w.ptr.onWriteChunk(0, this.err); seen.append(@intFromPtr(ptr)) catch bun.outOfMemory(); + // TODO: This probably shouldn't call .run() + w.ptr.onIOWriterChunk(0, this.err).run(); } + + this.total_bytes_written = 0; + this.writer_idx = 0; + this.buf.clearRetainingCapacity(); + this.writers.clearRetainingCapacity(); } +/// Returns the buffer of data that needs to be written +/// for the *current* writer. pub fn getBuffer(this: *IOWriter) []const u8 { const result = this.getBufferImpl(); if (comptime bun.Environment.isWindows) { @@ -331,20 +459,20 @@ pub fn getBuffer(this: *IOWriter) []const u8 { fn getBufferImpl(this: *IOWriter) []const u8 { const writer = brk: { - if (this.__idx >= this.writers.len()) { + if (this.writer_idx >= this.writers.len()) { log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd }); return ""; } - log("IOWriter(0x{x}, fd={}) getBufferImpl idx={d} writer_len={d}", .{ @intFromPtr(this), this.fd, this.__idx, this.writers.len() }); - var writer = this.writers.get(this.__idx); + log("IOWriter(0x{x}, fd={}) getBufferImpl idx={d} writer_len={d}", .{ @intFromPtr(this), this.fd, this.writer_idx, this.writers.len() }); + var writer = this.writers.get(this.writer_idx); if (!writer.isDead()) break :brk writer; log("IOWriter(0x{x}, fd={}) skipping dead", .{ @intFromPtr(this), this.fd }); this.skipDead(); - if (this.__idx >= this.writers.len()) { + if (this.writer_idx >= this.writers.len()) { log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd }); return ""; } - writer = this.writers.get(this.__idx); + writer = this.writers.get(this.writer_idx); break :brk writer; }; log("IOWriter(0x{x}, fd={}) getBufferImpl writer_len={} writer_written={}", .{ @intFromPtr(this), this.fd, writer.len, writer.written }); @@ -355,39 +483,32 @@ fn getBufferImpl(this: *IOWriter) []const u8 { return this.buf.items[this.total_bytes_written .. this.total_bytes_written + remaining]; } -pub fn bump(this: *IOWriter, current_writer: *Writer) void { +pub fn bump(this: *IOWriter, current_writer: *Writer) Yield { log("IOWriter(0x{x}, fd={}) bump(0x{x} {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(current_writer), @tagName(current_writer.ptr.ptr.tag()) }); const is_dead = current_writer.isDead(); const written = current_writer.written; const child_ptr = current_writer.ptr; - defer { - if (!is_dead) child_ptr.onWriteChunk(written, null); - } - if (is_dead) { this.skipDead(); } else { if (bun.Environment.allow_assert) { if (!is_dead) assert(current_writer.written == current_writer.len); } - this.__idx += 1; + this.writer_idx += 1; } - if (this.__idx >= this.writers.len()) { + if (this.writer_idx >= this.writers.len()) { log("IOWriter(0x{x}, fd={}) all writers complete: truncating", .{ @intFromPtr(this), this.fd }); this.buf.clearRetainingCapacity(); - this.__idx = 0; + this.writer_idx = 0; this.writers.clearRetainingCapacity(); this.total_bytes_written = 0; - return; - } - - if (this.total_bytes_written >= SHRINK_THRESHOLD) { + } else if (this.total_bytes_written >= SHRINK_THRESHOLD) { const slice = this.buf.items[this.total_bytes_written..]; const remaining_len = slice.len; - log("IOWriter(0x{x}, fd={}) exceeded shrink threshold: truncating (new_len={d}, writer_starting_idx={d})", .{ @intFromPtr(this), this.fd, remaining_len, this.__idx }); + log("IOWriter(0x{x}, fd={}) exceeded shrink threshold: truncating (new_len={d}, writer_starting_idx={d})", .{ @intFromPtr(this), this.fd, remaining_len, this.writer_idx }); if (slice.len == 0) { this.buf.clearRetainingCapacity(); this.total_bytes_written = 0; @@ -396,23 +517,66 @@ pub fn bump(this: *IOWriter, current_writer: *Writer) void { this.buf.items.len = remaining_len; this.total_bytes_written = 0; } - this.writers.truncate(this.__idx); - this.__idx = 0; + this.writers.truncate(this.writer_idx); + this.writer_idx = 0; if (bun.Environment.allow_assert) { if (this.writers.len() > 0) { - const first = this.writers.getConst(this.__idx); + const first = this.writers.getConst(this.writer_idx); assert(this.buf.items.len >= first.len); } } } + + // If the writer was not dead then call its `onIOWriterChunk` callback + if (!is_dead) { + return child_ptr.onIOWriterChunk(written, null); + } + + return .done; } -pub fn enqueue(this: *IOWriter, ptr: anytype, bytelist: ?*bun.ByteList, buf: []const u8) void { +fn enqueueFile(this: *IOWriter) Yield { + if (this.is_writing) { + return .suspended; + } + this.setWriting(true); + + return this.doFileWrite(); +} + +/// `writer` is the new writer to queue +/// +/// You MUST have already added the data to `this.buf`!! +pub fn enqueueInternal(this: *IOWriter) Yield { + bun.assert(!this.flags.broken_pipe); + if (!this.flags.pollable and bun.Environment.isPosix) return this.enqueueFile(); + switch (this.write()) { + .suspended => return .suspended, + .is_actually_file => { + bun.assert(bun.Environment.isPosix); + return this.enqueueFile(); + }, + // FIXME + .failed => return .failed, + } +} + +pub fn handleBrokenPipe(this: *IOWriter, ptr: ChildPtr) ?Yield { + if (this.flags.broken_pipe) { + const err: JSC.SystemError = bun.sys.Error.fromCode(.PIPE, .write).toSystemError(); + log("IOWriter(0x{x}, fd={}) broken pipe {s}(0x{x})", .{ @intFromPtr(this), this.fd, @tagName(ptr.ptr.tag()), @intFromPtr(ptr.ptr.ptr()) }); + return .{ .on_io_writer_chunk = .{ .child = ptr.asAnyOpaque(), .written = 0, .err = err } }; + } + return null; +} + +pub fn enqueue(this: *IOWriter, ptr: anytype, bytelist: ?*bun.ByteList, buf: []const u8) Yield { const childptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr); + if (this.handleBrokenPipe(childptr)) |yield| return yield; + if (buf.len == 0) { log("IOWriter(0x{x}, fd={}) enqueue EMPTY", .{ @intFromPtr(this), this.fd }); - childptr.onWriteChunk(0, null); - return; + return .{ .on_io_writer_chunk = .{ .child = childptr.asAnyOpaque(), .written = 0, .err = null } }; } const writer: Writer = .{ .ptr = childptr, @@ -422,7 +586,7 @@ pub fn enqueue(this: *IOWriter, ptr: anytype, bytelist: ?*bun.ByteList, buf: []c log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, buf_len={d}, buf={s}, writer_len={d})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), buf.len, buf[0..@min(128, buf.len)], this.writers.len() + 1 }); this.buf.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory(); this.writers.append(writer); - this.write(); + return this.enqueueInternal(); } pub fn enqueueFmtBltn( @@ -432,10 +596,10 @@ pub fn enqueueFmtBltn( comptime kind: ?Interpreter.Builtin.Kind, comptime fmt_: []const u8, args: anytype, -) void { +) Yield { const cmd_str = comptime if (kind) |k| @tagName(k) ++ ": " else ""; const fmt__ = cmd_str ++ fmt_; - this.enqueueFmt(ptr, bytelist, fmt__, args); + return this.enqueueFmt(ptr, bytelist, fmt__, args); } pub fn enqueueFmt( @@ -444,19 +608,23 @@ pub fn enqueueFmt( bytelist: ?*bun.ByteList, comptime fmt: []const u8, args: anytype, -) void { +) Yield { var buf_writer = this.buf.writer(bun.default_allocator); const start = this.buf.items.len; buf_writer.print(fmt, args) catch bun.outOfMemory(); + + const childptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr); + if (this.handleBrokenPipe(childptr)) |yield| return yield; + const end = this.buf.items.len; const writer: Writer = .{ - .ptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr), + .ptr = childptr, .len = end - start, .bytelist = bytelist, }; log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), this.buf.items[start..end] }); this.writers.append(writer); - this.write(); + return this.enqueueInternal(); } fn asyncDeinit(this: *@This()) void { @@ -490,55 +658,177 @@ pub inline fn setWriting(this: *IOWriter, writing: bool) void { } } +// this is unused +pub fn runFromMainThread(_: *IOWriter) void {} + +// this is unused +pub fn runFromMainThreadMini(_: *IOWriter, _: *void) void {} + /// Anything which uses `*IOWriter` to write to a file descriptor needs to /// register itself here so we know how to call its callback on completion. pub const IOWriterChildPtr = struct { ptr: ChildPtrRaw, - pub const ChildPtrRaw = bun.TaggedPointerUnion(.{ - Interpreter.Cmd, - Interpreter.Pipeline, - Interpreter.CondExpr, - Interpreter.Subshell, - Interpreter.Builtin.Cd, - Interpreter.Builtin.Echo, - Interpreter.Builtin.Export, - Interpreter.Builtin.Ls, - Interpreter.Builtin.Ls.ShellLsOutputTask, - Interpreter.Builtin.Mv, - Interpreter.Builtin.Pwd, - Interpreter.Builtin.Rm, - Interpreter.Builtin.Which, - Interpreter.Builtin.Mkdir, - Interpreter.Builtin.Mkdir.ShellMkdirOutputTask, - Interpreter.Builtin.Touch, - Interpreter.Builtin.Touch.ShellTouchOutputTask, - Interpreter.Builtin.Cat, - Interpreter.Builtin.Exit, - Interpreter.Builtin.True, - Interpreter.Builtin.False, - Interpreter.Builtin.Yes, - Interpreter.Builtin.Seq, - Interpreter.Builtin.Dirname, - Interpreter.Builtin.Basename, - Interpreter.Builtin.Cp, - Interpreter.Builtin.Cp.ShellCpOutputTask, - shell.subproc.PipeReader.CapturedWriter, - }); - pub fn init(p: anytype) IOWriterChildPtr { return .{ .ptr = ChildPtrRaw.init(p), }; } + pub fn asAnyOpaque(this: IOWriterChildPtr) *anyopaque { + return this.ptr.ptr(); + } + + pub fn fromAnyOpaque(p: *anyopaque) IOWriterChildPtr { + return .{ .ptr = ChildPtrRaw.from(p) }; + } + /// Called when the IOWriter writes a complete chunk of data the child enqueued - pub fn onWriteChunk(this: IOWriterChildPtr, amount: usize, err: ?JSC.SystemError) void { - return this.ptr.call("onIOWriterChunk", .{ amount, err }, void); + pub fn onIOWriterChunk(this: IOWriterChildPtr, amount: usize, err: ?JSC.SystemError) Yield { + return this.ptr.call("onIOWriterChunk", .{ amount, err }, Yield); + } +}; + +pub const ChildPtrRaw = bun.TaggedPointerUnion(.{ + Interpreter.Cmd, + Interpreter.Pipeline, + Interpreter.CondExpr, + Interpreter.Subshell, + Interpreter.Builtin.Cd, + Interpreter.Builtin.Echo, + Interpreter.Builtin.Export, + Interpreter.Builtin.Ls, + Interpreter.Builtin.Ls.ShellLsOutputTask, + Interpreter.Builtin.Mv, + Interpreter.Builtin.Pwd, + Interpreter.Builtin.Rm, + Interpreter.Builtin.Which, + Interpreter.Builtin.Mkdir, + Interpreter.Builtin.Mkdir.ShellMkdirOutputTask, + Interpreter.Builtin.Touch, + Interpreter.Builtin.Touch.ShellTouchOutputTask, + Interpreter.Builtin.Cat, + Interpreter.Builtin.Exit, + Interpreter.Builtin.True, + Interpreter.Builtin.False, + Interpreter.Builtin.Yes, + Interpreter.Builtin.Seq, + Interpreter.Builtin.Dirname, + Interpreter.Builtin.Basename, + Interpreter.Builtin.Cp, + Interpreter.Builtin.Cp.ShellCpOutputTask, + shell.subproc.PipeReader.CapturedWriter, +}); + +/// TODO: This function and `drainBufferedData` are copy pastes from +/// `PipeWriter.zig`, it would be nice to not have to do that +fn tryWriteWithWriteFn(fd: bun.FileDescriptor, buf: []const u8, comptime write_fn: *const fn (bun.FileDescriptor, []const u8) JSC.Maybe(usize)) bun.io.WriteResult { + var offset: usize = 0; + + while (offset < buf.len) { + switch (write_fn(fd, buf[offset..])) { + .err => |err| { + if (err.isRetry()) { + return .{ .pending = offset }; + } + + if (err.getErrno() == .PIPE) { + return .{ .done = offset }; + } + + return .{ .err = err }; + }, + + .result => |wrote| { + offset += wrote; + if (wrote == 0) { + return .{ .done = offset }; + } + }, + } + } + + return .{ .wrote = offset }; +} + +pub fn drainBufferedData(parent: *IOWriter, buf: []const u8, max_write_size: usize, received_hup: bool) bun.io.WriteResult { + _ = received_hup; + + const trimmed = if (max_write_size < buf.len and max_write_size > 0) buf[0..max_write_size] else buf; + + var drained: usize = 0; + + while (drained < trimmed.len) { + const attempt = tryWriteWithWriteFn(parent.fd, buf, bun.sys.write); + switch (attempt) { + .pending => |pending| { + drained += pending; + return .{ .pending = drained }; + }, + .wrote => |amt| { + drained += amt; + }, + .err => |err| { + if (drained > 0) { + onError(parent, err); + return .{ .wrote = drained }; + } else { + return .{ .err = err }; + } + }, + .done => |amt| { + drained += amt; + return .{ .done = drained }; + }, + } + } + + return .{ .wrote = drained }; +} + +/// TODO: Investigate what we need to do to remove this since we did most of the leg +/// work in removing recursion in the shell. That is what caused the need for +/// making deinitialization asynchronous in the first place. +/// +/// There are two areas which need to change: +/// +/// 1. `IOWriter.onWritePollable` calls `this.bump(child).run()` which could +/// deinitialize the child which will deref and potentially deinitalize the +/// `IOWriter`. Simple solution is to ref and defer ref the `IOWriter` +/// +/// 2. `PipeWriter` seems to try to use this struct after IOWriter +/// deinitializes. We might not be able to get around this. +pub const AsyncDeinitWriter = struct { + ran: bool = false, + + pub fn enqueue(this: *@This()) void { + if (this.ran) return; + this.ran = true; + + var iowriter = this.writer(); + + if (iowriter.evtloop == .js) { + iowriter.evtloop.js.enqueueTaskConcurrent(iowriter.concurrent_task.js.from(this, .manual_deinit)); + } else { + iowriter.evtloop.mini.enqueueTaskConcurrent(iowriter.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } + + pub fn writer(this: *@This()) *IOWriter { + return @alignCast(@fieldParentPtr("async_deinit", this)); + } + + pub fn runFromMainThread(this: *@This()) void { + this.writer().deinitOnMainThread(); + } + + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); } }; const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = shell.Interpreter; const JSC = bun.JSC; @@ -547,4 +837,3 @@ const assert = bun.assert; const log = bun.Output.scoped(.IOWriter, true); const SmolList = shell.SmolList; const Maybe = JSC.Maybe; -const AsyncDeinitWriter = shell.Interpreter.AsyncDeinitWriter; diff --git a/src/shell/Yield.zig b/src/shell/Yield.zig new file mode 100644 index 0000000000..cac815bb4b --- /dev/null +++ b/src/shell/Yield.zig @@ -0,0 +1,164 @@ +/// There are constraints on Bun's shell interpreter which are unique to shells in +/// general: +/// 1. We try to keep everything in the Bun process as much as possible for +/// performance reasons and also to leverage Bun's existing IO/FS code +/// 2. We try to use non-blocking operations as much as possible so the shell +/// does not interfere with the JS event loop +/// 3. Zig does not have coroutines (yet) +/// +/// These cause two problems: +/// 1. Unbounded recursion, if we keep calling .next() on state machine structs +/// then the call stack could get really deep, we need some mechanism to allow +/// execution to continue without blowing up the call stack +/// +/// 2. Correctly handling suspension points. These occur when IO would block so +/// we must, for example, wait for epoll/kqueue. The easiest solution is to have +/// functions return some value indicating that they suspended execution of the +/// interpreter. +/// +/// This `Yield` struct solves these problems. It represents a "continuation" of +/// the shell interpreter. Shell interpreter functions must return this value. +/// At the top-level of execution, `Yield.run(...)` serves as a "trampoline" to +/// drive execution without blowing up the callstack. +/// +/// Note that the "top-level of execution" could be in `Interpreter.run` or when +/// shell execution resumes after suspension in a task callback (for example in +/// IOWriter.onWritePoll). +pub const Yield = union(enum) { + script: *Script, + stmt: *Stmt, + pipeline: *Pipeline, + cmd: *Cmd, + assigns: *Assigns, + expansion: *Expansion, + @"if": *If, + subshell: *Subshell, + cond_expr: *CondExpr, + + /// This can occur if data is written using IOWriter and it immediately + /// completes (e.g. the buf to write was empty or the fd was immediately + /// writeable). + /// + /// When that happens, we return this variant to ensure that the + /// `.onIOWriterChunk` is called at the top of the callstack. + /// + /// TODO: this struct is massive, also I think we can remove this since + /// it is only used in 2 places. we might need to implement signals + /// first tho. + on_io_writer_chunk: struct { + err: ?JSC.SystemError, + written: usize, + /// This type is actually `IOWriterChildPtr`, but because + /// of an annoying cyclic Zig compile error we're doing this + /// quick fix of making it `*anyopaque`. + child: *anyopaque, + }, + + suspended, + /// Failed and throwed a JS error + failed, + done, + + /// Used in debug builds to ensure the shell is not creating a callstack + /// that is too deep. + threadlocal var _dbg_catch_exec_within_exec: if (Environment.isDebug) usize else u0 = 0; + + /// Ideally this should be 1, but since we actually call the `resolve` of the Promise in + /// Interpreter.finish it could actually result in another shell script running. + const MAX_DEPTH = 2; + + pub fn isDone(this: *const Yield) bool { + return this.* == .done; + } + + pub fn run(this: Yield) void { + if (comptime Environment.isDebug) log("Yield({s}) _dbg_catch_exec_within_exec = {d} + 1 = {d}", .{ @tagName(this), _dbg_catch_exec_within_exec, _dbg_catch_exec_within_exec + 1 }); + bun.debugAssert(_dbg_catch_exec_within_exec <= MAX_DEPTH); + if (comptime Environment.isDebug) _dbg_catch_exec_within_exec += 1; + defer { + if (comptime Environment.isDebug) log("Yield({s}) _dbg_catch_exec_within_exec = {d} - 1 = {d}", .{ @tagName(this), _dbg_catch_exec_within_exec, _dbg_catch_exec_within_exec + 1 }); + if (comptime Environment.isDebug) _dbg_catch_exec_within_exec -= 1; + } + + // A pipeline creates multiple "threads" of execution: + // + // ```bash + // cmd1 | cmd2 | cmd3 + // ``` + // + // We need to start cmd1, go back to the pipeline, start cmd2, and so + // on. + // + // This means we need to store a reference to the pipeline. And + // there can be nested pipelines, so we need a stack. + var sfb = std.heap.stackFallback(@sizeOf(*Pipeline) * 4, bun.default_allocator); + const alloc = sfb.get(); + var pipeline_stack = std.ArrayList(*Pipeline).initCapacity(alloc, 4) catch bun.outOfMemory(); + defer pipeline_stack.deinit(); + + // Note that we're using labelled switch statements but _not_ + // re-assigning `this`, so the `this` variable is stale after the first + // execution. Don't touch it. + state: switch (this) { + .pipeline => |x| { + pipeline_stack.append(x) catch bun.outOfMemory(); + continue :state x.next(); + }, + .cmd => |x| continue :state x.next(), + .script => |x| continue :state x.next(), + .stmt => |x| continue :state x.next(), + .assigns => |x| continue :state x.next(), + .expansion => |x| continue :state x.next(), + .@"if" => |x| continue :state x.next(), + .subshell => |x| continue :state x.next(), + .cond_expr => |x| continue :state x.next(), + .on_io_writer_chunk => |x| { + const child = IOWriterChildPtr.fromAnyOpaque(x.child); + continue :state child.onIOWriterChunk(x.written, x.err); + }, + .failed, .suspended, .done => { + if (drainPipelines(&pipeline_stack)) |yield| { + continue :state yield; + } + return; + }, + } + } + + pub fn drainPipelines(pipeline_stack: *std.ArrayList(*Pipeline)) ?Yield { + if (pipeline_stack.items.len == 0) return null; + var i: i64 = @as(i64, @intCast(pipeline_stack.items.len)) - 1; + while (i >= 0 and i < pipeline_stack.items.len) : (i -= 1) { + const pipeline = pipeline_stack.items[@intCast(i)]; + if (pipeline.state == .starting_cmds) return pipeline.next(); + _ = pipeline_stack.pop(); + if (pipeline.state == .done) { + return pipeline.next(); + } + } + return null; + } +}; + +const std = @import("std"); +const bun = @import("bun"); +const Environment = bun.Environment; +const shell = bun.shell; + +const Interpreter = bun.shell.Interpreter; +const IO = bun.shell.Interpreter.IO; +const log = bun.shell.interpret.log; +const IOWriter = bun.shell.Interpreter.IOWriter; +const IOWriterChildPtr = IOWriter.IOWriterChildPtr; + +const Assigns = bun.shell.Interpreter.Assigns; +const Script = bun.shell.Interpreter.Script; +const Subshell = bun.shell.Interpreter.Subshell; +const Cmd = bun.shell.Interpreter.Cmd; +const If = bun.shell.Interpreter.If; +const CondExpr = bun.shell.Interpreter.CondExpr; +const Expansion = bun.shell.Interpreter.Expansion; +const Stmt = bun.shell.Interpreter.Stmt; +const Pipeline = bun.shell.Interpreter.Pipeline; + +const JSC = bun.JSC; diff --git a/src/shell/builtin/basename.zig b/src/shell/builtin/basename.zig index 1e04d32fee..e296f51bf5 100644 --- a/src/shell/builtin/basename.zig +++ b/src/shell/builtin/basename.zig @@ -1,7 +1,7 @@ -state: enum { idle, waiting_io, err, done } = .idle, +state: enum { idle, err, done } = .idle, buf: std.ArrayListUnmanaged(u8) = .{}, -pub fn start(this: *@This()) Maybe(void) { +pub fn start(this: *@This()) Yield { const args = this.bltn().argsSlice(); var iter = bun.SliceIterator([*:0]const u8).init(args); @@ -9,17 +9,15 @@ pub fn start(this: *@This()) Maybe(void) { while (iter.next()) |item| { const arg = bun.sliceTo(item, 0); - _ = this.print(bun.path.basename(arg)); - _ = this.print("\n"); + this.print(bun.path.basename(arg)); + this.print("\n"); } this.state = .done; if (this.bltn().stdout.needsIO()) |safeguard| { - this.bltn().stdout.enqueue(this, this.buf.items, safeguard); - } else { - this.bltn().done(0); + return this.bltn().stdout.enqueue(this, this.buf.items, safeguard); } - return Maybe(void).success; + return this.bltn().done(0); } pub fn deinit(this: *@This()) void { @@ -27,38 +25,33 @@ pub fn deinit(this: *@This()) void { //basename } -fn fail(this: *@This(), msg: []const u8) Maybe(void) { +fn fail(this: *@This(), msg: []const u8) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .err; - this.bltn().stderr.enqueue(this, msg, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, msg, safeguard); } _ = this.bltn().writeNoIO(.stderr, msg); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } -fn print(this: *@This(), msg: []const u8) Maybe(void) { +fn print(this: *@This(), msg: []const u8) void { if (this.bltn().stdout.needsIO() != null) { this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory(); - return Maybe(void).success; + return; } - const res = this.bltn().writeNoIO(.stdout, msg); - if (res == .err) return Maybe(void).initErr(res.err); - return Maybe(void).success; + _ = this.bltn().writeNoIO(.stdout, msg); } -pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) Yield { if (maybe_e) |e| { defer e.deref(); this.state = .err; - this.bltn().done(1); - return; + return this.bltn().done(1); } switch (this.state) { - .done => this.bltn().done(0), - .err => this.bltn().done(1), - else => {}, + .done => return this.bltn().done(0), + .err => return this.bltn().done(1), + .idle => bun.shell.unreachableState("Basename.onIOWriterChunk", "idle"), } } @@ -68,9 +61,9 @@ pub inline fn bltn(this: *@This()) *Builtin { } const bun = @import("bun"); +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); diff --git a/src/shell/builtin/cat.zig b/src/shell/builtin/cat.zig index b9ee5e1667..340b44a674 100644 --- a/src/shell/builtin/cat.zig +++ b/src/shell/builtin/cat.zig @@ -24,20 +24,18 @@ state: union(enum) { done, } = .idle, -pub fn writeFailingError(this: *Cat, buf: []const u8, exit_code: ExitCode) Maybe(void) { +pub fn writeFailingError(this: *Cat, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } -pub fn start(this: *Cat) Maybe(void) { +pub fn start(this: *Cat) Yield { const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) { .ok => |filepath_args| filepath_args, .err => |e| { @@ -47,8 +45,7 @@ pub fn start(this: *Cat) Maybe(void) { .unsupported => |unsupported| this.bltn().fmtErrorArena(.cat, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), }; - _ = this.writeFailingError(buf, 1); - return Maybe(void).success; + return this.writeFailingError(buf, 1); }, }; @@ -66,12 +63,10 @@ pub fn start(this: *Cat) Maybe(void) { }; } - _ = this.next(); - - return Maybe(void).success; + return this.next(); } -pub fn next(this: *Cat) void { +pub fn next(this: *Cat) Yield { switch (this.state) { .idle => @panic("Invalid state"), .exec_stdin => { @@ -79,17 +74,13 @@ pub fn next(this: *Cat) void { this.state.exec_stdin.in_done = true; const buf = this.bltn().readStdinNoIO(); if (this.bltn().stdout.needsIO()) |safeguard| { - this.bltn().stdout.enqueue(this, buf, safeguard); - } else { - _ = this.bltn().writeNoIO(.stdout, buf); - this.bltn().done(0); - return; + return this.bltn().stdout.enqueue(this, buf, safeguard); } - return; + _ = this.bltn().writeNoIO(.stdout, buf); + return this.bltn().done(0); } this.bltn().stdin.fd.addReader(this); - this.bltn().stdin.fd.start(); - return; + return this.bltn().stdin.fd.start(); }, .exec_filepath_args => { var exec = &this.state.exec_filepath_args; @@ -107,9 +98,8 @@ pub fn next(this: *Cat) void { .result => |fd| fd, .err => |e| { const buf = this.bltn().taskErrorToString(.cat, e); - _ = this.writeFailingError(buf, 1); - exec.deinit(); - return; + defer exec.deinit(); + return this.writeFailingError(buf, 1); }, }; @@ -118,14 +108,14 @@ pub fn next(this: *Cat) void { exec.chunks_queued = 0; exec.reader = reader; exec.reader.?.addReader(this); - exec.reader.?.start(); + return exec.reader.?.start(); }, - .waiting_write_err => return, - .done => this.bltn().done(0), + .waiting_write_err => return .failed, + .done => return this.bltn().done(0), } } -pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) Yield { debug("onIOWriterChunk(0x{x}, {s}, had_err={any})", .{ @intFromPtr(this), @tagName(this.state), err != null }); const errno: ExitCode = if (err) |e| brk: { defer e.deref(); @@ -144,7 +134,7 @@ pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void { } this.state.exec_stdin.in_done = true; } - this.bltn().done(e.getErrno()); + return this.bltn().done(e.getErrno()); }, .exec_filepath_args => { var exec = &this.state.exec_filepath_args; @@ -152,22 +142,21 @@ pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void { r.removeReader(this); } exec.deinit(); - this.bltn().done(e.getErrno()); + return this.bltn().done(e.getErrno()); }, - .waiting_write_err => this.bltn().done(e.getErrno()), + .waiting_write_err => return this.bltn().done(e.getErrno()), else => @panic("Invalid state"), } - return; } switch (this.state) { .exec_stdin => { this.state.exec_stdin.chunks_done += 1; if (this.state.exec_stdin.in_done and (this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued)) { - this.bltn().done(0); - return; + return this.bltn().done(0); } // Need to wait for more chunks to be written + return .suspended; }, .exec_filepath_args => { this.state.exec_filepath_args.chunks_done += 1; @@ -175,42 +164,42 @@ pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void { this.state.exec_filepath_args.out_done = true; } if (this.state.exec_filepath_args.in_done and this.state.exec_filepath_args.out_done) { - this.next(); - return; + return this.next(); } // Wait for reader to be done - return; + return .suspended; }, - .waiting_write_err => this.bltn().done(1), + .waiting_write_err => return this.bltn().done(1), else => @panic("Invalid state"), } } -pub fn onIOReaderChunk(this: *Cat, chunk: []const u8) ReadChunkAction { +pub fn onIOReaderChunk(this: *Cat, chunk: []const u8, remove: *bool) Yield { debug("onIOReaderChunk(0x{x}, {s}, chunk_len={d})", .{ @intFromPtr(this), @tagName(this.state), chunk.len }); + remove.* = false; switch (this.state) { .exec_stdin => { if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec_stdin.chunks_queued += 1; - this.bltn().stdout.enqueue(this, chunk, safeguard); - return .cont; + return this.bltn().stdout.enqueue(this, chunk, safeguard); } _ = this.bltn().writeNoIO(.stdout, chunk); + return .done; }, .exec_filepath_args => { if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec_filepath_args.chunks_queued += 1; - this.bltn().stdout.enqueue(this, chunk, safeguard); - return .cont; + return this.bltn().stdout.enqueue(this, chunk, safeguard); } _ = this.bltn().writeNoIO(.stdout, chunk); + return .done; }, else => @panic("Invalid state"), } - return .cont; + return .done; } -pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void { +pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) Yield { const errno: ExitCode = if (err) |e| brk: { defer e.deref(); break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno()))); @@ -223,14 +212,13 @@ pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void { this.state.exec_stdin.in_done = true; if (errno != 0) { if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn().stdout.needsIO() == null) { - this.bltn().done(errno); - return; + return this.bltn().done(errno); } this.bltn().stdout.fd.writer.cancelChunks(this); - return; + return .suspended; } if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn().stdout.needsIO() == null) { - this.bltn().done(0); + return this.bltn().done(0); } }, .exec_filepath_args => { @@ -238,18 +226,19 @@ pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void { if (errno != 0) { if (this.state.exec_filepath_args.out_done or this.bltn().stdout.needsIO() == null) { this.state.exec_filepath_args.deinit(); - this.bltn().done(errno); - return; + return this.bltn().done(errno); } this.bltn().stdout.fd.writer.cancelChunks(this); - return; + return .suspended; } if (this.state.exec_filepath_args.out_done or (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) or this.bltn().stdout.needsIO() == null) { - this.next(); + return this.next(); } }, .done, .waiting_write_err, .idle => {}, } + + return .suspended; } pub fn deinit(_: *Cat) void {} @@ -342,6 +331,7 @@ const Opts = struct { const debug = bun.Output.scoped(.ShellCat, true); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; @@ -352,9 +342,7 @@ const ParseFlagResult = interpreter.ParseFlagResult; const ExitCode = shell.ExitCode; const IOReader = shell.IOReader; const Cat = @This(); -const ReadChunkAction = interpreter.ReadChunkAction; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const FlagParser = interpreter.FlagParser; diff --git a/src/shell/builtin/cd.zig b/src/shell/builtin/cd.zig index 532477b42b..37980bfd08 100644 --- a/src/shell/builtin/cd.zig +++ b/src/shell/builtin/cd.zig @@ -9,24 +9,21 @@ state: union(enum) { err: Syscall.Error, } = .idle, -fn writeStderrNonBlocking(this: *Cd, comptime fmt: []const u8, args: anytype) void { +fn writeStderrNonBlocking(this: *Cd, comptime fmt: []const u8, args: anytype) Yield { this.state = .waiting_write_stderr; if (this.bltn().stderr.needsIO()) |safeguard| { - this.bltn().stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard); - } else { - const buf = this.bltn().fmtErrorArena(.cd, fmt, args); - _ = this.bltn().writeNoIO(.stderr, buf); - this.state = .done; - this.bltn().done(1); + return this.bltn().stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard); } + const buf = this.bltn().fmtErrorArena(.cd, fmt, args); + _ = this.bltn().writeNoIO(.stderr, buf); + this.state = .done; + return this.bltn().done(1); } -pub fn start(this: *Cd) Maybe(void) { +pub fn start(this: *Cd) Yield { const args = this.bltn().argsSlice(); if (args.len > 1) { - this.writeStderrNonBlocking("too many arguments\n", .{}); - // yield execution - return Maybe(void).success; + return this.writeStderrNonBlocking("too many arguments\n", .{}); } if (args.len == 1) { @@ -36,7 +33,10 @@ pub fn start(this: *Cd) Maybe(void) { switch (this.bltn().parentCmd().base.shell.changePrevCwd(this.bltn().parentCmd().base.interpreter)) { .result => {}, .err => |err| { - return this.handleChangeCwdErr(err, this.bltn().parentCmd().base.shell.prevCwdZ()); + return this.handleChangeCwdErr( + err, + this.bltn().parentCmd().base.shell.prevCwdZ(), + ); }, } }, @@ -57,11 +57,10 @@ pub fn start(this: *Cd) Maybe(void) { } } - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } -fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe(void) { +fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Yield { const errno: usize = @intCast(err.errno); switch (errno) { @@ -70,44 +69,37 @@ fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe const buf = this.bltn().fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_}); _ = this.bltn().writeNoIO(.stderr, buf); this.state = .done; - this.bltn().done(1); - // yield execution - return Maybe(void).success; + return this.bltn().done(1); } - this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_}); - return Maybe(void).success; + return this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_}); }, @as(usize, @intFromEnum(Syscall.E.NOENT)) => { if (this.bltn().stderr.needsIO() == null) { const buf = this.bltn().fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_}); _ = this.bltn().writeNoIO(.stderr, buf); this.state = .done; - this.bltn().done(1); - // yield execution - return Maybe(void).success; + return this.bltn().done(1); } - this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_}); - return Maybe(void).success; + return this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_}); }, - else => return Maybe(void).success, + else => return .failed, } } -pub fn onIOWriterChunk(this: *Cd, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Cd, _: usize, e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .waiting_write_stderr); } if (e != null) { defer e.?.deref(); - this.bltn().done(e.?.getErrno()); - return; + return this.bltn().done(e.?.getErrno()); } this.state = .done; - this.bltn().done(1); + return this.bltn().done(1); } pub inline fn bltn(this: *Cd) *Builtin { @@ -123,13 +115,13 @@ pub fn deinit(this: *Cd) void { // -- const log = bun.Output.scoped(.Cd, true); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const Cd = @This(); const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const Syscall = bun.sys; diff --git a/src/shell/builtin/cp.zig b/src/shell/builtin/cp.zig index a4f6a06333..638e3485be 100644 --- a/src/shell/builtin/cp.zig +++ b/src/shell/builtin/cp.zig @@ -64,7 +64,7 @@ const EbusyState = struct { } }; -pub fn start(this: *Cp) Maybe(void) { +pub fn start(this: *Cp) Yield { const maybe_filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) { .ok => |args| args, .err => |e| { @@ -74,14 +74,12 @@ pub fn start(this: *Cp) Maybe(void) { .unsupported => |unsupported| this.bltn().fmtErrorArena(.cp, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), }; - _ = this.writeFailingError(buf, 1); - return Maybe(void).success; + return this.writeFailingError(buf, 1); }, }; if (maybe_filepath_args == null or maybe_filepath_args.?.len <= 1) { - _ = this.writeFailingError(Builtin.Kind.cp.usageString(), 1); - return Maybe(void).success; + return this.writeFailingError(Builtin.Kind.cp.usageString(), 1); } const args = maybe_filepath_args orelse unreachable; @@ -93,12 +91,10 @@ pub fn start(this: *Cp) Maybe(void) { .paths_to_copy = paths_to_copy, } }; - this.next(); - - return Maybe(void).success; + return this.next(); } -pub fn ignoreEbusyErrorIfPossible(this: *Cp) void { +pub fn ignoreEbusyErrorIfPossible(this: *Cp) Yield { if (!bun.Environment.isWindows) @compileError("dont call this plz"); if (this.state.ebusy.idx < this.state.ebusy.state.tasks.items.len) { @@ -115,18 +111,17 @@ pub fn ignoreEbusyErrorIfPossible(this: *Cp) void { continue :outer_loop; } this.state.ebusy.idx += i + 1; - this.printShellCpTask(task); - return; + return this.printShellCpTask(task); } } this.state.ebusy.state.deinit(); const exit_code = this.state.ebusy.main_exit_code; this.state = .done; - this.bltn().done(exit_code); + return this.bltn().done(exit_code); } -pub fn next(this: *Cp) void { +pub fn next(this: *Cp) Yield { while (this.state != .done) { switch (this.state) { .idle => @panic("Invalid state for \"Cp\": idle, this indicates a bug in Bun. Please file a GitHub issue"), @@ -146,10 +141,9 @@ pub fn next(this: *Cp) void { exec.ebusy.deinit(); } this.state = .done; - this.bltn().done(exit_code); - return; + return this.bltn().done(exit_code); } - return; + return .suspended; } exec.started = true; @@ -163,46 +157,44 @@ pub fn next(this: *Cp) void { const cp_task = ShellCpTask.create(this, this.bltn().eventLoop(), this.opts, 1 + exec.paths_to_copy.len, path, exec.target_path, cwd_path); cp_task.schedule(); } - return; + return .suspended; }, .ebusy => { if (comptime bun.Environment.isWindows) { - this.ignoreEbusyErrorIfPossible(); - return; - } else @panic("Should only be called on Windows"); + return this.ignoreEbusyErrorIfPossible(); + } + @panic("Should only be called on Windows"); }, - .waiting_write_err => return, + .waiting_write_err => return .failed, .done => unreachable, } } - this.bltn().done(0); + return this.bltn().done(0); } pub fn deinit(cp: *Cp) void { assert(cp.state == .done or cp.state == .waiting_write_err); } -pub fn writeFailingError(this: *Cp, buf: []const u8, exit_code: ExitCode) Maybe(void) { +pub fn writeFailingError(this: *Cp, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } -pub fn onIOWriterChunk(this: *Cp, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Cp, _: usize, e: ?JSC.SystemError) Yield { if (e) |err| err.deref(); if (this.state == .waiting_write_err) { return this.bltn().done(1); } this.state.exec.output_done += 1; - this.next(); + return this.next(); } pub inline fn bltn(this: *@This()) *Builtin { @@ -226,7 +218,7 @@ pub fn onShellCpTaskDone(this: *Cp, task: *ShellCpTask) void { { log("{} got ebusy {d} {d}", .{ this, this.state.exec.ebusy.tasks.items.len, this.state.exec.paths_to_copy.len }); this.state.exec.ebusy.tasks.append(bun.default_allocator, task) catch bun.outOfMemory(); - this.next(); + this.next().run(); return; } } else { @@ -239,10 +231,10 @@ pub fn onShellCpTaskDone(this: *Cp, task: *ShellCpTask) void { } } - this.printShellCpTask(task); + this.printShellCpTask(task).run(); } -pub fn printShellCpTask(this: *Cp, task: *ShellCpTask) void { +pub fn printShellCpTask(this: *Cp, task: *ShellCpTask) Yield { // Deinitialize this task as we are starting a new one defer task.deinit(); @@ -256,10 +248,9 @@ pub fn printShellCpTask(this: *Cp, task: *ShellCpTask) void { if (bun.take(&task.err)) |err| { this.state.exec.err = err; const error_string = this.bltn().taskErrorToString(.cp, this.state.exec.err.?); - output_task.start(error_string); - return; + return output_task.start(error_string); } - output_task.start(null); + return output_task.start(null); } pub const ShellCpOutputTask = OutputTask(Cp, .{ @@ -271,36 +262,34 @@ pub const ShellCpOutputTask = OutputTask(Cp, .{ }); const ShellCpOutputTaskVTable = struct { - pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) CoroutineResult { + pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) ?Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stderr.enqueue(childptr, errbuf, safeguard); - return .yield; + return this.bltn().stderr.enqueue(childptr, errbuf, safeguard); } _ = this.bltn().writeNoIO(.stderr, errbuf); - return .cont; + return null; } pub fn onWriteErr(this: *Cp) void { this.state.exec.output_done += 1; } - pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) CoroutineResult { + pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) ?Yield { if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stdout.enqueue(childptr, output.slice(), safeguard); - return .yield; + return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard); } _ = this.bltn().writeNoIO(.stdout, output.slice()); - return .cont; + return null; } pub fn onWriteOut(this: *Cp) void { this.state.exec.output_done += 1; } - pub fn onDone(this: *Cp) void { - this.next(); + pub fn onDone(this: *Cp) Yield { + return this.next(); } }; @@ -743,6 +732,7 @@ const ArrayList = std.ArrayList; const Syscall = bun.sys; const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; @@ -751,7 +741,6 @@ const ParseError = interpreter.ParseError; const ParseFlagResult = interpreter.ParseFlagResult; const ExitCode = shell.ExitCode; const Cp = @This(); -const CoroutineResult = interpreter.CoroutineResult; const OutputTask = interpreter.OutputTask; const assert = bun.assert; diff --git a/src/shell/builtin/dirname.zig b/src/shell/builtin/dirname.zig index 64ad7faf10..169a8e88e8 100644 --- a/src/shell/builtin/dirname.zig +++ b/src/shell/builtin/dirname.zig @@ -1,7 +1,7 @@ -state: enum { idle, waiting_io, err, done } = .idle, +state: enum { idle, err, done } = .idle, buf: std.ArrayListUnmanaged(u8) = .{}, -pub fn start(this: *@This()) Maybe(void) { +pub fn start(this: *@This()) Yield { const args = this.bltn().argsSlice(); var iter = bun.SliceIterator([*:0]const u8).init(args); @@ -15,11 +15,9 @@ pub fn start(this: *@This()) Maybe(void) { this.state = .done; if (this.bltn().stdout.needsIO()) |safeguard| { - this.bltn().stdout.enqueue(this, this.buf.items, safeguard); - } else { - this.bltn().done(0); + return this.bltn().stdout.enqueue(this, this.buf.items, safeguard); } - return Maybe(void).success; + return this.bltn().done(0); } pub fn deinit(this: *@This()) void { @@ -27,15 +25,13 @@ pub fn deinit(this: *@This()) void { //dirname } -fn fail(this: *@This(), msg: []const u8) Maybe(void) { +fn fail(this: *@This(), msg: []const u8) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .err; - this.bltn().stderr.enqueue(this, msg, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, msg, safeguard); } _ = this.bltn().writeNoIO(.stderr, msg); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } fn print(this: *@This(), msg: []const u8) Maybe(void) { @@ -48,17 +44,16 @@ fn print(this: *@This(), msg: []const u8) Maybe(void) { return Maybe(void).success; } -pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) Yield { if (maybe_e) |e| { defer e.deref(); this.state = .err; - this.bltn().done(1); - return; + return this.bltn().done(1); } switch (this.state) { - .done => this.bltn().done(0), - .err => this.bltn().done(1), - else => {}, + .done => return this.bltn().done(0), + .err => return this.bltn().done(1), + .idle => bun.shell.unreachableState("Dirname.onIOWriterChunk", "idle"), } } @@ -69,6 +64,7 @@ pub inline fn bltn(this: *@This()) *Builtin { // -- const bun = @import("bun"); +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; diff --git a/src/shell/builtin/echo.zig b/src/shell/builtin/echo.zig index 321d4b0140..ea6b0d8830 100644 --- a/src/shell/builtin/echo.zig +++ b/src/shell/builtin/echo.zig @@ -4,14 +4,19 @@ output: std.ArrayList(u8), state: union(enum) { idle, waiting, + waiting_write_err, done, } = .idle, -pub fn start(this: *Echo) Maybe(void) { - const args = this.bltn().argsSlice(); +pub fn start(this: *Echo) Yield { + var args = this.bltn().argsSlice(); + const no_newline = args.len >= 1 and std.mem.eql(u8, bun.sliceTo(args[0], 0), "-n"); - var has_leading_newline: bool = false; + args = args[if (no_newline) 1 else 0..]; const args_len = args.len; + var has_leading_newline: bool = false; + + // TODO: Should flush buffer after it gets to a certain size for (args, 0..) |arg, i| { const thearg = std.mem.span(arg); if (i < args_len - 1) { @@ -25,32 +30,30 @@ pub fn start(this: *Echo) Maybe(void) { } } - if (!has_leading_newline) this.output.append('\n') catch bun.outOfMemory(); + if (!has_leading_newline and !no_newline) this.output.append('\n') catch bun.outOfMemory(); if (this.bltn().stdout.needsIO()) |safeguard| { this.state = .waiting; - this.bltn().stdout.enqueue(this, this.output.items[0..], safeguard); - return Maybe(void).success; + return this.bltn().stdout.enqueue(this, this.output.items[0..], safeguard); } _ = this.bltn().writeNoIO(.stdout, this.output.items[0..]); this.state = .done; - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } -pub fn onIOWriterChunk(this: *Echo, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Echo, _: usize, e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { - assert(this.state == .waiting); + assert(this.state == .waiting or this.state == .waiting_write_err); } if (e != null) { defer e.?.deref(); - this.bltn().done(e.?.getErrno()); - return; + return this.bltn().done(e.?.getErrno()); } this.state = .done; - this.bltn().done(0); + const exit_code: ExitCode = if (this.state == .waiting_write_err) 1 else 0; + return this.bltn().done(exit_code); } pub fn deinit(this: *Echo) void { @@ -63,15 +66,15 @@ pub inline fn bltn(this: *Echo) *Builtin { return @fieldParentPtr("impl", impl); } -// -- const log = bun.Output.scoped(.echo, true); const bun = @import("bun"); +const ExitCode = bun.shell.ExitCode; +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const Echo = @This(); const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const assert = bun.assert; diff --git a/src/shell/builtin/exit.zig b/src/shell/builtin/exit.zig index c9e98830aa..96d47d563b 100644 --- a/src/shell/builtin/exit.zig +++ b/src/shell/builtin/exit.zig @@ -5,12 +5,11 @@ state: enum { done, } = .idle, -pub fn start(this: *Exit) Maybe(void) { +pub fn start(this: *Exit) Yield { const args = this.bltn().argsSlice(); switch (args.len) { 0 => { - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); }, 1 => { const first_arg = args[0][0..std.mem.len(args[0]) :0]; @@ -18,8 +17,7 @@ pub fn start(this: *Exit) Maybe(void) { error.Overflow => @intCast((std.fmt.parseInt(usize, first_arg, 10) catch return this.fail("exit: numeric argument required\n")) % 256), error.InvalidCharacter => return this.fail("exit: numeric argument required\n"), }; - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); }, else => { return this.fail("exit: too many arguments\n"); @@ -27,46 +25,41 @@ pub fn start(this: *Exit) Maybe(void) { } } -fn fail(this: *Exit, msg: []const u8) Maybe(void) { +fn fail(this: *Exit, msg: []const u8) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_io; - this.bltn().stderr.enqueue(this, msg, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, msg, safeguard); } _ = this.bltn().writeNoIO(.stderr, msg); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } -pub fn next(this: *Exit) void { +pub fn next(this: *Exit) Yield { switch (this.state) { - .idle => @panic("Unexpected \"idle\" state in Exit. This indicates a bug in Bun. Please file a GitHub issue."), + .idle => shell.unreachableState("Exit.next", "idle"), .waiting_io => { - return; + return .suspended; }, .err => { - this.bltn().done(1); - return; + return this.bltn().done(1); }, .done => { - this.bltn().done(1); - return; + return this.bltn().done(1); }, } } -pub fn onIOWriterChunk(this: *Exit, _: usize, maybe_e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Exit, _: usize, maybe_e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .waiting_io); } if (maybe_e) |e| { defer e.deref(); this.state = .err; - this.next(); - return; + return this.next(); } this.state = .done; - this.next(); + return this.next(); } pub fn deinit(this: *Exit) void { @@ -81,13 +74,13 @@ pub inline fn bltn(this: *Exit) *Builtin { // -- const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const ExitCode = shell.ExitCode; const Exit = @This(); const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const assert = bun.assert; diff --git a/src/shell/builtin/export.zig b/src/shell/builtin/export.zig index 2ad6bcb464..5e4b424f4b 100644 --- a/src/shell/builtin/export.zig +++ b/src/shell/builtin/export.zig @@ -9,21 +9,19 @@ const Entry = struct { } }; -pub fn writeOutput(this: *Export, comptime io_kind: @Type(.enum_literal), comptime fmt: []const u8, args: anytype) Maybe(void) { +pub fn writeOutput(this: *Export, comptime io_kind: @Type(.enum_literal), comptime fmt: []const u8, args: anytype) Yield { if (this.bltn().stdout.needsIO()) |safeguard| { var output: *BuiltinIO.Output = &@field(this.bltn(), @tagName(io_kind)); this.printing = true; - output.enqueueFmtBltn(this, .@"export", fmt, args, safeguard); - return Maybe(void).success; + return output.enqueueFmtBltn(this, .@"export", fmt, args, safeguard); } const buf = this.bltn().fmtErrorArena(.@"export", fmt, args); _ = this.bltn().writeNoIO(io_kind, buf); - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } -pub fn onIOWriterChunk(this: *Export, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Export, _: usize, e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.printing); } @@ -33,10 +31,10 @@ pub fn onIOWriterChunk(this: *Export, _: usize, e: ?JSC.SystemError) void { break :brk @intFromEnum(e.?.getErrno()); } else 0; - this.bltn().done(exit_code); + return this.bltn().done(exit_code); } -pub fn start(this: *Export) Maybe(void) { +pub fn start(this: *Export) Yield { const args = this.bltn().argsSlice(); // Calling `export` with no arguments prints all exported variables lexigraphically ordered @@ -72,14 +70,11 @@ pub fn start(this: *Export) Maybe(void) { if (this.bltn().stdout.needsIO()) |safeguard| { this.printing = true; - this.bltn().stdout.enqueue(this, buf, safeguard); - - return Maybe(void).success; + return this.bltn().stdout.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stdout, buf); - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } for (args) |arg_raw| { @@ -101,8 +96,7 @@ pub fn start(this: *Export) Maybe(void) { this.bltn().parentCmd().base.shell.assignVar(this.bltn().parentCmd().base.interpreter, EnvStr.initSlice(label), EnvStr.initSlice(value), .exported); } - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } pub fn deinit(this: *Export) void { @@ -118,6 +112,7 @@ pub inline fn bltn(this: *Export) *Builtin { // -- const debug = bun.Output.scoped(.ShellExport, true); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; @@ -125,7 +120,6 @@ const Builtin = Interpreter.Builtin; const ExitCode = shell.ExitCode; const Export = @This(); const JSC = bun.JSC; -const Maybe = JSC.Maybe; const std = @import("std"); const log = debug; const EnvStr = interpreter.EnvStr; diff --git a/src/shell/builtin/false.zig b/src/shell/builtin/false.zig index b838cd4ca0..a7d3111f2d 100644 --- a/src/shell/builtin/false.zig +++ b/src/shell/builtin/false.zig @@ -1,16 +1,15 @@ -pub fn start(this: *@This()) Maybe(void) { - this.bltn().done(1); - return Maybe(void).success; -} - -pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void { - // no IO is done +pub fn start(this: *@This()) Yield { + return this.bltn().done(1); } pub fn deinit(this: *@This()) void { _ = this; } +pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) Yield { + return .done; +} + pub inline fn bltn(this: *@This()) *Builtin { const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("false", this)); return @fieldParentPtr("impl", impl); @@ -18,9 +17,9 @@ pub inline fn bltn(this: *@This()) *Builtin { // -- const bun = @import("bun"); +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; diff --git a/src/shell/builtin/ls.zig b/src/shell/builtin/ls.zig index 0cd8549612..6ad1524388 100644 --- a/src/shell/builtin/ls.zig +++ b/src/shell/builtin/ls.zig @@ -13,25 +13,22 @@ state: union(enum) { done, } = .idle, -pub fn start(this: *Ls) Maybe(void) { - this.next(); - return Maybe(void).success; +pub fn start(this: *Ls) Yield { + return this.next(); } -pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Maybe(void) { +pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } -fn next(this: *Ls) void { +fn next(this: *Ls) Yield { while (!(this.state == .done)) { switch (this.state) { .idle => { @@ -44,8 +41,7 @@ fn next(this: *Ls) void { .show_usage => Builtin.Kind.ls.usageString(), }; - _ = this.writeFailingError(buf, 1); - return; + return this.writeFailingError(buf, 1); }, }; @@ -70,7 +66,6 @@ fn next(this: *Ls) void { } }, .exec => { - // It's done log("Ls(0x{x}, state=exec) Check: tasks_done={d} task_count={d} output_done={d} output_waiting={d}", .{ @intFromPtr(this), this.state.exec.tasks_done, @@ -78,34 +73,33 @@ fn next(this: *Ls) void { this.state.exec.output_done, this.state.exec.output_waiting, }); + // It's done if (this.state.exec.tasks_done >= this.state.exec.task_count.load(.monotonic) and this.state.exec.output_done >= this.state.exec.output_waiting) { const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; this.state = .done; - this.bltn().done(exit_code); - return; + return this.bltn().done(exit_code); } - return; + return .suspended; }, .waiting_write_err => { - return; + return .failed; }, .done => unreachable, } } - this.bltn().done(0); - return; + return this.bltn().done(0); } pub fn deinit(_: *Ls) void {} -pub fn onIOWriterChunk(this: *Ls, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Ls, _: usize, e: ?JSC.SystemError) Yield { if (e) |err| err.deref(); if (this.state == .waiting_write_err) { return this.bltn().done(1); } this.state.exec.output_done += 1; - this.next(); + return this.next(); } pub fn onShellLsTaskDone(this: *Ls, task: *ShellLsTask) void { @@ -124,10 +118,10 @@ pub fn onShellLsTaskDone(this: *Ls, task: *ShellLsTask) void { this.state.exec.err = err.*; task.err = null; const error_string = this.bltn().taskErrorToString(.ls, this.state.exec.err.?); - output_task.start(error_string); + output_task.start(error_string).run(); return; } - output_task.start(null); + output_task.start(null).run(); } pub const ShellLsOutputTask = OutputTask(Ls, .{ @@ -139,36 +133,40 @@ pub const ShellLsOutputTask = OutputTask(Ls, .{ }); const ShellLsOutputTaskVTable = struct { - pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) CoroutineResult { + pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) ?Yield { + log("ShellLsOutputTaskVTable.writeErr(0x{x}, {s})", .{ @intFromPtr(this), errbuf }); if (this.bltn().stderr.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stderr.enqueue(childptr, errbuf, safeguard); - return .yield; + return this.bltn().stderr.enqueue(childptr, errbuf, safeguard); } _ = this.bltn().writeNoIO(.stderr, errbuf); - return .cont; + return null; } pub fn onWriteErr(this: *Ls) void { + log("ShellLsOutputTaskVTable.onWriteErr(0x{x})", .{@intFromPtr(this)}); this.state.exec.output_done += 1; } - pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) CoroutineResult { + pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) ?Yield { + log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s})", .{ @intFromPtr(this), output.slice() }); if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stdout.enqueue(childptr, output.slice(), safeguard); - return .yield; + return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard); } + log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s}) no IO", .{ @intFromPtr(this), output.slice() }); _ = this.bltn().writeNoIO(.stdout, output.slice()); - return .cont; + return null; } pub fn onWriteOut(this: *Ls) void { + log("ShellLsOutputTaskVTable.onWriteOut(0x{x})", .{@intFromPtr(this)}); this.state.exec.output_done += 1; } - pub fn onDone(this: *Ls) void { - this.next(); + pub fn onDone(this: *Ls) Yield { + log("ShellLsOutputTaskVTable.onDone(0x{x})", .{@intFromPtr(this)}); + return this.next(); } }; @@ -780,6 +778,7 @@ pub inline fn bltn(this: *Ls) *Builtin { const Ls = @This(); const log = bun.Output.scoped(.ls, true); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; @@ -788,7 +787,6 @@ const Result = Interpreter.Builtin.Result; const ParseError = interpreter.ParseError; const ExitCode = shell.ExitCode; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const Syscall = bun.sys; const ShellSyscall = interpreter.ShellSyscall; @@ -796,4 +794,3 @@ const Allocator = std.mem.Allocator; const DirIterator = bun.DirIterator; const OutputTask = interpreter.OutputTask; const OutputSrc = interpreter.OutputSrc; -const CoroutineResult = interpreter.CoroutineResult; diff --git a/src/shell/builtin/mkdir.zig b/src/shell/builtin/mkdir.zig index 58921f0143..31e5dbe3db 100644 --- a/src/shell/builtin/mkdir.zig +++ b/src/shell/builtin/mkdir.zig @@ -14,7 +14,7 @@ state: union(enum) { done, } = .idle, -pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?JSC.SystemError) Yield { if (e) |err| err.deref(); switch (this.state) { @@ -25,13 +25,13 @@ pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?JSC.SystemError) void { .idle, .done => @panic("Invalid state"), } - this.next(); + return this.next(); } -pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) Maybe(void) { + +pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); @@ -39,11 +39,10 @@ pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) May // return .{ .err = e }; // } - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } -pub fn start(this: *Mkdir) Maybe(void) { +pub fn start(this: *Mkdir) Yield { const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) { .ok => |filepath_args| filepath_args, .err => |e| { @@ -53,12 +52,10 @@ pub fn start(this: *Mkdir) Maybe(void) { .unsupported => |unsupported| this.bltn().fmtErrorArena(.mkdir, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), }; - _ = this.writeFailingError(buf, 1); - return Maybe(void).success; + return this.writeFailingError(buf, 1); }, } orelse { - _ = this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1); - return Maybe(void).success; + return this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1); }; this.state = .{ @@ -67,12 +64,10 @@ pub fn start(this: *Mkdir) Maybe(void) { }, }; - _ = this.next(); - - return Maybe(void).success; + return this.next(); } -pub fn next(this: *Mkdir) void { +pub fn next(this: *Mkdir) Yield { switch (this.state) { .idle => @panic("Invalid state"), .exec => { @@ -82,10 +77,9 @@ pub fn next(this: *Mkdir) void { const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; if (this.state.exec.err) |e| e.deref(); this.state = .done; - this.bltn().done(exit_code); - return; + return this.bltn().done(exit_code); } - return; + return .suspended; } exec.started = true; @@ -96,9 +90,10 @@ pub fn next(this: *Mkdir) void { var task = ShellMkdirTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ()); task.schedule(); } + return .suspended; }, - .waiting_write_err => return, - .done => this.bltn().done(0), + .waiting_write_err => return .failed, + .done => return this.bltn().done(0), } } @@ -116,10 +111,10 @@ pub fn onShellMkdirTaskDone(this: *Mkdir, task: *ShellMkdirTask) void { if (err) |e| { const error_string = this.bltn().taskErrorToString(.mkdir, e); this.state.exec.err = e; - output_task.start(error_string); + output_task.start(error_string).run(); return; } - output_task.start(null); + output_task.start(null).run(); } pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{ @@ -131,38 +126,36 @@ pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{ }); const ShellMkdirOutputTaskVTable = struct { - pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) CoroutineResult { + pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) ?Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stderr.enqueue(childptr, errbuf, safeguard); - return .yield; + return this.bltn().stderr.enqueue(childptr, errbuf, safeguard); } _ = this.bltn().writeNoIO(.stderr, errbuf); - return .cont; + return null; } pub fn onWriteErr(this: *Mkdir) void { this.state.exec.output_done += 1; } - pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) CoroutineResult { + pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) ?Yield { if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; const slice = output.slice(); log("THE SLICE: {d} {s}", .{ slice.len, slice }); - this.bltn().stdout.enqueue(childptr, slice, safeguard); - return .yield; + return this.bltn().stdout.enqueue(childptr, slice, safeguard); } _ = this.bltn().writeNoIO(.stdout, output.slice()); - return .cont; + return null; } pub fn onWriteOut(this: *Mkdir) void { this.state.exec.output_done += 1; } - pub fn onDone(this: *Mkdir) void { - this.next(); + pub fn onDone(this: *Mkdir) Yield { + return this.next(); } }; @@ -379,6 +372,7 @@ pub inline fn bltn(this: *Mkdir) *Builtin { // -- const debug = bun.Output.scoped(.ShellMkdir, true); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; @@ -388,14 +382,12 @@ const ParseError = interpreter.ParseError; const ParseFlagResult = interpreter.ParseFlagResult; const ExitCode = shell.ExitCode; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); const FlagParser = interpreter.FlagParser; const Mkdir = @This(); const log = debug; const OutputTask = interpreter.OutputTask; -const CoroutineResult = interpreter.CoroutineResult; const OutputSrc = interpreter.OutputSrc; const WorkPool = bun.JSC.WorkPool; const ResolvePath = bun.path; diff --git a/src/shell/builtin/mv.zig b/src/shell/builtin/mv.zig index 3e6c2c21c2..358f7097c1 100644 --- a/src/shell/builtin/mv.zig +++ b/src/shell/builtin/mv.zig @@ -166,24 +166,22 @@ pub const ShellMvBatchedTask = struct { } }; -pub fn start(this: *Mv) Maybe(void) { +pub fn start(this: *Mv) Yield { return this.next(); } -pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Maybe(void) { +pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .{ .waiting_write_err = .{ .exit_code = exit_code } }; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } -pub fn next(this: *Mv) Maybe(void) { +pub fn next(this: *Mv) Yield { while (!(this.state == .done or this.state == .err)) { switch (this.state) { .idle => { @@ -210,10 +208,10 @@ pub fn next(this: *Mv) Maybe(void) { }, }; this.state.check_target.task.task.schedule(); - return Maybe(void).success; + return .suspended; }, .check_target => { - if (this.state.check_target.state == .running) return Maybe(void).success; + if (this.state.check_target.state == .running) return .suspended; const check_target = &this.state.check_target; if (comptime bun.Environment.allow_assert) { @@ -296,36 +294,32 @@ pub fn next(this: *Mv) Maybe(void) { t.task.schedule(); } - return Maybe(void).success; + return .suspended; }, // Shouldn't happen .executing => {}, .waiting_write_err => { - return Maybe(void).success; + return .failed; }, .done, .err => unreachable, } } switch (this.state) { - .done => this.bltn().done(0), - else => this.bltn().done(1), + .done => return this.bltn().done(0), + else => return this.bltn().done(1), } - - return Maybe(void).success; } -pub fn onIOWriterChunk(this: *Mv, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Mv, _: usize, e: ?JSC.SystemError) Yield { defer if (e) |err| err.deref(); switch (this.state) { .waiting_write_err => { if (e != null) { this.state = .err; - _ = this.next(); - return; + return this.next(); } - this.bltn().done(this.state.waiting_write_err.exit_code); - return; + return this.bltn().done(this.state.waiting_write_err.exit_code); }, else => @panic("Invalid state"), } @@ -340,8 +334,7 @@ pub fn checkTargetTaskDone(this: *Mv, task: *ShellMvCheckTargetTask) void { } this.state.check_target.state = .done; - _ = this.next(); - return; + this.next().run(); } pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void { @@ -371,8 +364,7 @@ pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void { } this.state = .done; - _ = this.next(); - return; + this.next().run(); } } @@ -500,6 +492,7 @@ const assert = bun.assert; const std = @import("std"); const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const ExitCode = shell.ExitCode; const JSC = bun.JSC; const Maybe = bun.sys.Maybe; diff --git a/src/shell/builtin/pwd.zig b/src/shell/builtin/pwd.zig index 4df76b51ef..734cda8623 100644 --- a/src/shell/builtin/pwd.zig +++ b/src/shell/builtin/pwd.zig @@ -7,53 +7,49 @@ state: union(enum) { done, } = .idle, -pub fn start(this: *Pwd) Maybe(void) { +pub fn start(this: *Pwd) Yield { const args = this.bltn().argsSlice(); if (args.len > 0) { const msg = "pwd: too many arguments\n"; if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .{ .waiting_io = .{ .kind = .stderr } }; - this.bltn().stderr.enqueue(this, msg, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, msg, safeguard); } _ = this.bltn().writeNoIO(.stderr, msg); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } const cwd_str = this.bltn().parentCmd().base.shell.cwd(); if (this.bltn().stdout.needsIO()) |safeguard| { this.state = .{ .waiting_io = .{ .kind = .stdout } }; - this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{cwd_str}, safeguard); - return Maybe(void).success; + return this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{cwd_str}, safeguard); } const buf = this.bltn().fmtErrorArena(null, "{s}\n", .{cwd_str}); _ = this.bltn().writeNoIO(.stdout, buf); this.state = .done; - this.bltn().done(0); - return Maybe(void).success; + return this.bltn().done(0); } -pub fn next(this: *Pwd) void { +pub fn next(this: *Pwd) Yield { while (!(this.state == .err or this.state == .done)) { switch (this.state) { - .waiting_io => return, + .waiting_io => return .suspended, .idle => @panic("Unexpected \"idle\" state in Pwd. This indicates a bug in Bun. Please file a GitHub issue."), .done, .err => unreachable, } } switch (this.state) { - .done => this.bltn().done(0), - .err => this.bltn().done(1), - else => {}, + .done => return this.bltn().done(0), + .err => return this.bltn().done(1), + else => unreachable, } } -pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .waiting_io); } @@ -61,8 +57,7 @@ pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) void { if (e != null) { defer e.?.deref(); this.state = .err; - this.next(); - return; + return this.next(); } this.state = switch (this.state.waiting_io.kind) { @@ -70,7 +65,7 @@ pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) void { .stderr => .err, }; - this.next(); + return this.next(); } pub fn deinit(this: *Pwd) void { @@ -85,11 +80,11 @@ pub inline fn bltn(this: *Pwd) *Builtin { // -- const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const Pwd = @This(); const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const assert = bun.assert; diff --git a/src/shell/builtin/rm.zig b/src/shell/builtin/rm.zig index 5db57a6b44..31c6268adf 100644 --- a/src/shell/builtin/rm.zig +++ b/src/shell/builtin/rm.zig @@ -45,6 +45,7 @@ state: union(enum) { } }, done: struct { exit_code: ExitCode }, + waiting_write_err, err: ExitCode, } = .idle, @@ -100,13 +101,14 @@ pub const Opts = struct { }; }; -pub fn start(this: *Rm) Maybe(void) { +pub fn start(this: *Rm) Yield { return this.next(); } -pub noinline fn next(this: *Rm) Maybe(void) { +pub noinline fn next(this: *Rm) Yield { while (this.state != .done and this.state != .err) { switch (this.state) { + .waiting_write_err => return .suspended, .idle => { this.state = .{ .parse_opts = .{ @@ -127,14 +129,12 @@ pub noinline fn next(this: *Rm) Maybe(void) { const error_string = Builtin.Kind.usageString(.rm); if (this.bltn().stderr.needsIO()) |safeguard| { parse_opts.state = .wait_write_err; - this.bltn().stderr.enqueue(this, error_string, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, error_string, safeguard); } _ = this.bltn().writeNoIO(.stderr, error_string); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } const idx = parse_opts.idx; @@ -156,14 +156,11 @@ pub noinline fn next(this: *Rm) Maybe(void) { const buf = "rm: \"-i\" is not supported yet"; if (this.bltn().stderr.needsIO()) |safeguard| { parse_opts.state = .wait_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - continue; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } const filepath_args_start = idx; @@ -174,7 +171,12 @@ pub noinline fn next(this: *Rm) Maybe(void) { var buf: bun.PathBuffer = undefined; const cwd = switch (Syscall.getcwd(&buf)) { .err => |err| { - return .{ .err = err }; + const errbuf = this.bltn().fmtErrorArena( + .rm, + "{s}: {s}", + .{ "getcwd", err.msg() orelse "failed to get cwd" }, + ); + return this.writeFailingError(errbuf, 1); }, .result => |cwd| cwd, }; @@ -192,16 +194,14 @@ pub noinline fn next(this: *Rm) Maybe(void) { if (is_root) { if (this.bltn().stderr.needsIO()) |safeguard| { parse_opts.state = .wait_write_err; - this.bltn().stderr.enqueueFmtBltn(this, .rm, "\"{s}\" may not be removed\n", .{resolved_path}, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueueFmtBltn(this, .rm, "\"{s}\" may not be removed\n", .{resolved_path}, safeguard); } const error_string = this.bltn().fmtErrorArena(.rm, "\"{s}\" may not be removed\n", .{resolved_path}); _ = this.bltn().writeNoIO(.stderr, error_string); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } } } @@ -224,28 +224,24 @@ pub noinline fn next(this: *Rm) Maybe(void) { const error_string = "rm: illegal option -- -\n"; if (this.bltn().stderr.needsIO()) |safeguard| { parse_opts.state = .wait_write_err; - this.bltn().stderr.enqueue(this, error_string, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, error_string, safeguard); } _ = this.bltn().writeNoIO(.stderr, error_string); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); }, .illegal_option_with_flag => { const flag = arg; if (this.bltn().stderr.needsIO()) |safeguard| { parse_opts.state = .wait_write_err; - this.bltn().stderr.enqueueFmtBltn(this, .rm, "illegal option -- {s}\n", .{flag[1..]}, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueueFmtBltn(this, .rm, "illegal option -- {s}\n", .{flag[1..]}, safeguard); } const error_string = this.bltn().fmtErrorArena(.rm, "illegal option -- {s}\n", .{flag[1..]}); _ = this.bltn().writeNoIO(.stderr, error_string); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); }, } }, @@ -284,26 +280,25 @@ pub noinline fn next(this: *Rm) Maybe(void) { } // do nothing - return Maybe(void).success; + return .suspended; }, .done, .err => unreachable, } } switch (this.state) { - .done => this.bltn().done(0), - .err => this.bltn().done(this.state.err), - else => {}, + .done => return this.bltn().done(0), + .err => return this.bltn().done(this.state.err), + else => unreachable, } - - return Maybe(void).success; } -pub fn onIOWriterChunk(this: *Rm, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Rm, _: usize, e: ?JSC.SystemError) Yield { log("Rm(0x{x}).onIOWriterChunk()", .{@intFromPtr(this)}); if (comptime bun.Environment.allow_assert) { assert((this.state == .parse_opts and this.state.parse_opts.state == .wait_write_err) or - (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_count.load(.seq_cst) > 0)); + (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_count.load(.seq_cst) > 0) or + this.state == .waiting_write_err); } if (this.state == .exec and this.state.exec.state == .waiting) { @@ -311,21 +306,18 @@ pub fn onIOWriterChunk(this: *Rm, _: usize, e: ?JSC.SystemError) void { this.state.exec.incrementOutputCount(.output_done); if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { const code: ExitCode = if (this.state.exec.err != null) 1 else 0; - this.bltn().done(code); - return; + return this.bltn().done(code); } - return; + return .suspended; } if (e != null) { defer e.?.deref(); this.state = .{ .err = @intFromEnum(e.?.getErrno()) }; - this.bltn().done(e.?.getErrno()); - return; + return this.bltn().done(e.?.getErrno()); } - this.bltn().done(1); - return; + return this.bltn().done(1); } pub fn deinit(this: *Rm) void { @@ -421,7 +413,7 @@ pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void { if (this.bltn().stderr.needsIO()) |safeguard| { log("Rm(0x{x}) task=0x{x} ERROR={s}", .{ @intFromPtr(this), @intFromPtr(task), error_string }); exec.incrementOutputCount(.output_count); - this.bltn().stderr.enqueue(this, error_string, safeguard); + this.bltn().stderr.enqueue(this, error_string, safeguard).run(); return; } else { _ = this.bltn().writeNoIO(.stderr, error_string); @@ -437,25 +429,22 @@ pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void { exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count)) { this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } }; - _ = this.next(); - return; + this.next().run(); } } -fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) void { +fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) Yield { if (this.bltn().stdout.needsIO()) |safeguard| { const buf = verbose.takeDeletedEntries(); defer buf.deinit(); - this.bltn().stdout.enqueue(this, buf.items, safeguard); - } else { - _ = this.bltn().writeNoIO(.stdout, verbose.deleted_entries.items); - _ = this.state.exec.incrementOutputCount(.output_done); - if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { - this.bltn().done(if (this.state.exec.err != null) @as(ExitCode, 1) else @as(ExitCode, 0)); - return; - } - return; + return this.bltn().stdout.enqueue(this, buf.items, safeguard); } + _ = this.bltn().writeNoIO(.stdout, verbose.deleted_entries.items); + _ = this.state.exec.incrementOutputCount(.output_done); + if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { + return this.bltn().done(if (this.state.exec.err != null) @as(ExitCode, 1) else @as(ExitCode, 0)); + } + return .done; } pub const ShellRmTask = struct { @@ -530,7 +519,7 @@ pub const ShellRmTask = struct { pub fn runFromMainThread(this: *DirTask) void { debug("DirTask(0x{x}, path={s}) runFromMainThread", .{ @intFromPtr(this), this.path }); - this.task_manager.rm.writeVerbose(this); + this.task_manager.rm.writeVerbose(this).run(); } pub fn runFromMainThreadMini(this: *DirTask, _: *void) void { @@ -1194,10 +1183,21 @@ inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) { return val & (rhs - 1); } -// -- +pub fn writeFailingError(this: *Rm, buf: []const u8, exit_code: ExitCode) Yield { + if (this.bltn().stderr.needsIO()) |safeguard| { + this.state = .waiting_write_err; + return this.bltn().stderr.enqueue(this, buf, safeguard); + } + + _ = this.bltn().writeNoIO(.stderr, buf); + + return this.bltn().done(exit_code); +} + const log = bun.Output.scoped(.Rm, true); const bun = @import("bun"); const shell = bun.shell; +const Yield = shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; diff --git a/src/shell/builtin/seq.zig b/src/shell/builtin/seq.zig index ac8e9cf605..8e55a3d530 100644 --- a/src/shell/builtin/seq.zig +++ b/src/shell/builtin/seq.zig @@ -1,4 +1,4 @@ -state: enum { idle, waiting_io, err, done } = .idle, +state: enum { idle, err, done } = .idle, buf: std.ArrayListUnmanaged(u8) = .{}, _start: f32 = 1, _end: f32 = 1, @@ -7,7 +7,7 @@ separator: []const u8 = "\n", terminator: []const u8 = "", fixed_width: bool = false, -pub fn start(this: *@This()) Maybe(void) { +pub fn start(this: *@This()) Yield { const args = this.bltn().argsSlice(); var iter = bun.SliceIterator([*:0]const u8).init(args); @@ -71,18 +71,16 @@ pub fn start(this: *@This()) Maybe(void) { return this.do(); } -fn fail(this: *@This(), msg: []const u8) Maybe(void) { +fn fail(this: *@This(), msg: []const u8) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .err; - this.bltn().stderr.enqueue(this, msg, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, msg, safeguard); } _ = this.bltn().writeNoIO(.stderr, msg); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } -fn do(this: *@This()) Maybe(void) { +fn do(this: *@This()) Yield { var current = this._start; var arena = std.heap.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); @@ -97,34 +95,30 @@ fn do(this: *@This()) Maybe(void) { this.state = .done; if (this.bltn().stdout.needsIO()) |safeguard| { - this.bltn().stdout.enqueue(this, this.buf.items, safeguard); - } else { - this.bltn().done(0); + return this.bltn().stdout.enqueue(this, this.buf.items, safeguard); } - return Maybe(void).success; + return this.bltn().done(0); } -fn print(this: *@This(), msg: []const u8) Maybe(void) { +fn print(this: *@This(), msg: []const u8) void { if (this.bltn().stdout.needsIO() != null) { this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory(); - return Maybe(void).success; + return; } - const res = this.bltn().writeNoIO(.stdout, msg); - if (res == .err) return Maybe(void).initErr(res.err); - return Maybe(void).success; + _ = this.bltn().writeNoIO(.stdout, msg); + return; } -pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) Yield { if (maybe_e) |e| { defer e.deref(); this.state = .err; - this.bltn().done(1); - return; + return this.bltn().done(1); } switch (this.state) { - .done => this.bltn().done(0), - .err => this.bltn().done(1), - else => {}, + .done => return this.bltn().done(0), + .err => return this.bltn().done(1), + .idle => bun.shell.unreachableState("Seq.onIOWriterChunk", "idle"), } } @@ -140,9 +134,9 @@ pub inline fn bltn(this: *@This()) *Builtin { // -- const bun = @import("bun"); +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const std = @import("std"); diff --git a/src/shell/builtin/touch.zig b/src/shell/builtin/touch.zig index 5029b3f6e7..77e17cc137 100644 --- a/src/shell/builtin/touch.zig +++ b/src/shell/builtin/touch.zig @@ -25,7 +25,7 @@ pub fn deinit(this: *Touch) void { log("{} deinit", .{this}); } -pub fn start(this: *Touch) Maybe(void) { +pub fn start(this: *Touch) Yield { const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) { .ok => |filepath_args| filepath_args, .err => |e| { @@ -35,12 +35,10 @@ pub fn start(this: *Touch) Maybe(void) { .unsupported => |unsupported| this.bltn().fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), }; - _ = this.writeFailingError(buf, 1); - return Maybe(void).success; + return this.writeFailingError(buf, 1); }, } orelse { - _ = this.writeFailingError(Builtin.Kind.touch.usageString(), 1); - return Maybe(void).success; + return this.writeFailingError(Builtin.Kind.touch.usageString(), 1); }; this.state = .{ @@ -49,12 +47,10 @@ pub fn start(this: *Touch) Maybe(void) { }, }; - _ = this.next(); - - return Maybe(void).success; + return this.next(); } -pub fn next(this: *Touch) void { +pub fn next(this: *Touch) Yield { switch (this.state) { .idle => @panic("Invalid state"), .exec => { @@ -63,10 +59,9 @@ pub fn next(this: *Touch) void { if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) { const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; this.state = .done; - this.bltn().done(exit_code); - return; + return this.bltn().done(exit_code); } - return; + return .suspended; } exec.started = true; @@ -77,33 +72,32 @@ pub fn next(this: *Touch) void { var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ()); task.schedule(); } + return .suspended; }, - .waiting_write_err => return, - .done => this.bltn().done(0), + .waiting_write_err => return .failed, + .done => return this.bltn().done(0), } } -pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?JSC.SystemError) Yield { if (this.state == .waiting_write_err) { return this.bltn().done(1); } if (e) |err| err.deref(); - this.next(); + return this.next(); } -pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Maybe(void) { +pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state = .waiting_write_err; - this.bltn().stderr.enqueue(this, buf, safeguard); - return Maybe(void).success; + return this.bltn().stderr.enqueue(this, buf, safeguard); } _ = this.bltn().writeNoIO(.stderr, buf); - this.bltn().done(exit_code); - return Maybe(void).success; + return this.bltn().done(exit_code); } pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void { @@ -121,11 +115,11 @@ pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void { }); const error_string = this.bltn().taskErrorToString(.touch, e); this.state.exec.err = e; - output_task.start(error_string); + output_task.start(error_string).run(); return; } - this.next(); + this.next().run(); } pub const ShellTouchOutputTask = OutputTask(Touch, .{ @@ -137,38 +131,36 @@ pub const ShellTouchOutputTask = OutputTask(Touch, .{ }); const ShellTouchOutputTaskVTable = struct { - pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) CoroutineResult { + pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) ?Yield { if (this.bltn().stderr.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; - this.bltn().stderr.enqueue(childptr, errbuf, safeguard); - return .yield; + return this.bltn().stderr.enqueue(childptr, errbuf, safeguard); } _ = this.bltn().writeNoIO(.stderr, errbuf); - return .cont; + return null; } pub fn onWriteErr(this: *Touch) void { this.state.exec.output_done += 1; } - pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) CoroutineResult { + pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) ?Yield { if (this.bltn().stdout.needsIO()) |safeguard| { this.state.exec.output_waiting += 1; const slice = output.slice(); log("THE SLICE: {d} {s}", .{ slice.len, slice }); - this.bltn().stdout.enqueue(childptr, slice, safeguard); - return .yield; + return this.bltn().stdout.enqueue(childptr, slice, safeguard); } _ = this.bltn().writeNoIO(.stdout, output.slice()); - return .cont; + return null; } pub fn onWriteOut(this: *Touch) void { this.state.exec.output_done += 1; } - pub fn onDone(this: *Touch) void { - this.next(); + pub fn onDone(this: *Touch) Yield { + return this.next(); } }; @@ -398,10 +390,10 @@ const Touch = @This(); const log = debug; const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const ExitCode = shell.ExitCode; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const WorkPool = bun.JSC.WorkPool; const ResolvePath = bun.path; const Syscall = bun.sys; @@ -414,5 +406,4 @@ const ParseFlagResult = interpreter.ParseFlagResult; const FlagParser = interpreter.FlagParser; const unsupportedFlag = interpreter.unsupportedFlag; const OutputTask = interpreter.OutputTask; -const CoroutineResult = interpreter.CoroutineResult; const OutputSrc = interpreter.OutputSrc; diff --git a/src/shell/builtin/true.zig b/src/shell/builtin/true.zig index b647d50e42..c01bffa576 100644 --- a/src/shell/builtin/true.zig +++ b/src/shell/builtin/true.zig @@ -1,16 +1,15 @@ -pub fn start(this: *@This()) Maybe(void) { - this.bltn().done(0); - return Maybe(void).success; -} - -pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void { - // no IO is done +pub fn start(this: *@This()) Yield { + return this.bltn().done(0); } pub fn deinit(this: *@This()) void { _ = this; } +pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) Yield { + return .done; +} + pub inline fn bltn(this: *@This()) *Builtin { const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("true", this)); return @fieldParentPtr("impl", impl); @@ -18,9 +17,9 @@ pub inline fn bltn(this: *@This()) *Builtin { // -- const bun = @import("bun"); +const Yield = bun.shell.Yield; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; const Builtin = Interpreter.Builtin; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; diff --git a/src/shell/builtin/which.zig b/src/shell/builtin/which.zig index a4f99e6bbd..29eba3c740 100644 --- a/src/shell/builtin/which.zig +++ b/src/shell/builtin/which.zig @@ -18,17 +18,15 @@ state: union(enum) { err: JSC.SystemError, } = .idle, -pub fn start(this: *Which) Maybe(void) { +pub fn start(this: *Which) Yield { const args = this.bltn().argsSlice(); if (args.len == 0) { if (this.bltn().stdout.needsIO()) |safeguard| { this.state = .one_arg; - this.bltn().stdout.enqueue(this, "\n", safeguard); - return Maybe(void).success; + return this.bltn().stdout.enqueue(this, "\n", safeguard); } _ = this.bltn().writeNoIO(.stdout, "\n"); - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } if (this.bltn().stdout.needsIO() == null) { @@ -47,8 +45,7 @@ pub fn start(this: *Which) Maybe(void) { _ = this.bltn().writeNoIO(.stdout, resolved); } - this.bltn().done(@intFromBool(had_not_found)); - return Maybe(void).success; + return this.bltn().done(@intFromBool(had_not_found)); } this.state = .{ @@ -58,16 +55,14 @@ pub fn start(this: *Which) Maybe(void) { .state = .none, }, }; - this.next(); - return Maybe(void).success; + return this.next(); } -pub fn next(this: *Which) void { +pub fn next(this: *Which) Yield { var multiargs = &this.state.multi_args; if (multiargs.arg_idx >= multiargs.args_slice.len) { // Done - this.bltn().done(@intFromBool(multiargs.had_not_found)); - return; + return this.bltn().done(@intFromBool(multiargs.had_not_found)); } const arg_raw = multiargs.args_slice[multiargs.arg_idx]; @@ -81,40 +76,35 @@ pub fn next(this: *Which) void { multiargs.had_not_found = true; if (this.bltn().stdout.needsIO()) |safeguard| { multiargs.state = .waiting_write; - this.bltn().stdout.enqueueFmtBltn(this, null, "{s} not found\n", .{arg}, safeguard); - // yield execution - return; + return this.bltn().stdout.enqueueFmtBltn(this, null, "{s} not found\n", .{arg}, safeguard); } const buf = this.bltn().fmtErrorArena(null, "{s} not found\n", .{arg}); _ = this.bltn().writeNoIO(.stdout, buf); - this.argComplete(); - return; + return this.argComplete(); }; if (this.bltn().stdout.needsIO()) |safeguard| { multiargs.state = .waiting_write; - this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{resolved}, safeguard); - return; + return this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{resolved}, safeguard); } const buf = this.bltn().fmtErrorArena(null, "{s}\n", .{resolved}); _ = this.bltn().writeNoIO(.stdout, buf); - this.argComplete(); - return; + return this.argComplete(); } -fn argComplete(this: *Which) void { +fn argComplete(this: *Which) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .multi_args and this.state.multi_args.state == .waiting_write); } this.state.multi_args.arg_idx += 1; this.state.multi_args.state = .none; - this.next(); + return this.next(); } -pub fn onIOWriterChunk(this: *Which, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Which, _: usize, e: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .one_arg or (this.state == .multi_args and this.state.multi_args.state == .waiting_write)); @@ -122,17 +112,15 @@ pub fn onIOWriterChunk(this: *Which, _: usize, e: ?JSC.SystemError) void { if (e != null) { this.state = .{ .err = e.? }; - this.bltn().done(e.?.getErrno()); - return; + return this.bltn().done(e.?.getErrno()); } if (this.state == .one_arg) { // Calling which with on arguments returns exit code 1 - this.bltn().done(1); - return; + return this.bltn().done(1); } - this.argComplete(); + return this.argComplete(); } pub fn deinit(this: *Which) void { @@ -151,9 +139,9 @@ const Which = @This(); const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const JSC = bun.JSC; -const Maybe = bun.sys.Maybe; const assert = bun.assert; const interpreter = @import("../interpreter.zig"); diff --git a/src/shell/builtin/yes.zig b/src/shell/builtin/yes.zig index 6ddcedc7cd..bb6ed6445d 100644 --- a/src/shell/builtin/yes.zig +++ b/src/shell/builtin/yes.zig @@ -2,7 +2,7 @@ state: enum { idle, waiting_io, err, done } = .idle, expletive: []const u8 = "y", task: YesTask = undefined, -pub fn start(this: *@This()) Maybe(void) { +pub fn start(this: *@This()) Yield { const args = this.bltn().argsSlice(); if (args.len > 0) { @@ -16,35 +16,31 @@ pub fn start(this: *@This()) Maybe(void) { .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop), }; this.state = .waiting_io; - this.bltn().stdout.enqueue(this, this.expletive, safeguard); - this.bltn().stdout.enqueue(this, "\n", safeguard); this.task.enqueue(); - return Maybe(void).success; + return this.bltn().stdout.enqueueFmt(this, "{s}\n", .{this.expletive}, safeguard); } var res: Maybe(usize) = undefined; while (true) { res = this.bltn().writeNoIO(.stdout, this.expletive); if (res == .err) { - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } res = this.bltn().writeNoIO(.stdout, "\n"); if (res == .err) { - this.bltn().done(1); - return Maybe(void).success; + return this.bltn().done(1); } } @compileError(unreachable); } -pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) Yield { if (maybe_e) |e| { defer e.deref(); this.state = .err; - this.bltn().done(1); - return; + return this.bltn().done(1); } + return .suspended; } pub inline fn bltn(this: *@This()) *Builtin { @@ -72,8 +68,7 @@ pub const YesTask = struct { const yes: *Yes = @fieldParentPtr("task", this); // Manually make safeguard since this task should not be created if output does not need IO - yes.bltn().stdout.enqueue(yes, yes.expletive, .output_needs_io); - yes.bltn().stdout.enqueue(yes, "\n", .output_needs_io); + yes.bltn().stdout.enqueueFmt(yes, "{s}\n", .{yes.expletive}, .output_needs_io).run(); this.enqueue(); } @@ -85,6 +80,7 @@ pub const YesTask = struct { // -- const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const interpreter = @import("../interpreter.zig"); const Interpreter = interpreter.Interpreter; diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 067299760b..75d452902d 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -39,6 +39,7 @@ const windows = bun.windows; const uv = windows.libuv; const Maybe = JSC.Maybe; const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct; +const Yield = shell.Yield; pub const Pipe = [2]bun.FileDescriptor; const shell = bun.shell; @@ -87,6 +88,11 @@ const assert = bun.assert; /// safely assume the stdout/stderr they are working with require IO. pub const OutputNeedsIOSafeGuard = enum(u0) { output_needs_io }; +/// Similar to `OutputNeedsIOSafeGuard` but to ensure a function is +/// called at the "top" of the call-stack relative to the interpreter's +/// execution. +pub const CallstackGuard = enum(u0) { __i_know_what_i_am_doing }; + pub const ExitCode = u16; pub const StateKind = enum(u8) { @@ -553,19 +559,29 @@ pub const Interpreter = struct { enqueueCb: fn (c: @TypeOf(ctx)) void, comptime fmt: []const u8, args: anytype, - ) void { + ) Yield { const io: *IO.OutKind = &@field(ctx.io, "stderr"); switch (io.*) { .fd => |x| { enqueueCb(ctx); - x.writer.enqueueFmt(ctx, x.captured, fmt, args); + return x.writer.enqueueFmt(ctx, x.captured, fmt, args); }, .pipe => { const bufio: *bun.ByteList = this.buffered_stderr(); bufio.appendFmt(bun.default_allocator, fmt, args) catch bun.outOfMemory(); - ctx.parent.childDone(ctx, 1); + return ctx.parent.childDone(ctx, 1); + }, + // FIXME: This is not correct? This would just make the entire shell hang I think? + .ignore => { + const childptr = IOWriterChildPtr.init(ctx); + // TODO: is this necessary + const count = std.fmt.count(fmt, args); + return .{ .on_io_writer_chunk = .{ + .child = childptr.asAnyOpaque(), + .err = null, + .written = count, + } }; }, - .ignore => {}, } } }; @@ -745,8 +761,8 @@ pub const Interpreter = struct { }; // Avoid the large stack allocation on Windows. - const pathbuf = bun.default_allocator.create(bun.PathBuffer) catch bun.outOfMemory(); - defer bun.default_allocator.destroy(pathbuf); + const pathbuf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(pathbuf); const cwd: [:0]const u8 = switch (Syscall.getcwdZ(pathbuf)) { .result => |cwd| cwd, .err => |err| { @@ -1016,18 +1032,20 @@ pub const Interpreter = struct { } pub fn run(this: *ThisInterpreter) !Maybe(void) { + log("Interpreter(0x{x}) run", .{@intFromPtr(this)}); if (this.setupIOBeforeRun().asErr()) |e| { return .{ .err = e }; } var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy()); this.started.store(true, .seq_cst); - root.start(); + root.start().run(); return Maybe(void).success; } pub fn runFromJS(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + log("Interpreter(0x{x}) runFromJS", .{@intFromPtr(this)}); _ = callframe; // autofix if (this.setupIOBeforeRun().asErr()) |e| { @@ -1039,7 +1057,7 @@ pub const Interpreter = struct { var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy()); this.started.store(true, .seq_cst); - root.start(); + root.start().run(); if (globalThis.hasException()) return error.JSError; return .js_undefined; @@ -1060,23 +1078,23 @@ pub const Interpreter = struct { @"async".actuallyDeinit(); this.async_commands_executing -= 1; if (this.async_commands_executing == 0 and this.exit_code != null) { - this.finish(this.exit_code.?); + this.finish(this.exit_code.?).run(); } } - pub fn childDone(this: *ThisInterpreter, child: InterpreterChildPtr, exit_code: ExitCode) void { + pub fn childDone(this: *ThisInterpreter, child: InterpreterChildPtr, exit_code: ExitCode) Yield { if (child.ptr.is(Script)) { const script = child.as(Script); script.deinitFromInterpreter(); this.exit_code = exit_code; - if (this.async_commands_executing == 0) this.finish(exit_code); - return; + if (this.async_commands_executing == 0) return this.finish(exit_code); + return .suspended; } @panic("Bad child"); } - pub fn finish(this: *ThisInterpreter, exit_code: ExitCode) void { - log("finish {d}", .{exit_code}); + pub fn finish(this: *ThisInterpreter, exit_code: ExitCode) Yield { + log("Interpreter(0x{x}) finish {d}", .{ @intFromPtr(this), exit_code }); defer decrPendingActivityFlag(&this.has_pending_activity); if (this.event_loop == .js) { @@ -1104,6 +1122,8 @@ pub const Interpreter = struct { this.flags.done = true; this.exit_code = exit_code; } + + return .done; } fn errored(this: *ThisInterpreter, the_error: ShellError) void { @@ -1135,7 +1155,7 @@ pub const Interpreter = struct { } fn deinitAfterJSRun(this: *ThisInterpreter) void { - log("deinit interpreter", .{}); + log("Interpreter(0x{x}) deinitAfterJSRun", .{@intFromPtr(this)}); for (this.jsobjs) |jsobj| { jsobj.unprotect(); } @@ -1303,6 +1323,8 @@ pub const Interpreter = struct { pub const Builtin = @import("./Builtin.zig"); + /// TODO: Investigate whether or not this can be removed now that we have + /// removed recursion pub const AsyncDeinitReader = IOReader.AsyncDeinitReader; pub const IO = @import("./IO.zig"); @@ -1310,34 +1332,7 @@ pub const Interpreter = struct { pub const IOReaderChildPtr = IOReader.ChildPtr; pub const IOWriter = @import("./IOWriter.zig"); - pub const AsyncDeinitWriter = struct { - ran: bool = false, - - pub fn enqueue(this: *@This()) void { - if (this.ran) return; - this.ran = true; - - var iowriter = this.writer(); - - if (iowriter.evtloop == .js) { - iowriter.evtloop.js.enqueueTaskConcurrent(iowriter.concurrent_task.js.from(this, .manual_deinit)); - } else { - iowriter.evtloop.mini.enqueueTaskConcurrent(iowriter.concurrent_task.mini.from(this, "runFromMainThreadMini")); - } - } - - pub fn writer(this: *@This()) *IOWriter { - return @alignCast(@fieldParentPtr("async_deinit", this)); - } - - pub fn runFromMainThread(this: *@This()) void { - this.writer().deinitOnMainThread(); - } - - pub fn runFromMainThreadMini(this: *@This(), _: *void) void { - this.runFromMainThread(); - } - }; + pub const AsyncDeinitWriter = IOWriter.AsyncDeinitWriter; }; /// Construct a tagged union of the state nodes provided in `TypesValue`. @@ -1366,15 +1361,14 @@ pub fn StatePtrUnion(comptime TypesValue: anytype) type { } /// Starts the state node. - pub fn start(this: @This()) void { + pub fn start(this: @This()) Yield { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); var casted = this.as(Ty); - casted.start(); - return; + return casted.start(); } } unknownTag(this.tagInt()); @@ -1398,7 +1392,7 @@ pub fn StatePtrUnion(comptime TypesValue: anytype) type { /// Signals to the state node that one of its children completed with the /// given exit code - pub fn childDone(this: @This(), child: anytype, exit_code: ExitCode) void { + pub fn childDone(this: @This(), child: anytype, exit_code: ExitCode) Yield { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { @@ -1409,15 +1403,14 @@ pub fn StatePtrUnion(comptime TypesValue: anytype) type { break :brk ChildPtr.init(child); }; var casted = this.as(Ty); - casted.childDone(child_ptr, exit_code); - return; + return casted.childDone(child_ptr, exit_code); } } unknownTag(this.tagInt()); } - pub fn unknownTag(tag: Ptr.TagInt) void { - if (bun.Environment.allow_assert) std.debug.panic("Bad tag: {d}\n", .{tag}); + pub fn unknownTag(tag: Ptr.TagInt) noreturn { + return bun.Output.panic("Unknown tag for shell state node: {d}\n", .{tag}); } pub fn tagInt(this: @This()) Ptr.TagInt { @@ -1638,8 +1631,9 @@ pub const ShellSyscall = struct { pub fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) { if (bun.Environment.isWindows) { - var buf: bun.PathBuffer = undefined; - const path = switch (getPath(dir, path_, &buf)) { + const buf: *bun.PathBuffer = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(buf); + const path = switch (getPath(dir, path_, buf)) { .err => |e| return .{ .err = e }, .result => |p| p, }; @@ -1653,12 +1647,15 @@ pub const ShellSyscall = struct { return Syscall.fstatat(dir, path_); } + /// Same thing as bun.sys.openat on posix + /// On windows it will convert paths for us pub fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (bun.Environment.isWindows) { if (flags & bun.O.DIRECTORY != 0) { if (ResolvePath.Platform.posix.isAbsolute(path[0..path.len])) { - var buf: bun.PathBuffer = undefined; - const p = switch (getPath(dir, path, &buf)) { + const buf: *bun.PathBuffer = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(buf); + const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, }; @@ -1673,8 +1670,9 @@ pub const ShellSyscall = struct { }; } - var buf: bun.PathBuffer = undefined; - const p = switch (getPath(dir, path, &buf)) { + const buf: *bun.PathBuffer = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(buf); + const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, }; @@ -1717,11 +1715,11 @@ pub const ShellSyscall = struct { pub fn OutputTask( comptime Parent: type, comptime vtable: struct { - writeErr: *const fn (*Parent, childptr: anytype, []const u8) CoroutineResult, + writeErr: *const fn (*Parent, childptr: anytype, []const u8) ?Yield, onWriteErr: *const fn (*Parent) void, - writeOut: *const fn (*Parent, childptr: anytype, *OutputSrc) CoroutineResult, + writeOut: *const fn (*Parent, childptr: anytype, *OutputSrc) ?Yield, onWriteOut: *const fn (*Parent) void, - onDone: *const fn (*Parent) void, + onDone: *const fn (*Parent) Yield, }, ) type { return struct { @@ -1733,59 +1731,49 @@ pub fn OutputTask( done, }, - pub fn deinit(this: *@This()) void { + pub fn deinit(this: *@This()) Yield { if (comptime bun.Environment.allow_assert) assert(this.state == .done); - vtable.onDone(this.parent); - this.output.deinit(); - bun.destroy(this); + log("OutputTask({s}, 0x{x}) deinit", .{ @typeName(Parent), @intFromPtr(this) }); + defer bun.destroy(this); + defer this.output.deinit(); + return vtable.onDone(this.parent); } - pub fn start(this: *@This(), errbuf: ?[]const u8) void { + pub fn start(this: *@This(), errbuf: ?[]const u8) Yield { + log("OutputTask({s}, 0x{x}) start errbuf={s}", .{ @typeName(Parent), @intFromPtr(this), if (errbuf) |err| err[0..@min(128, err.len)] else "null" }); this.state = .waiting_write_err; if (errbuf) |err| { - switch (vtable.writeErr(this.parent, this, err)) { - .cont => { - this.next(); - }, - .yield => return, - } - return; + if (vtable.writeErr(this.parent, this, err)) |yield| return yield; + return this.next(); } this.state = .waiting_write_out; - switch (vtable.writeOut(this.parent, this, &this.output)) { - .cont => { - vtable.onWriteOut(this.parent); - this.state = .done; - this.deinit(); - }, - .yield => return, - } + if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; + vtable.onWriteOut(this.parent); + this.state = .done; + return this.deinit(); } - pub fn next(this: *@This()) void { + pub fn next(this: *@This()) Yield { switch (this.state) { .waiting_write_err => { vtable.onWriteErr(this.parent); this.state = .waiting_write_out; - switch (vtable.writeOut(this.parent, this, &this.output)) { - .cont => { - vtable.onWriteOut(this.parent); - this.state = .done; - this.deinit(); - }, - .yield => return, - } + if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; + vtable.onWriteOut(this.parent); + this.state = .done; + return this.deinit(); }, .waiting_write_out => { vtable.onWriteOut(this.parent); this.state = .done; - this.deinit(); + return this.deinit(); }, .done => @panic("Invalid state"), } } - pub fn onIOWriterChunk(this: *@This(), _: usize, err: ?JSC.SystemError) void { + pub fn onIOWriterChunk(this: *@This(), _: usize, err: ?JSC.SystemError) Yield { + log("OutputTask({s}, 0x{x}) onIOWriterChunk", .{ @typeName(Parent), @intFromPtr(this) }); if (err) |e| { e.deref(); } @@ -1794,19 +1782,15 @@ pub fn OutputTask( .waiting_write_err => { vtable.onWriteErr(this.parent); this.state = .waiting_write_out; - switch (vtable.writeOut(this.parent, this, &this.output)) { - .cont => { - vtable.onWriteOut(this.parent); - this.state = .done; - this.deinit(); - }, - .yield => return, - } + if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; + vtable.onWriteOut(this.parent); + this.state = .done; + return this.deinit(); }, .waiting_write_out => { vtable.onWriteOut(this.parent); this.state = .done; - this.deinit(); + return this.deinit(); }, .done => @panic("Invalid state"), } @@ -1904,7 +1888,23 @@ pub fn isPollable(fd: bun.FileDescriptor, mode: bun.Mode) bool { return switch (bun.Environment.os) { .windows, .wasm => false, .linux => posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode) or posix.isatty(fd.native()), - // macos allows regular files to be pollable: ISREG(mode) == true - .mac => posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode) or posix.S.ISREG(mode) or posix.isatty(fd.native()), + // macos DOES allow regular files to be pollable, but we don't want that because + // our IOWriter code has a separate and better codepath for writing to files. + .mac => if (posix.S.ISREG(mode)) false else posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode) or posix.isatty(fd.native()), }; } + +pub fn isPollableFromMode(mode: bun.Mode) bool { + return switch (bun.Environment.os) { + .windows, .wasm => false, + .linux => posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode), + // macos DOES allow regular files to be pollable, but we don't want that because + // our IOWriter code has a separate and better codepath for writing to files. + .mac => if (posix.S.ISREG(mode)) false else posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode), + }; +} + +pub fn unreachableState(context: []const u8, state: []const u8) noreturn { + @branchHint(.cold); + return bun.Output.panic("Bun shell has reached an unreachable state \"{s}\" in the {s} context. This indicates a bug, please open a GitHub issue.", .{ state, context }); +} diff --git a/src/shell/shell.zig b/src/shell/shell.zig index e4c3160a64..17d5ee464a 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -25,6 +25,9 @@ pub const IOReader = Interpreter.IOReader; // pub const IOWriter = interpret.IOWriter; // pub const SubprocessMini = subproc.ShellSubprocessMini; +pub const Yield = @import("./Yield.zig").Yield; +pub const unreachableState = interpret.unreachableState; + const GlobWalker = Glob.GlobWalker_(null, true); // const GlobWalker = Glob.BunGlobWalker; @@ -4149,6 +4152,19 @@ pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type { } } + pub fn pop(this: *@This()) T { + switch (this.*) { + .heap => { + return this.heap.pop().?; + }, + .inlined => { + const val = this.inlined.items[this.inlined.len - 1]; + this.inlined.len -= 1; + return val; + }, + } + } + pub fn swapRemove(this: *@This(), idx: usize) void { switch (this.*) { .heap => { diff --git a/src/shell/states/Assigns.zig b/src/shell/states/Assigns.zig index f4ae4176d2..9503772e2c 100644 --- a/src/shell/states/Assigns.zig +++ b/src/shell/states/Assigns.zig @@ -36,8 +36,8 @@ pub inline fn deinit(this: *Assigns) void { this.io.deinit(); } -pub inline fn start(this: *Assigns) void { - return this.next(); +pub fn start(this: *Assigns) Yield { + return .{ .assigns = this }; } pub fn init( @@ -59,7 +59,7 @@ pub fn init( }; } -pub fn next(this: *Assigns) void { +pub fn next(this: *Assigns) Yield { while (!(this.state == .done)) { switch (this.state) { .idle => { @@ -86,18 +86,17 @@ pub fn next(this: *Assigns) void { }, this.io.copy(), ); - this.state.expanding.expansion.start(); - return; + return this.state.expanding.expansion.start(); }, .done => unreachable, .err => return this.parent.childDone(this, 1), } } - this.parent.childDone(this, 0); + return this.parent.childDone(this, 0); } -pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) Yield { if (child.ptr.is(Expansion)) { const expansion = child.ptr.as(Expansion); if (exit_code != 0) { @@ -105,7 +104,7 @@ pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { .err = expansion.state.err, }; expansion.deinit(); - return; + return .failed; } var expanding = &this.state.expanding; @@ -157,8 +156,7 @@ pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { expanding.idx += 1; expansion.deinit(); - this.next(); - return; + return .{ .assigns = this }; } @panic("Invalid child to Assigns expression, this indicates a bug in Bun. Please file a report on Github."); @@ -172,6 +170,7 @@ pub const AssignCtx = enum { const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const Interpreter = bun.shell.Interpreter; const StatePtrUnion = bun.shell.interpret.StatePtrUnion; diff --git a/src/shell/states/Async.zig b/src/shell/states/Async.zig index 3149841b95..7c21c358d9 100644 --- a/src/shell/states/Async.zig +++ b/src/shell/states/Async.zig @@ -48,23 +48,23 @@ pub fn init( }); } -pub fn start(this: *Async) void { +pub fn start(this: *Async) Yield { log("{} start", .{this}); this.enqueueSelf(); - this.parent.childDone(this, 0); + return this.parent.childDone(this, 0); } -pub fn next(this: *Async) void { +pub fn next(this: *Async) Yield { log("{} next {s}", .{ this, @tagName(this.state) }); switch (this.state) { .idle => { this.state = .{ .exec = .{} }; this.enqueueSelf(); + return .suspended; }, .exec => { if (this.state.exec.child) |child| { - child.start(); - return; + return child.start(); } const child = brk: { @@ -104,9 +104,11 @@ pub fn next(this: *Async) void { }; this.state.exec.child = child; this.enqueueSelf(); + return .suspended; }, .done => { this.base.interpreter.asyncCmdDone(this); + return .done; }, } } @@ -119,11 +121,12 @@ pub fn enqueueSelf(this: *Async) void { } } -pub fn childDone(this: *Async, child_ptr: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Async, child_ptr: ChildPtr, exit_code: ExitCode) Yield { log("{} childDone", .{this}); child_ptr.deinit(); this.state = .{ .done = exit_code }; this.enqueueSelf(); + return .suspended; } /// This function is purposefully empty as a hack to ensure Async runs in the background while appearing to @@ -143,7 +146,7 @@ pub fn actuallyDeinit(this: *Async) void { } pub fn runFromMainThread(this: *Async) void { - this.next(); + this.next().run(); } pub fn runFromMainThreadMini(this: *Async, _: *void) void { @@ -152,6 +155,7 @@ pub fn runFromMainThreadMini(this: *Async, _: *void) void { const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = bun.shell.Interpreter; diff --git a/src/shell/states/Base.zig b/src/shell/states/Base.zig index 94a63f7550..78747078aa 100644 --- a/src/shell/states/Base.zig +++ b/src/shell/states/Base.zig @@ -12,6 +12,7 @@ pub inline fn eventLoop(this: *const Base) JSC.EventLoopHandle { return this.interpreter.event_loop; } +/// FIXME: We should get rid of this pub fn throw(this: *const Base, err: *const bun.shell.ShellErr) void { throwShellErr(err, this.eventLoop()) catch {}; //TODO: } diff --git a/src/shell/states/Binary.zig b/src/shell/states/Binary.zig index f8a8c9b570..3753393d50 100644 --- a/src/shell/states/Binary.zig +++ b/src/shell/states/Binary.zig @@ -44,7 +44,7 @@ pub fn init( return binary; } -pub fn start(this: *Binary) void { +pub fn start(this: *Binary) Yield { log("binary start {x} ({s})", .{ @intFromPtr(this), @tagName(this.node.op) }); if (comptime bun.Environment.allow_assert) { assert(this.left == null); @@ -57,9 +57,8 @@ pub fn start(this: *Binary) void { this.currently_executing = this.makeChild(false); this.left = 0; } - if (this.currently_executing) |exec| { - exec.start(); - } + bun.assert(this.currently_executing != null); + return this.currently_executing.?.start(); } fn makeChild(this: *Binary, left: bool) ?ChildPtr { @@ -109,7 +108,7 @@ fn makeChild(this: *Binary, left: bool) ?ChildPtr { } } -pub fn childDone(this: *Binary, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Binary, child: ChildPtr, exit_code: ExitCode) Yield { if (comptime bun.Environment.allow_assert) { assert(this.left == null or this.right == null); assert(this.currently_executing != null); @@ -122,23 +121,20 @@ pub fn childDone(this: *Binary, child: ChildPtr, exit_code: ExitCode) void { if (this.left == null) { this.left = exit_code; if ((this.node.op == .And and exit_code != 0) or (this.node.op == .Or and exit_code == 0)) { - this.parent.childDone(this, exit_code); - return; + return this.parent.childDone(this, exit_code); } this.currently_executing = this.makeChild(false); if (this.currently_executing == null) { this.right = 0; - this.parent.childDone(this, 0); - return; - } else { - this.currently_executing.?.start(); + return this.parent.childDone(this, 0); } - return; + + return this.currently_executing.?.start(); } this.right = exit_code; - this.parent.childDone(this, exit_code); + return this.parent.childDone(this, exit_code); } pub fn deinit(this: *Binary) void { @@ -150,6 +146,7 @@ pub fn deinit(this: *Binary) void { } const bun = @import("bun"); +const Yield = bun.shell.Yield; const Interpreter = bun.shell.Interpreter; const StatePtrUnion = bun.shell.interpret.StatePtrUnion; diff --git a/src/shell/states/Cmd.zig b/src/shell/states/Cmd.zig index 19ebd5778c..41fb157069 100644 --- a/src/shell/states/Cmd.zig +++ b/src/shell/states/Cmd.zig @@ -87,7 +87,7 @@ pub const ShellAsyncSubprocessDone = struct { pub fn runFromMainThread(this: *ShellAsyncSubprocessDone) void { log("{} runFromMainThread", .{this}); defer this.deinit(); - this.cmd.parent.childDone(this.cmd, this.cmd.exit_code orelse 0); + this.cmd.parent.childDone(this.cmd, this.cmd.exit_code orelse 0).run(); } pub fn deinit(this: *ShellAsyncSubprocessDone) void { @@ -219,13 +219,13 @@ pub fn isSubproc(this: *Cmd) bool { /// If starting a command results in an error (failed to find executable in path for example) /// then it should write to the stderr of the entire shell script process -pub fn writeFailingError(this: *Cmd, comptime fmt: []const u8, args: anytype) void { +pub fn writeFailingError(this: *Cmd, comptime fmt: []const u8, args: anytype) Yield { const handler = struct { fn enqueueCb(ctx: *Cmd) void { ctx.state = .waiting_write_err; } }; - this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + return this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); } pub fn init( @@ -256,17 +256,16 @@ pub fn init( return cmd; } -pub fn next(this: *Cmd) void { +pub fn next(this: *Cmd) Yield { while (this.state != .done) { switch (this.state) { .idle => { this.state = .{ .expanding_assigns = undefined }; Assigns.init(&this.state.expanding_assigns, this.base.interpreter, this.base.shell, this.node.assigns, .cmd, Assigns.ParentPtr.init(this), this.io.copy()); - this.state.expanding_assigns.start(); - return; // yield execution + return this.state.expanding_assigns.start(); }, .expanding_assigns => { - return; // yield execution + return .suspended; }, .expanding_redirect => { if (this.state.expanding_redirect.idx >= 1) { @@ -305,14 +304,11 @@ pub fn next(this: *Cmd) void { this.io.copy(), ); - this.state.expanding_redirect.expansion.start(); - return; + return this.state.expanding_redirect.expansion.start(); }, .expanding_args => { if (this.state.expanding_args.idx >= this.node.name_and_args.len) { - this.transitionToExecStateAndYield(); - // yield execution to subproc - return; + return this.transitionToExecStateAndYield(); } this.args.ensureUnusedCapacity(1) catch bun.outOfMemory(); @@ -330,51 +326,45 @@ pub fn next(this: *Cmd) void { this.state.expanding_args.idx += 1; - this.state.expanding_args.expansion.start(); - // yield execution to expansion - return; + return this.state.expanding_args.expansion.start(); }, .waiting_write_err => { - return; + bun.shell.unreachableState("Cmd.next", "waiting_write_err"); }, .exec => { - // yield execution to subproc/builtin - return; + bun.shell.unreachableState("Cmd.next", "exec"); }, .done => unreachable, } } if (this.state == .done) { - this.parent.childDone(this, this.exit_code.?); - return; + return this.parent.childDone(this, this.exit_code.?); } - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); } -fn transitionToExecStateAndYield(this: *Cmd) void { +fn transitionToExecStateAndYield(this: *Cmd) Yield { this.state = .exec; - this.initSubproc(); + return this.initSubproc(); } -pub fn start(this: *Cmd) void { +pub fn start(this: *Cmd) Yield { log("cmd start {x}", .{@intFromPtr(this)}); - return this.next(); + return .{ .cmd = this }; } -pub fn onIOWriterChunk(this: *Cmd, _: usize, e: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Cmd, _: usize, e: ?JSC.SystemError) Yield { if (e) |err| { this.base.throw(&bun.shell.ShellErr.newSys(err)); - return; + return .failed; } assert(this.state == .waiting_write_err); - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); } -pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) Yield { if (child.ptr.is(Assigns)) { if (exit_code != 0) { const err = this.state.expanding_assigns.state.err; @@ -382,8 +372,7 @@ pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { defer err.deinit(bun.default_allocator); this.state.expanding_assigns.deinit(); - this.writeFailingError("{}\n", .{err}); - return; + return this.writeFailingError("{}\n", .{err}); } this.state.expanding_assigns.deinit(); @@ -392,8 +381,7 @@ pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { .expansion = undefined, }, }; - this.next(); - return; + return .{ .cmd = this }; } if (child.ptr.is(Expansion)) { @@ -405,8 +393,7 @@ pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { else => @panic("Invalid state"), }; defer err.deinit(bun.default_allocator); - this.writeFailingError("{}\n", .{err}); - return; + return this.writeFailingError("{}\n", .{err}); } // Handling this case from the shell spec: // "If there is no command name, but the command contained a @@ -423,14 +410,13 @@ pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { { this.exit_code = e.out_exit_code; } - this.next(); - return; + return .{ .cmd = this }; } @panic("Expected Cmd child to be Assigns or Expansion. This indicates a bug in Bun. Please file a GitHub issue. "); } -fn initSubproc(this: *Cmd) void { +fn initSubproc(this: *Cmd) Yield { log("cmd init subproc ({x}, cwd={s})", .{ @intFromPtr(this), this.base.shell.cwd() }); var arena = &this.spawn_arena; @@ -444,10 +430,15 @@ fn initSubproc(this: *Cmd) void { const args = args: { this.args.append(null) catch bun.outOfMemory(); - if (bun.Environment.allow_assert) { + log("Cmd(0x{x}, {s}) IO: {}", .{ @intFromPtr(this), if (this.args.items.len > 0) this.args.items[0] orelse "" else "", this.io }); + if (bun.Environment.isDebug) { for (this.args.items) |maybe_arg| { if (maybe_arg) |arg| { - log("ARG: {s}\n", .{arg}); + if (bun.sliceTo(arg, 0).len > 80) { + log("ARG: {s}...\n", .{arg[0..80]}); + } else { + log("ARG: {s}\n", .{arg}); + } } } } @@ -466,8 +457,7 @@ fn initSubproc(this: *Cmd) void { // BUT, if the expansion contained a single command // substitution (third example above), then we need to // return the exit code of that command substitution. - this.parent.childDone(this, this.exit_code orelse 0); - return; + return this.parent.childDone(this, this.exit_code orelse 0); }; const first_arg_len = std.mem.len(first_arg); @@ -481,7 +471,7 @@ fn initSubproc(this: *Cmd) void { if (Builtin.Kind.fromStr(first_arg[0..first_arg_len])) |b| { const cwd = this.base.shell.cwd_fd; - const coro_result = Builtin.init( + const maybe_yield = Builtin.init( this, this.base.interpreter, b, @@ -493,7 +483,7 @@ fn initSubproc(this: *Cmd) void { cwd, &this.io, ); - if (coro_result == .yield) return; + if (maybe_yield) |yield| return yield; if (comptime bun.Environment.allow_assert) { assert(this.exec == .bltn); @@ -501,14 +491,7 @@ fn initSubproc(this: *Cmd) void { log("Builtin name: {s}", .{@tagName(this.exec)}); - switch (this.exec.bltn.start()) { - .result => {}, - .err => |e| { - this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toShellSystemError().message }); - return; - }, - } - return; + return this.exec.bltn.start(); } const path_buf = bun.PathBufferPool.get(); @@ -517,8 +500,7 @@ fn initSubproc(this: *Cmd) void { if (bun.strings.eqlComptime(first_arg_real, "bun") or bun.strings.eqlComptime(first_arg_real, "bun-debug")) blk2: { break :blk bun.selfExePath() catch break :blk2; } - this.writeFailingError("bun: command not found: {s}\n", .{first_arg}); - return; + return this.writeFailingError("bun: command not found: {s}\n", .{first_arg}); }; const duped = arena_allocator.dupeZ(u8, bun.span(resolved)) catch bun.outOfMemory(); @@ -540,6 +522,42 @@ fn initSubproc(this: *Cmd) void { defer shellio.deref(); this.io.to_subproc_stdio(&spawn_args.stdio, &shellio); + if (this.initRedirections(&spawn_args)) |yield| return yield; + + const buffered_closed = BufferedIoClosed.fromStdio(&spawn_args.stdio); + log("cmd ({x}) set buffered closed => {any}", .{ @intFromPtr(this), buffered_closed }); + + this.exec = .{ .subproc = .{ + .child = undefined, + .buffered_closed = buffered_closed, + } }; + var did_exit_immediately = false; + const subproc = switch (Subprocess.spawnAsync(this.base.eventLoop(), &shellio, spawn_args, &this.exec.subproc.child, &did_exit_immediately)) { + .result => this.exec.subproc.child, + .err => |*e| { + this.exec = .none; + return this.writeFailingError("{}\n", .{e}); + }, + }; + subproc.ref(); + this.spawn_arena_freed = true; + arena.deinit(); + + if (did_exit_immediately) { + if (subproc.process.hasExited()) { + // process has already exited, we called wait4(), but we did not call onProcessExit() + subproc.process.onExit(subproc.process.status, &std.mem.zeroes(bun.spawn.Rusage)); + } else { + // process has already exited, but we haven't called wait4() yet + // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 + subproc.process.wait(false); + } + } + + return .suspended; +} + +fn initRedirections(this: *Cmd, spawn_args: *Subprocess.SpawnArgs) ?Yield { if (this.node.redirect_file) |redirect| { const in_cmd_subst = false; @@ -561,11 +579,11 @@ fn initSubproc(this: *Cmd) void { } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Blob)) |blob__| { const blob = blob__.dupe(); if (this.node.redirect.stdin) { - spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stdin_no) catch return; + spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stdin_no) catch return .failed; } else if (this.node.redirect.stdout) { - spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stdout_no) catch return; + spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stdout_no) catch return .failed; } else if (this.node.redirect.stderr) { - spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stderr_no) catch return; + spawn_args.stdio[stdin_no].extractBlob(global, .{ .Blob = blob }, stderr_no) catch return .failed; } } else if (JSC.WebCore.ReadableStream.fromJS(this.base.interpreter.jsobjs[val.idx], global)) |rstream| { _ = rstream; @@ -573,24 +591,23 @@ fn initSubproc(this: *Cmd) void { } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Response)) |req| { req.getBodyValue().toBlobIfPossible(); if (this.node.redirect.stdin) { - spawn_args.stdio[stdin_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdin_no) catch return; + spawn_args.stdio[stdin_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdin_no) catch return .failed; } if (this.node.redirect.stdout) { - spawn_args.stdio[stdout_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdout_no) catch return; + spawn_args.stdio[stdout_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdout_no) catch return .failed; } if (this.node.redirect.stderr) { - spawn_args.stdio[stderr_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stderr_no) catch return; + spawn_args.stdio[stderr_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stderr_no) catch return .failed; } } else { const jsval = this.base.interpreter.jsobjs[val.idx]; global.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(global)}) catch {}; // TODO: propagate - return; + return .failed; } }, .atom => { if (this.redirection_file.items.len == 0) { - this.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{spawn_args.argv.items[0] orelse ""}); - return; + return this.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{spawn_args.argv.items[0] orelse ""}); } const path = this.redirection_file.items[0..this.redirection_file.items.len -| 1 :0]; log("Expanded Redirect: {s}\n", .{this.redirection_file.items[0..]}); @@ -616,36 +633,7 @@ fn initSubproc(this: *Cmd) void { } } - const buffered_closed = BufferedIoClosed.fromStdio(&spawn_args.stdio); - log("cmd ({x}) set buffered closed => {any}", .{ @intFromPtr(this), buffered_closed }); - - this.exec = .{ .subproc = .{ - .child = undefined, - .buffered_closed = buffered_closed, - } }; - var did_exit_immediately = false; - const subproc = switch (Subprocess.spawnAsync(this.base.eventLoop(), &shellio, spawn_args, &this.exec.subproc.child, &did_exit_immediately)) { - .result => this.exec.subproc.child, - .err => |*e| { - this.exec = .none; - this.writeFailingError("{}\n", .{e}); - return; - }, - }; - subproc.ref(); - this.spawn_arena_freed = true; - arena.deinit(); - - if (did_exit_immediately) { - if (subproc.process.hasExited()) { - // process has already exited, we called wait4(), but we did not call onProcessExit() - subproc.process.onExit(subproc.process.status, &std.mem.zeroes(bun.spawn.Rusage)); - } else { - // process has already exited, but we haven't called wait4() yet - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subproc.process.wait(false); - } - } + return null; } fn setStdioFromRedirect(stdio: *[3]shell.subproc.Stdio, flags: ast.RedirectFlags, val: shell.subproc.Stdio) void { @@ -708,8 +696,7 @@ pub fn onExit(this: *Cmd, exit_code: ExitCode) void { log("cmd exit code={d} has_finished={any} ({x})", .{ exit_code, has_finished, @intFromPtr(this) }); if (has_finished) { this.state = .done; - this.next(); - return; + this.next().run(); } } @@ -752,7 +739,7 @@ pub fn bufferedInputClose(this: *Cmd) void { this.exec.subproc.buffered_closed.close(this, .stdin); } -pub fn bufferedOutputClose(this: *Cmd, kind: Subprocess.OutKind, err: ?JSC.SystemError) void { +pub fn bufferedOutputClose(this: *Cmd, kind: Subprocess.OutKind, err: ?JSC.SystemError) Yield { switch (kind) { .stdout => this.bufferedOutputCloseStdout(err), .stderr => this.bufferedOutputCloseStderr(err), @@ -764,10 +751,12 @@ pub fn bufferedOutputClose(this: *Cmd, kind: Subprocess.OutKind, err: ?JSC.Syste .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.base.eventLoop()), }); async_subprocess_done.enqueue(); + return .suspended; } else { - this.parent.childDone(this, this.exit_code orelse 0); + return this.parent.childDone(this, this.exit_code orelse 0); } } + return .suspended; } pub fn bufferedOutputCloseStdout(this: *Cmd, err: ?JSC.SystemError) void { @@ -805,6 +794,7 @@ pub fn bufferedOutputCloseStderr(this: *Cmd, err: ?JSC.SystemError) void { const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Allocator = std.mem.Allocator; diff --git a/src/shell/states/CondExpr.zig b/src/shell/states/CondExpr.zig index 0553594cc0..17e3738efc 100644 --- a/src/shell/states/CondExpr.zig +++ b/src/shell/states/CondExpr.zig @@ -80,12 +80,12 @@ pub fn format(this: *const CondExpr, comptime _: []const u8, _: std.fmt.FormatOp try writer.print("CondExpr(0x{x}, op={s})", .{ @intFromPtr(this), @tagName(this.node.op) }); } -pub fn start(this: *CondExpr) void { +pub fn start(this: *CondExpr) Yield { log("{} start", .{this}); - this.next(); + return .{ .cond_expr = this }; } -fn next(this: *CondExpr) void { +pub fn next(this: *CondExpr) Yield { while (this.state != .done) { switch (this.state) { .idle => { @@ -94,8 +94,7 @@ fn next(this: *CondExpr) void { }, .expanding_args => { if (this.state.expanding_args.idx >= this.node.args.len()) { - this.commandImplStart(); - return; + return this.commandImplStart(); } this.args.ensureUnusedCapacity(1) catch bun.outOfMemory(); @@ -111,39 +110,33 @@ fn next(this: *CondExpr) void { this.io.copy(), ); this.state.expanding_args.idx += 1; - this.state.expanding_args.expansion.start(); - return; + return this.state.expanding_args.expansion.start(); }, - .waiting_stat => return, + .waiting_stat => return .suspended, .stat_complete => { switch (this.node.op) { .@"-f" => { - this.parent.childDone(this, if (this.state.stat_complete.stat == .result) 0 else 1); - return; + return this.parent.childDone(this, if (this.state.stat_complete.stat == .result) 0 else 1); }, .@"-d" => { const st: bun.Stat = switch (this.state.stat_complete.stat) { .result => |st| st, .err => { // It seems that bash always gives exit code 1 - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); }, }; - this.parent.childDone(this, if (bun.S.ISDIR(@intCast(st.mode))) 0 else 1); - return; + return this.parent.childDone(this, if (bun.S.ISDIR(@intCast(st.mode))) 0 else 1); }, .@"-c" => { const st: bun.Stat = switch (this.state.stat_complete.stat) { .result => |st| st, .err => { // It seems that bash always gives exit code 1 - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); }, }; - this.parent.childDone(this, if (bun.S.ISCHR(@intCast(st.mode))) 0 else 1); - return; + return this.parent.childDone(this, if (bun.S.ISCHR(@intCast(st.mode))) 0 else 1); }, .@"-z", .@"-n", .@"==", .@"!=" => @panic("This conditional expression op does not need `stat()`. This indicates a bug in Bun. Please file a GitHub issue."), else => { @@ -158,32 +151,32 @@ fn next(this: *CondExpr) void { }, } }, - .waiting_write_err => return, + .waiting_write_err => return .suspended, .done => assert(false), } } - this.parent.childDone(this, 0); + return this.parent.childDone(this, 0); } -fn commandImplStart(this: *CondExpr) void { +fn commandImplStart(this: *CondExpr) Yield { switch (this.node.op) { .@"-c", .@"-d", .@"-f", => { this.state = .waiting_stat; - this.doStat(); + return this.doStat(); }, - .@"-z" => this.parent.childDone(this, if (this.args.items.len == 0 or this.args.items[0].len == 0) 0 else 1), - .@"-n" => this.parent.childDone(this, if (this.args.items.len > 0 and this.args.items[0].len != 0) 0 else 1), + .@"-z" => return this.parent.childDone(this, if (this.args.items.len == 0 or this.args.items[0].len == 0) 0 else 1), + .@"-n" => return this.parent.childDone(this, if (this.args.items.len > 0 and this.args.items[0].len != 0) 0 else 1), .@"==" => { const is_eq = this.args.items.len == 0 or (this.args.items.len >= 2 and bun.strings.eql(this.args.items[0], this.args.items[1])); - this.parent.childDone(this, if (is_eq) 0 else 1); + return this.parent.childDone(this, if (is_eq) 0 else 1); }, .@"!=" => { const is_neq = this.args.items.len >= 2 and !bun.strings.eql(this.args.items[0], this.args.items[1]); - this.parent.childDone(this, if (is_neq) 0 else 1); + return this.parent.childDone(this, if (is_neq) 0 else 1); }, // else => @panic("Invalid node op: " ++ @tagName(this.node.op) ++ ", this indicates a bug in Bun. Please file a GithHub issue."), else => { @@ -200,7 +193,7 @@ fn commandImplStart(this: *CondExpr) void { } } -fn doStat(this: *CondExpr) void { +fn doStat(this: *CondExpr) Yield { const stat_task = bun.new(ShellCondExprStatTask, .{ .task = .{ .event_loop = this.base.eventLoop(), @@ -211,6 +204,7 @@ fn doStat(this: *CondExpr) void { .cwdfd = this.base.shell.cwd_fd, }); stat_task.task.schedule(); + return .suspended; } pub fn deinit(this: *CondExpr) void { @@ -218,18 +212,16 @@ pub fn deinit(this: *CondExpr) void { bun.destroy(this); } -pub fn childDone(this: *CondExpr, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *CondExpr, child: ChildPtr, exit_code: ExitCode) Yield { if (child.ptr.is(Expansion)) { if (exit_code != 0) { const err = this.state.expanding_args.expansion.state.err; defer err.deinit(bun.default_allocator); this.state.expanding_args.expansion.deinit(); - this.writeFailingError("{}\n", .{err}); - return; + return this.writeFailingError("{}\n", .{err}); } child.deinit(); - this.next(); - return; + return this.next(); } @panic("Invalid child to cond expression, this indicates a bug in Bun. Please file a report on Github."); @@ -241,34 +233,35 @@ pub fn onStatTaskComplete(this: *CondExpr, result: Maybe(bun.Stat)) void { this.state = .{ .stat_complete = .{ .stat = result }, }; - this.next(); + this.next().run(); } -pub fn writeFailingError(this: *CondExpr, comptime fmt: []const u8, args: anytype) void { +pub fn writeFailingError(this: *CondExpr, comptime fmt: []const u8, args: anytype) Yield { const handler = struct { fn enqueueCb(ctx: *CondExpr) void { ctx.state = .waiting_write_err; } }; - this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + return this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); } -pub fn onIOWriterChunk(this: *CondExpr, _: usize, err: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *CondExpr, _: usize, err: ?JSC.SystemError) Yield { if (err != null) { defer err.?.deref(); const exit_code: ExitCode = @intFromEnum(err.?.getErrno()); - this.parent.childDone(this, exit_code); - return; + return this.parent.childDone(this, exit_code); } if (this.state == .waiting_write_err) { - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); } + + bun.shell.unreachableState("CondExpr.onIOWriterChunk", @tagName(this.state)); } const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = bun.shell.Interpreter; diff --git a/src/shell/states/Expansion.zig b/src/shell/states/Expansion.zig index c7b44fc658..7bf2963311 100644 --- a/src/shell/states/Expansion.zig +++ b/src/shell/states/Expansion.zig @@ -137,17 +137,17 @@ pub fn deinit(expansion: *Expansion) void { expansion.io.deinit(); } -pub fn start(this: *Expansion) void { +pub fn start(this: *Expansion) Yield { if (comptime bun.Environment.allow_assert) { assert(this.child_state == .idle); assert(this.word_idx == 0); } this.state = .normal; - this.next(); + return .{ .expansion = this }; } -pub fn next(this: *Expansion) void { +pub fn next(this: *Expansion) Yield { while (!(this.state == .done or this.state == .err)) { switch (this.state) { .normal => { @@ -160,9 +160,7 @@ pub fn next(this: *Expansion) void { } while (this.word_idx < this.node.atomsLen()) { - const is_cmd_subst = this.expandVarAndCmdSubst(this.word_idx); - // yield execution - if (is_cmd_subst) return; + if (this.expandVarAndCmdSubst(this.word_idx)) |yield| return yield; } if (this.word_idx >= this.node.atomsLen()) { @@ -202,7 +200,7 @@ pub fn next(this: *Expansion) void { // Shouldn't fall through to here assert(this.word_idx >= this.node.atomsLen()); - return; + return .suspended; }, .braces => { var arena = Arena.init(this.base.interpreter.allocator); @@ -249,27 +247,25 @@ pub fn next(this: *Expansion) void { } }, .glob => { - this.transitionToGlobState(); - // yield - return; + return this.transitionToGlobState(); }, .done, .err => unreachable, } } if (this.state == .done) { - this.parent.childDone(this, 0); - return; + return this.parent.childDone(this, 0); } // Parent will inspect the `this.state.err` if (this.state == .err) { - this.parent.childDone(this, 1); - return; + return this.parent.childDone(this, 1); } + + unreachable; } -fn transitionToGlobState(this: *Expansion) void { +fn transitionToGlobState(this: *Expansion) Yield { var arena = Arena.init(this.base.interpreter.allocator); this.child_state = .{ .glob = .{ .walker = .{} } }; const pattern = this.current_out.items[0..]; @@ -290,16 +286,16 @@ fn transitionToGlobState(this: *Expansion) void { .result => {}, .err => |e| { this.state = .{ .err = bun.shell.ShellErr.newSys(e) }; - this.next(); - return; + return .{ .expansion = this }; }, } var task = ShellGlobTask.createOnMainThread(this.base.interpreter.allocator, &this.child_state.glob.walker, this); task.schedule(); + return .suspended; } -pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { +pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) ?Yield { switch (this.node.*) { .simple => |*simp| { const is_cmd_subst = this.expandSimpleNoIO(simp, &this.current_out, true); @@ -313,7 +309,7 @@ pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { .result => |s| s, .err => |e| { this.base.throw(&bun.shell.ShellErr.newSys(e)); - return false; + return .failed; }, }; var script = Script.init(this.base.interpreter, shell_state, &this.node.simple.cmd_subst.script, Script.ParentPtr.init(this), io); @@ -323,8 +319,7 @@ pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { .quoted = simp.cmd_subst.quoted, }, }; - script.start(); - return true; + return script.start(); } else { this.word_idx += 1; } @@ -346,7 +341,7 @@ pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { .result => |s| s, .err => |e| { this.base.throw(&bun.shell.ShellErr.newSys(e)); - return false; + return .failed; }, }; var script = Script.init(this.base.interpreter, shell_state, &simple_atom.cmd_subst.script, Script.ParentPtr.init(this), io); @@ -356,8 +351,7 @@ pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { .quoted = simple_atom.cmd_subst.quoted, }, }; - script.start(); - return true; + return script.start(); } else { this.word_idx += 1; this.child_state = .idle; @@ -366,7 +360,7 @@ pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { }, } - return false; + return null; } /// Remove a set of values from the beginning and end of a slice. @@ -453,7 +447,7 @@ fn convertNewlinesToSpacesSlow(i: usize, stdout: []u8) void { } } -pub fn childDone(this: *Expansion, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Expansion, child: ChildPtr, exit_code: ExitCode) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state != .done and this.state != .err); assert(this.child_state != .idle); @@ -491,14 +485,13 @@ pub fn childDone(this: *Expansion, child: ChildPtr, exit_code: ExitCode) void { this.word_idx += 1; this.child_state = .idle; child.deinit(); - this.next(); - return; + return .{ .expansion = this }; } @panic("Invalid child to Expansion, this indicates a bug in Bun. Please file a report on Github."); } -fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { +fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) Yield { log("{} onGlobWalkDone", .{this}); if (comptime bun.Environment.allow_assert) { assert(this.child_state == .glob); @@ -524,8 +517,7 @@ fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { this.child_state.glob.walker.deinit(true); this.child_state = .idle; this.state = .done; - this.next(); - return; + return .{ .expansion = this }; } const msg = std.fmt.allocPrint(bun.default_allocator, "no matches found: {s}", .{this.child_state.glob.walker.pattern}) catch bun.outOfMemory(); @@ -536,8 +528,7 @@ fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { }; this.child_state.glob.walker.deinit(true); this.child_state = .idle; - this.next(); - return; + return .{ .expansion = this }; } for (task.result.items) |sentinel_str| { @@ -550,7 +541,7 @@ fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { this.child_state.glob.walker.deinit(true); this.child_state = .idle; this.state = .done; - this.next(); + return .{ .expansion = this }; } /// If the atom is actually a command substitution then does nothing and returns true @@ -800,7 +791,7 @@ pub const ShellGlobTask = struct { pub fn runFromMainThread(this: *This) void { debug("runFromJS", .{}); - this.expansion.onGlobWalkDone(this); + this.expansion.onGlobWalkDone(this).run(); this.ref.unref(this.event_loop); } @@ -831,6 +822,7 @@ pub const ShellGlobTask = struct { const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const Allocator = std.mem.Allocator; diff --git a/src/shell/states/If.zig b/src/shell/states/If.zig index 42cd157d52..fda4e3b29d 100644 --- a/src/shell/states/If.zig +++ b/src/shell/states/If.zig @@ -53,11 +53,11 @@ pub fn init( }); } -pub fn start(this: *If) void { - this.next(); +pub fn start(this: *If) Yield { + return .{ .@"if" = this }; } -fn next(this: *If) void { +pub fn next(this: *If) Yield { while (this.state != .done) { switch (this.state) { .idle => { @@ -79,8 +79,7 @@ fn next(this: *If) void { } switch (this.node.else_parts.len()) { 0 => { - this.parent.childDone(this, 0); - return; + return this.parent.childDone(this, 0); }, 1 => { this.state.exec.state = .@"else"; @@ -98,8 +97,7 @@ fn next(this: *If) void { }, // done .then => { - this.parent.childDone(this, this.state.exec.last_exit_code); - return; + return this.parent.childDone(this, this.state.exec.last_exit_code); }, // if succesful, execute the elif's then branch // otherwise, move to the next elif, or to the final else if it exists @@ -114,8 +112,7 @@ fn next(this: *If) void { this.state.exec.state.elif.idx += 2; if (this.state.exec.state.elif.idx >= this.node.else_parts.len()) { - this.parent.childDone(this, 0); - return; + return this.parent.childDone(this, 0); } if (this.state.exec.state.elif.idx == this.node.else_parts.len() -| 1) { @@ -130,8 +127,7 @@ fn next(this: *If) void { continue; }, .@"else" => { - this.parent.childDone(this, this.state.exec.last_exit_code); - return; + return this.parent.childDone(this, this.state.exec.last_exit_code); }, } } @@ -140,15 +136,14 @@ fn next(this: *If) void { this.state.exec.stmt_idx += 1; const stmt = this.state.exec.stmts.getConst(idx); var newstmt = Stmt.init(this.base.interpreter, this.base.shell, stmt, this, this.io.copy()); - newstmt.start(); - return; + return newstmt.start(); }, - .waiting_write_err => return, // yield execution + .waiting_write_err => return .suspended, // yield execution .done => @panic("This code should not be reachable"), } } - this.parent.childDone(this, 0); + return this.parent.childDone(this, 0); } pub fn deinit(this: *If) void { @@ -157,7 +152,7 @@ pub fn deinit(this: *If) void { bun.destroy(this); } -pub fn childDone(this: *If, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *If, child: ChildPtr, exit_code: ExitCode) Yield { defer child.deinit(); if (this.state != .exec) { @@ -168,8 +163,8 @@ pub fn childDone(this: *If, child: ChildPtr, exit_code: ExitCode) void { exec.last_exit_code = exit_code; switch (exec.state) { - .cond => this.next(), - .then => this.next(), + .cond => return .{ .@"if" = this }, + .then => return .{ .@"if" = this }, .elif => { // if (exit_code == 0) { // exec.stmts = this.node.else_parts.getConst(exec.state.elif.idx + 1); @@ -178,15 +173,15 @@ pub fn childDone(this: *If, child: ChildPtr, exit_code: ExitCode) void { // this.next(); // return; // } - this.next(); - return; + return .{ .@"if" = this }; }, - .@"else" => this.next(), + .@"else" => return .{ .@"if" = this }, } } const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = bun.shell.Interpreter; diff --git a/src/shell/states/Pipeline.zig b/src/shell/states/Pipeline.zig index 45f9a3f1e4..e970aeb630 100644 --- a/src/shell/states/Pipeline.zig +++ b/src/shell/states/Pipeline.zig @@ -4,17 +4,24 @@ base: State, node: *const ast.Pipeline, /// Based on precedence rules pipeline can only be child of a stmt or /// binary +/// +/// *WARNING*: Do not directly call `this.parent.childDone`, it should +/// be handed in `Pipeline.next()` parent: ParentPtr, exited_count: u32, cmds: ?[]CmdOrResult, pipes: ?[]Pipe, io: IO, state: union(enum) { - idle, - executing, + starting_cmds: struct { + idx: u32, + }, + pending, waiting_write_err, - done, -} = .idle, + done: struct { + exit_code: ExitCode = 0, + }, +} = .{ .starting_cmds = .{ .idx = 0 } }, pub const ParentPtr = StatePtrUnion(.{ Stmt, @@ -67,16 +74,16 @@ fn getIO(this: *Pipeline) IO { return this.io; } -fn writeFailingError(this: *Pipeline, comptime fmt: []const u8, args: anytype) void { +fn writeFailingError(this: *Pipeline, comptime fmt: []const u8, args: anytype) Yield { const handler = struct { fn enqueueCb(ctx: *Pipeline) void { ctx.state = .waiting_write_err; } }; - this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + return this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); } -fn setupCommands(this: *Pipeline) bun.shell.interpret.CoroutineResult { +fn setupCommands(this: *Pipeline) ?Yield { const cmd_count = brk: { var i: u32 = 0; for (this.node.items) |*item| { @@ -89,7 +96,7 @@ fn setupCommands(this: *Pipeline) bun.shell.interpret.CoroutineResult { }; this.cmds = if (cmd_count >= 1) this.base.interpreter.allocator.alloc(CmdOrResult, this.node.items.len) catch bun.outOfMemory() else null; - if (this.cmds == null) return .cont; + if (this.cmds == null) return null; var pipes = this.base.interpreter.allocator.alloc(Pipe, if (cmd_count > 1) cmd_count - 1 else 1) catch bun.outOfMemory(); if (cmd_count > 1) { @@ -100,8 +107,7 @@ fn setupCommands(this: *Pipeline) bun.shell.interpret.CoroutineResult { closefd(pipe[1]); } const system_err = err.toShellSystemError(); - this.writeFailingError("bun: {s}\n", .{system_err.message}); - return .yield; + return this.writeFailingError("bun: {s}\n", .{system_err.message}); } } @@ -120,8 +126,7 @@ fn setupCommands(this: *Pipeline) bun.shell.interpret.CoroutineResult { .result => |s| s, .err => |err| { const system_err = err.toShellSystemError(); - this.writeFailingError("bun: {s}\n", .{system_err.message}); - return .yield; + return this.writeFailingError("bun: {s}\n", .{system_err.message}); }, }; this.cmds.?[i] = .{ @@ -142,55 +147,66 @@ fn setupCommands(this: *Pipeline) bun.shell.interpret.CoroutineResult { this.pipes = pipes; - return .cont; + return null; } -pub fn start(this: *Pipeline) void { - if (this.setupCommands() == .yield) return; - - if (this.state == .waiting_write_err or this.state == .done) return; - const cmds = this.cmds orelse { - this.state = .done; - this.parent.childDone(this, 0); - return; - }; - - if (comptime bun.Environment.allow_assert) { - assert(this.exited_count == 0); +pub fn start(this: *Pipeline) Yield { + if (this.setupCommands()) |yield| return yield; + if (this.state == .waiting_write_err or this.state == .done) return .suspended; + if (this.cmds == null) { + this.state = .{ .done = .{} }; + return .done; } + + assert(this.exited_count == 0); + log("pipeline start {x} (count={d})", .{ @intFromPtr(this), this.node.items.len }); + if (this.node.items.len == 0) { - this.state = .done; - this.parent.childDone(this, 0); - return; + this.state = .{ .done = .{} }; + return .done; } - for (cmds) |*cmd_or_result| { - assert(cmd_or_result.* == .cmd); - log("Pipeline start cmd", .{}); - var cmd = cmd_or_result.cmd; - cmd.call("start", .{}, void); + return .{ .pipeline = this }; +} + +pub fn next(this: *Pipeline) Yield { + switch (this.state) { + .starting_cmds => { + const cmds = this.cmds.?; + const idx = this.state.starting_cmds.idx; + if (idx >= cmds.len) { + this.state = .pending; + return .suspended; + } + log("Pipeline(0x{x}) starting cmd {d}/{d}", .{ @intFromPtr(this), idx + 1, cmds.len }); + this.state.starting_cmds.idx += 1; + const cmd_or_result = cmds[idx]; + assert(cmd_or_result == .cmd); + return cmd_or_result.cmd.call("start", .{}, Yield); + }, + .pending => shell.unreachableState("Pipeline.next", "pending"), + .waiting_write_err => shell.unreachableState("Pipeline.next", "waiting_write_err"), + .done => return this.parent.childDone(this, this.state.done.exit_code), } } -pub fn onIOWriterChunk(this: *Pipeline, _: usize, err: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Pipeline, _: usize, err: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .waiting_write_err); } if (err) |e| { this.base.throw(&shell.ShellErr.newSys(e)); - return; + return .failed; } - this.state = .done; - this.parent.childDone(this, 0); + this.state = .{ .done = .{} }; + return .done; } -pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) void { - if (comptime bun.Environment.allow_assert) { - assert(this.cmds.?.len > 0); - } +pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) Yield { + assert(this.cmds.?.len > 0); const idx = brk: { const ptr_value: u64 = @bitCast(child.ptr.repr); @@ -204,7 +220,7 @@ pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) void { @panic("Invalid pipeline state"); }; - log("pipeline child done {x} ({d}) i={d}", .{ @intFromPtr(this), exit_code, idx }); + log("Pipeline(0x{x}) child done ({d}) i={d}", .{ @intFromPtr(this), exit_code, idx }); // We duped the subshell for commands in the pipeline so we need to // deinitialize it. if (child.ptr.is(Cmd)) { @@ -226,18 +242,22 @@ pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) void { this.cmds.?[idx] = .{ .result = exit_code }; this.exited_count += 1; + log("Pipeline(0x{x}) check exited_count={d} cmds.len={d}", .{ @intFromPtr(this), this.exited_count, this.cmds.?.len }); if (this.exited_count >= this.cmds.?.len) { var last_exit_code: ExitCode = 0; - for (this.cmds.?) |cmd_or_result| { + var i: i64 = @as(i64, @intCast(this.cmds.?.len)) - 1; + while (i > 0) : (i -= 1) { + const cmd_or_result = this.cmds.?[@intCast(i)]; if (cmd_or_result == .result) { last_exit_code = cmd_or_result.result; break; } } - this.state = .done; - this.parent.childDone(this, last_exit_code); - return; + this.state = .{ .done = .{ .exit_code = last_exit_code } }; + return this.next(); } + + return .suspended; } pub fn deinit(this: *Pipeline) void { @@ -268,7 +288,8 @@ fn initializePipes(pipes: []Pipe, set_count: *u32) Maybe(void) { pipe[0] = .fromUV(fds[0]); pipe[1] = .fromUV(fds[1]); } else { - switch (bun.sys.socketpair( + switch (bun.sys.socketpairForShell( + // switch (bun.sys.socketpair( std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, @@ -304,6 +325,7 @@ fn readPipe(pipes: []Pipe, proc_idx: usize, io: *IO, evtloop: JSC.EventLoopHandl const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = bun.shell.Interpreter; diff --git a/src/shell/states/Script.zig b/src/shell/states/Script.zig index 0c2a365a05..c738311561 100644 --- a/src/shell/states/Script.zig +++ b/src/shell/states/Script.zig @@ -55,43 +55,40 @@ fn getIO(this: *Script) IO { return this.io; } -pub fn start(this: *Script) void { +pub fn start(this: *Script) Yield { if (this.node.stmts.len == 0) return this.finish(0); - this.next(); + return .{ .script = this }; } -fn next(this: *Script) void { +pub fn next(this: *Script) Yield { switch (this.state) { .normal => { - if (this.state.normal.idx >= this.node.stmts.len) return; + if (this.state.normal.idx >= this.node.stmts.len) return .suspended; const stmt_node = &this.node.stmts[this.state.normal.idx]; this.state.normal.idx += 1; var io = this.getIO(); var stmt = Stmt.init(this.base.interpreter, this.base.shell, stmt_node, this, io.ref().*); - stmt.start(); - return; + return stmt.start(); }, } } -fn finish(this: *Script, exit_code: ExitCode) void { +fn finish(this: *Script, exit_code: ExitCode) Yield { if (this.parent.ptr.is(Interpreter)) { log("Interpreter script finish", .{}); - this.base.interpreter.childDone(InterpreterChildPtr.init(this), exit_code); - return; + return this.base.interpreter.childDone(InterpreterChildPtr.init(this), exit_code); } - this.parent.childDone(this, exit_code); + return this.parent.childDone(this, exit_code); } -pub fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) Yield { child.deinit(); if (this.state.normal.idx >= this.node.stmts.len) { - this.finish(exit_code); - return; + return this.finish(exit_code); } - this.next(); + return this.next(); } pub fn deinit(this: *Script) void { @@ -117,6 +114,7 @@ pub fn deinitFromInterpreter(this: *Script) void { const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const Interpreter = bun.shell.Interpreter; const InterpreterChildPtr = Interpreter.InterpreterChildPtr; diff --git a/src/shell/states/Stmt.zig b/src/shell/states/Stmt.zig index 5c646e9831..58010a181f 100644 --- a/src/shell/states/Stmt.zig +++ b/src/shell/states/Stmt.zig @@ -46,16 +46,16 @@ pub fn init( return script; } -pub fn start(this: *Stmt) void { +pub fn start(this: *Stmt) Yield { if (bun.Environment.allow_assert) { assert(this.idx == 0); assert(this.last_exit_code == null); assert(this.currently_executing == null); } - this.next(); + return .{ .stmt = this }; } -pub fn next(this: *Stmt) void { +pub fn next(this: *Stmt) Yield { if (this.idx >= this.node.exprs.len) return this.parent.childDone(this, this.last_exit_code orelse 0); @@ -64,50 +64,51 @@ pub fn next(this: *Stmt) void { .binary => { const binary = Binary.init(this.base.interpreter, this.base.shell, child.binary, Binary.ParentPtr.init(this), this.io.copy()); this.currently_executing = ChildPtr.init(binary); - binary.start(); + return binary.start(); }, .cmd => { const cmd = Cmd.init(this.base.interpreter, this.base.shell, child.cmd, Cmd.ParentPtr.init(this), this.io.copy()); this.currently_executing = ChildPtr.init(cmd); - cmd.start(); + return cmd.start(); }, .pipeline => { const pipeline = Pipeline.init(this.base.interpreter, this.base.shell, child.pipeline, Pipeline.ParentPtr.init(this), this.io.copy()); this.currently_executing = ChildPtr.init(pipeline); - pipeline.start(); + return pipeline.start(); }, .assign => |assigns| { var assign_machine = this.base.interpreter.allocator.create(Assigns) catch bun.outOfMemory(); assign_machine.init(this.base.interpreter, this.base.shell, assigns, .shell, Assigns.ParentPtr.init(this), this.io.copy()); - assign_machine.start(); + return assign_machine.start(); }, .subshell => { switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, this.io, .subshell)) { .result => |shell_state| { var script = Subshell.init(this.base.interpreter, shell_state, child.subshell, Subshell.ParentPtr.init(this), this.io.copy()); - script.start(); + return script.start(); }, .err => |e| { this.base.throw(&bun.shell.ShellErr.newSys(e)); + return .failed; }, } }, .@"if" => { const if_clause = If.init(this.base.interpreter, this.base.shell, child.@"if", If.ParentPtr.init(this), this.io.copy()); - if_clause.start(); + return if_clause.start(); }, .condexpr => { const condexpr = CondExpr.init(this.base.interpreter, this.base.shell, child.condexpr, CondExpr.ParentPtr.init(this), this.io.copy()); - condexpr.start(); + return condexpr.start(); }, .@"async" => { const @"async" = Async.init(this.base.interpreter, this.base.shell, child.@"async", Async.ParentPtr.init(this), this.io.copy()); - @"async".start(); + return @"async".start(); }, } } -pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) Yield { const data = child.ptr.repr.data; log("child done Stmt {x} child({s})={x} exit={d}", .{ @intFromPtr(this), child.tagName(), @as(usize, @intCast(child.ptr.repr._ptr)), exit_code }); this.last_exit_code = exit_code; @@ -116,7 +117,7 @@ pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) void { log("{d} {d}", .{ data, data2 }); child.deinit(); this.currently_executing = null; - this.next(); + return this.next(); } pub fn deinit(this: *Stmt) void { @@ -130,6 +131,7 @@ pub fn deinit(this: *Stmt) void { const bun = @import("bun"); +const Yield = bun.shell.Yield; const Interpreter = bun.shell.Interpreter; const StatePtrUnion = bun.shell.interpret.StatePtrUnion; const ast = bun.shell.AST; diff --git a/src/shell/states/Subshell.zig b/src/shell/states/Subshell.zig index 1d875d3c44..a83168acde 100644 --- a/src/shell/states/Subshell.zig +++ b/src/shell/states/Subshell.zig @@ -25,7 +25,6 @@ pub const ParentPtr = StatePtrUnion(.{ pub const ChildPtr = StatePtrUnion(.{ Script, - Subshell, Expansion, }); @@ -49,25 +48,24 @@ pub fn init( }); } -pub fn start(this: *Subshell) void { +pub fn start(this: *Subshell) Yield { log("{} start", .{this}); const script = Script.init(this.base.interpreter, this.base.shell, &this.node.script, Script.ParentPtr.init(this), this.io.copy()); - script.start(); + return script.start(); } -pub fn next(this: *Subshell) void { +pub fn next(this: *Subshell) Yield { while (this.state != .done) { switch (this.state) { .idle => { this.state = .{ .expanding_redirect = .{ .expansion = undefined }, }; - this.next(); + return .{ .subshell = this }; }, .expanding_redirect => { if (this.state.expanding_redirect.idx >= 1) { - this.transitionToExec(); - return; + return this.transitionToExec(); } this.state.expanding_redirect.idx += 1; @@ -75,8 +73,7 @@ pub fn next(this: *Subshell) void { // `expanding_args` state const node_to_expand = brk: { if (this.node.redirect != null and this.node.redirect.? == .atom) break :brk &this.node.redirect.?.atom; - this.transitionToExec(); - return; + return this.transitionToExec(); }; Expansion.init( @@ -93,25 +90,24 @@ pub fn next(this: *Subshell) void { this.io.copy(), ); - this.state.expanding_redirect.expansion.start(); - return; + return this.state.expanding_redirect.expansion.start(); }, - .wait_write_err, .exec => return, + .wait_write_err, .exec => return .suspended, .done => @panic("This should not be possible."), } } - this.parent.childDone(this, 0); + return this.parent.childDone(this, 0); } -pub fn transitionToExec(this: *Subshell) void { +pub fn transitionToExec(this: *Subshell) Yield { log("{} transitionToExec", .{this}); const script = Script.init(this.base.interpreter, this.base.shell, &this.node.script, Script.ParentPtr.init(this), this.io.copy()); this.state = .exec; - script.start(); + return script.start(); } -pub fn childDone(this: *Subshell, child_ptr: ChildPtr, exit_code: ExitCode) void { +pub fn childDone(this: *Subshell, child_ptr: ChildPtr, exit_code: ExitCode) Yield { defer child_ptr.deinit(); this.exit_code = exit_code; if (child_ptr.ptr.is(Expansion) and exit_code != 0) { @@ -119,19 +115,19 @@ pub fn childDone(this: *Subshell, child_ptr: ChildPtr, exit_code: ExitCode) void const err = this.state.expanding_redirect.expansion.state.err; defer err.deinit(bun.default_allocator); this.state.expanding_redirect.expansion.deinit(); - this.writeFailingError("{}\n", .{err}); - return; + return this.writeFailingError("{}\n", .{err}); } - this.next(); + return .{ .subshell = this }; } if (child_ptr.ptr.is(Script)) { - this.parent.childDone(this, exit_code); - return; + return this.parent.childDone(this, exit_code); } + + bun.shell.unreachableState("Subshell.childDone", "expected Script or Expansion"); } -pub fn onIOWriterChunk(this: *Subshell, _: usize, err: ?JSC.SystemError) void { +pub fn onIOWriterChunk(this: *Subshell, _: usize, err: ?JSC.SystemError) Yield { if (comptime bun.Environment.allow_assert) { assert(this.state == .wait_write_err); } @@ -141,7 +137,7 @@ pub fn onIOWriterChunk(this: *Subshell, _: usize, err: ?JSC.SystemError) void { } this.state = .done; - this.parent.childDone(this, this.exit_code); + return this.parent.childDone(this, this.exit_code); } pub fn deinit(this: *Subshell) void { @@ -151,17 +147,18 @@ pub fn deinit(this: *Subshell) void { bun.destroy(this); } -pub fn writeFailingError(this: *Subshell, comptime fmt: []const u8, args: anytype) void { +pub fn writeFailingError(this: *Subshell, comptime fmt: []const u8, args: anytype) Yield { const handler = struct { fn enqueueCb(ctx: *Subshell) void { ctx.state = .wait_write_err; } }; - this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + return this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); } const std = @import("std"); const bun = @import("bun"); +const Yield = bun.shell.Yield; const shell = bun.shell; const Interpreter = bun.shell.Interpreter; diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index ed36bc17cf..93a2e17b41 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -1,5 +1,6 @@ const default_allocator = bun.default_allocator; const bun = @import("bun"); +const Yield = bun.shell.Yield; const Environment = bun.Environment; const strings = bun.strings; const Output = bun.Output; @@ -796,6 +797,8 @@ pub const ShellSubprocess = struct { } } + const no_sigpipe = if (shellio.stdout) |iowriter| !iowriter.flags.is_socket else true; + var spawn_options = bun.spawn.SpawnOptions{ .cwd = spawn_args.cwd, .stdin = switch (spawn_args.stdio[0].asSpawnOption(0)) { @@ -828,6 +831,9 @@ pub const ShellSubprocess = struct { .loop = event_loop, }, }; + if (bun.Environment.isPosix) { + spawn_options.no_sigpipe = no_sigpipe; + } spawn_args.argv.append(allocator, null) catch { return .{ .err = .{ .custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory() } }; @@ -1003,7 +1009,10 @@ pub const PipeReader = struct { .bytelist => { this.bytelist.deinitWithAllocator(bun.default_allocator); }, - .array_buffer => {}, + .array_buffer => { + // FIXME: SHOULD THIS BE HERE? + this.array_buffer.buf.deinit(); + }, } } }; @@ -1018,7 +1027,7 @@ pub const PipeReader = struct { if (this.dead or this.err != null) return; log("CapturedWriter(0x{x}, {s}) doWrite len={d} parent_amount={d}", .{ @intFromPtr(this), @tagName(this.parent().out_type), chunk.len, this.parent().buffered_output.len() }); - this.writer.enqueue(this, null, chunk); + this.writer.enqueue(this, null, chunk).run(); } pub fn getBuffer(this: *CapturedWriter) []const u8 { @@ -1047,16 +1056,17 @@ pub const PipeReader = struct { return this.written + just_written >= this.parent().buffered_output.len(); } - pub fn onIOWriterChunk(this: *CapturedWriter, amount: usize, err: ?JSC.SystemError) void { + pub fn onIOWriterChunk(this: *CapturedWriter, amount: usize, err: ?JSC.SystemError) Yield { log("CapturedWriter({x}, {s}) onWrite({d}, has_err={any}) total_written={d} total_to_write={d}", .{ @intFromPtr(this), @tagName(this.parent().out_type), amount, err != null, this.written + amount, this.parent().buffered_output.len() }); this.written += amount; if (err) |e| { log("CapturedWriter(0x{x}, {s}) onWrite errno={d} errmsg={} errfd={} syscall={}", .{ @intFromPtr(this), @tagName(this.parent().out_type), e.errno, e.message, e.fd, e.syscall }); this.err = e; - this.parent().trySignalDoneToCmd(); + return this.parent().trySignalDoneToCmd(); } else if (this.written >= this.parent().buffered_output.len() and !(this.parent().state == .pending)) { - this.parent().trySignalDoneToCmd(); + return this.parent().trySignalDoneToCmd(); } + return .suspended; } pub fn onError(this: *CapturedWriter, err: bun.sys.Error) void { @@ -1093,7 +1103,7 @@ pub const PipeReader = struct { } pub fn onCapturedWriterDone(this: *PipeReader) void { - this.trySignalDoneToCmd(); + this.trySignalDoneToCmd().run(); } pub fn create(event_loop: JSC.EventLoopHandle, process: *ShellSubprocess, result: StdioResult, capture: ?*sh.IOWriter, out_type: bun.shell.Subprocess.OutKind) *PipeReader { @@ -1186,7 +1196,7 @@ pub const PipeReader = struct { // we need to ref because the process might be done and deref inside signalDoneToCmd and we wanna to keep it alive to check this.process this.ref(); defer this.deref(); - this.trySignalDoneToCmd(); + this.trySignalDoneToCmd().run(); if (this.process) |process| { // this.process = null; @@ -1197,8 +1207,8 @@ pub const PipeReader = struct { pub fn trySignalDoneToCmd( this: *PipeReader, - ) void { - if (!this.isDone()) return; + ) Yield { + if (!this.isDone()) return .suspended; log("signalDoneToCmd ({x}: {s}) isDone={any}", .{ @intFromPtr(this), @tagName(this.out_type), this.isDone() }); if (bun.Environment.allow_assert) assert(this.process != null); if (this.process) |proc| { @@ -1216,9 +1226,10 @@ pub const PipeReader = struct { } break :brk null; }; - cmd.bufferedOutputClose(this.out_type, e); + return cmd.bufferedOutputClose(this.out_type, e); } } + return .suspended; } pub fn kind(reader: *const PipeReader, process: *const ShellSubprocess) StdioKind { @@ -1309,7 +1320,7 @@ pub const PipeReader = struct { // we need to ref because the process might be done and deref inside signalDoneToCmd and we wanna to keep it alive to check this.process this.ref(); defer this.deref(); - this.trySignalDoneToCmd(); + this.trySignalDoneToCmd().run(); if (this.process) |process| { // this.process = null; process.onCloseIO(this.kind(process)); diff --git a/src/sys.zig b/src/sys.zig index 211730b523..19eb84d83d 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -519,6 +519,17 @@ pub const Error = struct { return null; } + pub fn msg(this: Error) ?[]const u8 { + if (this.getErrorCodeTagName()) |resolved_errno| { + const code, const system_errno = resolved_errno; + if (coreutils_error_map.get(system_errno)) |label| { + return label; + } + return code; + } + return null; + } + /// Simpler formatting which does not allocate a message pub fn toShellSystemError(this: Error) SystemError { @setEvalBranchQuota(1_000_000); @@ -2922,6 +2933,7 @@ pub fn setsockopt(fd: bun.FileDescriptor, level: c_int, optname: u32, value: i32 pub fn setNoSigpipe(fd: bun.FileDescriptor) Maybe(void) { if (comptime Environment.isMac) { + log("setNoSigpipe({})", .{fd}); return switch (setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.NOSIGPIPE, 1)) { .result => .{ .result = {} }, .err => |err| .{ .err = err }, @@ -2932,13 +2944,49 @@ pub fn setNoSigpipe(fd: bun.FileDescriptor) Maybe(void) { } const socketpair_t = if (Environment.isLinux) i32 else c_uint; +const NonblockingStatus = enum { blocking, nonblocking }; /// libc socketpair() except it defaults to: /// - SOCK_CLOEXEC on Linux /// - SO_NOSIGPIPE on macOS /// /// On POSIX it otherwise makes it do O_CLOEXEC. -pub fn socketpair(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: enum { blocking, nonblocking }) Maybe([2]bun.FileDescriptor) { +pub fn socketpair(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: NonblockingStatus) Maybe([2]bun.FileDescriptor) { + return socketpairImpl(domain, socktype, protocol, nonblocking_status, false); +} + +/// We can't actually use SO_NOSIGPIPE for the stdout of a +/// subprocess we don't control because they have different +/// semantics. +/// +/// For example, when running the shell script: +/// `grep hi src/js_parser/zig | echo hi`, +/// +/// The `echo hi` command will terminate first and close its +/// end of the socketpair. +/// +/// With SO_NOSIGPIPE, when `grep` continues and tries to write to +/// stdout, `ESIGPIPE` is returned and then `grep` handles this +/// and prints `grep: stdout: Broken pipe` +/// +/// So the solution is to NOT set SO_NOGSIGPIPE in that scenario. +/// +/// I think this only applies to stdout/stderr, not stdin. `read(...)` +/// and `recv(...)` do not return EPIPE as error codes. +pub fn socketpairForShell(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: NonblockingStatus) Maybe([2]bun.FileDescriptor) { + return socketpairImpl(domain, socktype, protocol, nonblocking_status, true); +} + +pub const ShellSigpipeConfig = enum { + /// Only SO_NOSIGPIPE for the socket in the pair + /// that *we're* going to use, don't touch the one + /// we hand off to the subprocess + spawn, + /// off completely + pipeline, +}; + +pub fn socketpairImpl(domain: socketpair_t, socktype: socketpair_t, protocol: socketpair_t, nonblocking_status: NonblockingStatus, for_shell: bool) Maybe([2]bun.FileDescriptor) { if (comptime !Environment.isPosix) @compileError("linux only!"); var fds_i: [2]syscall.fd_t = .{ 0, 0 }; @@ -2980,10 +3028,15 @@ pub fn socketpair(domain: socketpair_t, socktype: socketpair_t, protocol: socket } if (comptime Environment.isMac) { - inline for (0..2) |i| { - switch (setNoSigpipe(.fromNative(fds_i[i]))) { - .err => |err| break :err err, - else => {}, + if (for_shell) { + // see the comment on `socketpairForShell` for why we don't + // set SO_NOSIGPIPE here + } else { + inline for (0..2) |i| { + switch (setNoSigpipe(.fromNative(fds_i[i]))) { + .err => |err| break :err err, + else => {}, + } } } } @@ -3854,6 +3907,7 @@ pub fn getFileSize(fd: bun.FileDescriptor) Maybe(usize) { } pub fn isPollable(mode: mode_t) bool { + if (comptime bun.Environment.isWindows) return false; return posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode); } diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 235cf0f511..6c405342cc 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -7,7 +7,7 @@ import { $ } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { mkdir, rm, stat } from "fs/promises"; -import { bunExe, isWindows, runWithErrorPromise, tempDirWithFiles, tmpdirSync } from "harness"; +import { bunExe, isPosix, isWindows, runWithErrorPromise, tempDirWithFiles, tmpdirSync } from "harness"; import { join, sep } from "path"; import { createTestBuilder, sortedShellOutput } from "./util"; const TestBuilder = createTestBuilder(import.meta.path); @@ -974,6 +974,132 @@ describe("deno_task", () => { TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stderr)' 2> output.txt` .fileEquals("output.txt", "1\n") .runAsTest("pipe with redirect stderr to file"); + + if (isPosix) { + TestBuilder.command`ls . | echo hi`.exitCode(0).stdout("hi\n").runAsTest("broken pipe builtin"); + TestBuilder.command`grep hi src/js_parser.zig | echo hi` + .exitCode(0) + .stdout("hi\n") + .stderr("") + .runAsTest("broken pipe subproc"); + } + + TestBuilder.command`${BUN} -e 'process.exit(1)' | ${BUN} -e 'console.log("hi")'` + .exitCode(0) + .stdout("hi\n") + .runAsTest("last exit code"); + + TestBuilder.command`ls sldkfjlskdjflksdjflksjdf | ${BUN} -e 'console.log("hi")'` + .exitCode(0) + .stdout("hi\n") + .stderr("ls: sldkfjlskdjflksdjflksjdf: No such file or directory\n") + .runAsTest("last exit code"); + + TestBuilder.command`ksldfjsdflsdfjskdfjlskdjflksdf | ${BUN} -e 'console.log("hi")'` + .exitCode(0) + .stdout("hi\n") + .stderr("bun: command not found: ksldfjsdflsdfjskdfjlskdjflksdf\n") + .runAsTest("last exit code 2"); + + TestBuilder.command`echo hi | ${BUN} -e 'process.exit(69)'`.exitCode(69).stdout("").runAsTest("last exit code 3"); + + describe("pipeline stack behavior", () => { + // Test deep pipeline chains to stress the stack implementation + TestBuilder.command`echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("5\n") + .runAsTest("deep pipeline chain"); + + // Test very deep chains that could overflow a recursion-based implementation + TestBuilder.command`echo start | echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | echo 6 | echo 7 | echo 8 | echo 9 | echo 10 | echo 11 | echo 12 | echo 13 | echo 14 | echo 15 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("15\n") + .runAsTest("very deep pipeline chain"); + + // Test nested pipelines in subshells + TestBuilder.command`echo outer | (echo inner1 | echo inner2) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("inner2\n") + .runAsTest("nested pipeline in subshell"); + + // Test nested pipelines with command substitution + TestBuilder.command`echo $(echo nested | echo pipe) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("pipe\n") + .runAsTest("nested pipeline in command substitution"); + + // Test multiple nested pipelines + TestBuilder.command`(echo a | echo b) | (echo c | echo d) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("d\n") + .runAsTest("multiple nested pipelines"); + + // Test pipeline with conditional that contains another pipeline + TestBuilder.command`echo test | (echo inner | echo nested && echo after) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("nested\nafter\n") + .runAsTest("pipeline with conditional containing pipeline"); + + // Test deeply nested subshells with pipelines + TestBuilder.command`echo start | (echo l1 | (echo l2 | (echo l3 | echo final))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("final\n") + .runAsTest("deeply nested subshells with pipelines"); + + // Test pipeline stack unwinding with early termination + TestBuilder.command`echo 1 | echo 2 | echo 3 | false | echo 4 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("4\n") + .runAsTest("pipeline with failing command"); + + // Test interleaved pipelines and conditionals + TestBuilder.command`echo a | echo b && echo c | echo d | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("b\nd\n") + .runAsTest("interleaved pipelines and conditionals"); + + // Test pipeline with background process (when supported) + TestBuilder.command`echo foreground | echo pipe && (echo background &) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("pipe\n") + .todo("background processes not fully supported") + .runAsTest("pipeline with background process"); + + // Test rapid pipeline creation and destruction + TestBuilder.command`echo 1 | echo 2; echo 3 | echo 4; echo 5 | echo 6 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("2\n4\n6\n") + .runAsTest("rapid pipeline creation"); + + // Test pipeline stack with error propagation + TestBuilder.command`echo start | nonexistent_command | echo after || echo fallback` + .stdout("after\n") + .stderr("bun: command not found: nonexistent_command\n") + .runAsTest("pipeline error propagation"); + + // Test nested pipeline with mixed success/failure + TestBuilder.command`(echo success | echo works) | (nonexistent | echo backup) || echo final_fallback` + .stdout("backup\n") + .stderr(s => s.includes("command not found")) + .runAsTest("nested pipeline mixed success failure"); + + TestBuilder.command`echo 0 | echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | echo 6 | echo 7 | echo 8 | echo 9 | echo 10 | echo 11 | echo 12 | echo 13 | echo 14 | echo 15 | echo 16 | echo 17 | echo 18 | echo 19 | echo 20 | echo 21 | echo 22 | echo 23 | echo 24 | echo 25 | echo 26 | echo 27 | echo 28 | echo 29 | echo 30 | echo 31 | echo 32 | echo 33 | echo 34 | echo 35 | echo 36 | echo 37 | echo 38 | echo 39 | echo 40 | echo 41 | echo 42 | echo 43 | echo 44 | echo 45 | echo 46 | echo 47 | echo 48 | echo 49 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("49\n") + .runAsTest("long pipeline builtin"); + + TestBuilder.command`echo 0 | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("0\n") + .runAsTest("long pipeline"); + + // Test pipeline stack consistency with complex nesting + TestBuilder.command`echo outer | (echo inner1 | echo inner2 | (echo deep1 | echo deep2) | echo inner3) | echo final | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("final\n") + .runAsTest("complex nested pipeline consistency"); + + // Test pipeline interruption and resumption + TestBuilder.command`echo start | (echo pause; echo resume) | echo end | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("end\n") + .runAsTest("pipeline interruption resumption"); + + // Test extremely deep nested pipeline - this would cause stack overflow with recursion + TestBuilder.command`echo level0 | (echo level1 | (echo level2 | (echo level3 | (echo level4 | (echo level5 | (echo level6 | (echo level7 | (echo level8 | (echo level9 | (echo level10 | (echo level11 | (echo level12 | (echo level13 | (echo level14 | (echo level15 | (echo level16 | (echo level17 | (echo level18 | (echo level19 | echo deep_final))))))))))))))))))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("deep_final\n") + .runAsTest("extremely deep nested pipeline"); + + // Test pathological case: deep nesting + long chains + TestBuilder.command`echo start | (echo n1 | echo n2 | echo n3 | (echo deep1 | echo deep2 | echo deep3 | (echo deeper1 | echo deeper2 | echo deeper3 | (echo deepest1 | echo deepest2 | echo deepest_final)))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + .stdout("deepest_final\n") + .runAsTest("pathological deep nesting with long chains"); + }); }); describe("redirects", async function igodf() { @@ -1190,14 +1316,17 @@ describe("deno_task", () => { const expected = [ '\\x1b[B', '\\x0D' - ] + ].join("") let i = 0 + let buf = "" const writer = Bun.stdout.writer(); process.stdin.on("data", async chunk => { const input = chunk.toString(); - expect(input).toEqual(expected[i++]) - writer.write(input) - await writer.flush() + buf += input; + if (buf === expected) { + writer.write(buf); + await writer.flush(); + } }); `; diff --git a/test/js/bun/shell/commands/echo.test.ts b/test/js/bun/shell/commands/echo.test.ts new file mode 100644 index 0000000000..050d0bf3b6 --- /dev/null +++ b/test/js/bun/shell/commands/echo.test.ts @@ -0,0 +1,77 @@ +import { describe } from "bun:test"; +import { createTestBuilder } from "../test_builder"; +const TestBuilder = createTestBuilder(import.meta.path); + +describe("echo", async () => { + TestBuilder.command`echo`.exitCode(0).stdout("\n").stderr("").runAsTest("no arguments outputs newline"); + + TestBuilder.command`echo hello`.exitCode(0).stdout("hello\n").stderr("").runAsTest("single argument"); + + TestBuilder.command`echo hello world`.exitCode(0).stdout("hello world\n").stderr("").runAsTest("multiple arguments"); + + TestBuilder.command`echo "hello world"`.exitCode(0).stdout("hello world\n").stderr("").runAsTest("quoted argument"); + + TestBuilder.command`echo hello world` + .exitCode(0) + .stdout("hello world\n") + .stderr("") + .runAsTest("multiple spaces collapsed"); + + TestBuilder.command`echo ""`.exitCode(0).stdout("\n").stderr("").runAsTest("empty string"); + + TestBuilder.command`echo one two three four` + .exitCode(0) + .stdout("one two three four\n") + .stderr("") + .runAsTest("many arguments"); +}); + +describe("echo -n flag", async () => { + TestBuilder.command`echo -n`.exitCode(0).stdout("").stderr("").runAsTest("no arguments with -n flag"); + + TestBuilder.command`echo -n hello`.exitCode(0).stdout("hello").stderr("").runAsTest("single argument with -n flag"); + + TestBuilder.command`echo -n hello world` + .exitCode(0) + .stdout("hello world") + .stderr("") + .runAsTest("multiple arguments with -n flag"); + + TestBuilder.command`echo -n "hello world"` + .exitCode(0) + .stdout("hello world") + .stderr("") + .runAsTest("quoted argument with -n flag"); + + TestBuilder.command`echo -n ""`.exitCode(0).stdout("").stderr("").runAsTest("empty string with -n flag"); + + TestBuilder.command`echo -n one two three` + .exitCode(0) + .stdout("one two three") + .stderr("") + .runAsTest("many arguments with -n flag"); +}); + +describe("echo error handling", async () => { + TestBuilder.command`echo -x`.exitCode(0).stdout("-x\n").runAsTest("invalid flag"); + + TestBuilder.command`echo -abc`.exitCode(0).stdout("-abc\n").runAsTest("invalid multi-char flag"); + + TestBuilder.command`echo --invalid`.exitCode(0).stdout("--invalid\n").runAsTest("invalid long flag"); +}); + +describe("echo special cases", async () => { + TestBuilder.command`echo -n -n hello` + .exitCode(0) + .stdout("-n hello") + .stderr("") + .runAsTest("-n flag with -n as argument"); + + TestBuilder.command`echo -- -n hello` + .exitCode(0) + .stdout("-- -n hello\n") + .stderr("") + .runAsTest("double dash treated as argument"); + + TestBuilder.command`echo "\n"`.exitCode(0).stdout("\\n\n").stderr("").runAsTest("literal backslash n"); +}); diff --git a/test/js/bun/shell/file-io.test.ts b/test/js/bun/shell/file-io.test.ts new file mode 100644 index 0000000000..6c8f8e87e3 --- /dev/null +++ b/test/js/bun/shell/file-io.test.ts @@ -0,0 +1,143 @@ +import { describe } from "bun:test"; +import { createTestBuilder } from "./test_builder"; +const TestBuilder = createTestBuilder(import.meta.path); + +describe("IOWriter file output redirection", () => { + describe("basic file redirection", () => { + TestBuilder.command`echo "hello world" > output.txt` + .exitCode(0) + .fileEquals("output.txt", "hello world\n") + .runAsTest("simple echo to file"); + + TestBuilder.command`echo -n "" > empty.txt` + .exitCode(0) + .fileEquals("empty.txt", "") + .runAsTest("empty output to file"); + + TestBuilder.command`echo "" > zero.txt` + .exitCode(0) + .fileEquals("zero.txt", "\n") + .runAsTest("zero-length write should trigger onIOWriterChunk callback"); + }); + + describe("drainBufferedData edge cases", () => { + TestBuilder.command`echo -n ${"x".repeat(1024 * 10)} > large.txt` + .exitCode(0) + .fileEquals("large.txt", "x".repeat(1024 * 10)) + .runAsTest("large single write"); + + TestBuilder.command`mkdir -p subdir && echo "test" > subdir/file.txt` + .exitCode(0) + .fileEquals("subdir/file.txt", "test\n") + .runAsTest("write to subdirectory"); + }); + + describe("file system error conditions", () => { + TestBuilder.command`echo "should fail" > /dev/null/invalid/path` + .exitCode(1) + .stderr_contains("directory: /dev/null/invalid/path") + .runAsTest("write to invalid path should fail"); + + TestBuilder.command`echo "should fail" > /nonexistent/file.txt` + .exitCode(1) + .stderr_contains("No such file or directory") + .runAsTest("write to non-existent directory should fail"); + }); + + describe("special file types", () => { + TestBuilder.command`echo "disappear" > /dev/null`.exitCode(0).stdout("").runAsTest("write to /dev/null"); + }); + + describe("writer queue and bump behavior", () => { + TestBuilder.command`echo "single" > single_writer.txt` + .exitCode(0) + .fileEquals("single_writer.txt", "single\n") + .runAsTest("single writer completion and cleanup"); + + TestBuilder.command`echo "robust test" > robust.txt` + .exitCode(0) + .fileEquals("robust.txt", "robust test\n") + .runAsTest("writer marked as dead during write"); + + TestBuilder.command`echo "captured content" > capture.txt` + .exitCode(0) + .fileEquals("capture.txt", "captured content\n") + .stdout("") + .runAsTest("bytelist capture during file write"); + }); + + describe("error handling and unreachable paths", () => { + TestBuilder.command`echo -n ${"A".repeat(2 * 1024)} > atomic.txt` + .exitCode(0) + .fileEquals("atomic.txt", "A".repeat(2 * 1024)) + .runAsTest("attempt to trigger partial write panic"); + + TestBuilder.command`echo "synchronous" > sync_write.txt` + .exitCode(0) + .fileEquals("sync_write.txt", "synchronous\n") + .runAsTest("EAGAIN should never occur for files"); + + TestBuilder.command`echo "error test" > nonexistent_dir/file.txt` + .exitCode(1) + .stderr_contains("No such file or directory") + .runAsTest("write error propagation"); + }); + + describe("file permissions and creation", () => { + TestBuilder.command`echo "new file" > new_file.txt` + .exitCode(0) + .fileEquals("new_file.txt", "new file\n") + .runAsTest("file creation with default permissions"); + + TestBuilder.command`echo "original" > overwrite.txt && echo "short" > overwrite.txt` + .exitCode(0) + .fileEquals("overwrite.txt", "short\n") + .runAsTest("overwrite existing file"); + + TestBuilder.command`echo "line1" > append.txt && echo "line2" >> append.txt && echo "line3" >> append.txt` + .exitCode(0) + .fileEquals("append.txt", "line1\nline2\nline3\n") + .runAsTest("append to existing file"); + }); + + // describe("concurrent operations", () => { + // TestBuilder.command`echo "content 0" > concurrent_0.txt & echo "content 1" > concurrent_1.txt & echo "content 2" > concurrent_2.txt & wait` + // .exitCode(0) + // .fileEquals("concurrent_0.txt", "content 0\n") + // .fileEquals("concurrent_1.txt", "content 1\n") + // .fileEquals("concurrent_2.txt", "content 2\n") + // .runAsTest("concurrent writes to different files"); + + // TestBuilder.command`echo "iteration 0" > rapid.txt && echo "iteration 1" > rapid.txt && echo "iteration 2" > rapid.txt` + // .exitCode(0) + // .fileEquals("rapid.txt", "iteration 2\n") + // .runAsTest("rapid sequential writes to same file"); + // }); + + describe("additional TestBuilder integration", () => { + TestBuilder.command`echo "builder test" > output.txt` + .exitCode(0) + .fileEquals("output.txt", "builder test\n") + .runAsTest("basic file output"); + + TestBuilder.command`printf "no newline" > no_newline.txt` + .exitCode(0) + .fileEquals("no_newline.txt", "no newline") + .runAsTest("output without trailing newline"); + + TestBuilder.command`echo "first" > multi.txt && echo "second" >> multi.txt` + .exitCode(0) + .fileEquals("multi.txt", "first\nsecond\n") + .runAsTest("write then append"); + + TestBuilder.command`echo "test with spaces in filename" > "file with spaces.txt"` + .exitCode(0) + .fileEquals("file with spaces.txt", "test with spaces in filename\n") + .runAsTest("write to file with spaces in name"); + + TestBuilder.command`echo "pipe test" | cat > pipe_output.txt` + .exitCode(0) + .fileEquals("pipe_output.txt", "pipe test\n") + .runAsTest("pipe with file redirection"); + }); +}); diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index b63c3822eb..bfcb6f24ff 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -1,7 +1,7 @@ import { $ } from "bun"; import { heapStats } from "bun:jsc"; import { describe, expect, test } from "bun:test"; -import { bunEnv, tempDirWithFiles } from "harness"; +import { bunEnv, isPosix, tempDirWithFiles } from "harness"; import { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs"; import { devNull, tmpdir } from "os"; import { join } from "path"; @@ -113,6 +113,7 @@ describe("fd leak", () => { prev = val; prevprev = val; } else { + // console.error('Prev', prev, 'Val', val, 'Diff', Math.abs(prev - val), 'Threshold', threshold); if (!(Math.abs(prev - val) < threshold)) process.exit(1); } } @@ -125,7 +126,7 @@ describe("fd leak", () => { const { stdout, stderr, exitCode } = Bun.spawnSync([process.argv0, "--smol", "test", tempfile], { env: bunEnv, }); - // console.log('STDOUT:', stdout.toString(), '\n\nSTDERR:', stderr.toString()); + // console.log("STDOUT:", stdout.toString(), "\n\nSTDERR:", stderr.toString()); if (exitCode != 0) { console.log("\n\nSTDERR:", stderr.toString()); } @@ -139,8 +140,8 @@ describe("fd leak", () => { }); // Use text of this file so its big enough to cause a leak - memLeakTest("ArrayBuffer", () => TestBuilder.command`cat ${import.meta.filename} > ${new ArrayBuffer(1 << 20)}`, 100); - memLeakTest("Buffer", () => TestBuilder.command`cat ${import.meta.filename} > ${Buffer.alloc(1 << 20)}`, 100); + memLeakTest("ArrayBuffer", () => TestBuilder.command`cat ${import.meta.filename} > ${new ArrayBuffer(128)}`, 100); + memLeakTest("Buffer", () => TestBuilder.command`cat ${import.meta.filename} > ${Buffer.alloc(128)}`, 100); memLeakTest( "Blob_something", () => @@ -169,6 +170,209 @@ describe("fd leak", () => { ); memLeakTest("String", () => TestBuilder.command`echo ${Array(4096).fill("a").join("")}`.stdout(() => {}), 100); + function memLeakTestProtect( + name: string, + className: string, + constructStmt: string, + builder: string, + posixOnly: boolean = false, + runs: number = 5, + ) { + const runTheTest = !posixOnly ? true : isPosix; + test.if(runTheTest)( + `memleak_protect_${name}`, + async () => { + const tempfile = join(tmpdir(), "script.ts"); + + const filepath = import.meta.dirname; + const testcode = await Bun.file(join(filepath, "./test_builder.ts")).text(); + + writeFileSync(tempfile, testcode); + + const impl = /* ts */ ` + import { heapStats } from "bun:jsc"; + const TestBuilder = createTestBuilder(import.meta.path); + + Bun.gc(true); + const startValue = heapStats().protectedObjectTypeCounts.${className} ?? 0; + for (let i = 0; i < ${runs}; i++) { + await (async function() { + let val = ${constructStmt} + await ${builder} + })() + Bun.gc(true); + + let value = heapStats().protectedObjectTypeCounts.${className} ?? 0; + + if (value > startValue) { + console.error('Leaked ${className} objects') + process.exit(1); + } + } + `; + + appendFileSync(tempfile, impl); + + // console.log("THE CODE", readFileSync(tempfile, "utf-8")); + + const { stdout, stderr, exitCode } = Bun.spawnSync([process.argv0, "--smol", "test", tempfile], { + env: bunEnv, + }); + // console.log("STDOUT:", stdout.toString(), "\n\nSTDERR:", stderr.toString()); + if (exitCode != 0) { + console.log("\n\nSTDERR:", stderr.toString()); + } + expect(exitCode).toBe(0); + }, + 100_000, + ); + } + + memLeakTestProtect( + "ArrayBuffer", + "ArrayBuffer", + "new ArrayBuffer(64)", + "TestBuilder.command`cat ${import.meta.filename} > ${val}`", + ); + memLeakTestProtect( + "Buffer", + "Buffer", + "Buffer.alloc(64)", + "TestBuilder.command`cat ${import.meta.filename} > ${val}`", + ); + memLeakTestProtect( + "ArrayBuffer_builtin", + "ArrayBuffer", + "new ArrayBuffer(64)", + "TestBuilder.command`echo ${import.meta.filename} > ${val}`", + ); + memLeakTestProtect( + "Buffer_builtin", + "Buffer", + "Buffer.alloc(64)", + "TestBuilder.command`echo ${import.meta.filename} > ${val}`", + ); + + memLeakTestProtect( + "Uint8Array", + "Uint8Array", + "new Uint8Array(64)", + "TestBuilder.command`cat ${import.meta.filename} > ${val}`", + ); + memLeakTestProtect( + "Uint8Array_builtin", + "Uint8Array", + "new Uint8Array(64)", + "TestBuilder.command`echo ${import.meta.filename} > ${val}`", + ); + + memLeakTestProtect( + "DataView", + "DataView", + "new DataView(new ArrayBuffer(64))", + "TestBuilder.command`cat ${import.meta.filename} > ${val}`", + ); + memLeakTestProtect( + "DataView_builtin", + "DataView", + "new DataView(new ArrayBuffer(64))", + "TestBuilder.command`echo ${import.meta.filename} > ${val}`", + ); + + memLeakTestProtect( + "String_large_input", + "String", + "Array(4096).fill('test').join('')", + "TestBuilder.command`echo ${val}`", + ); + memLeakTestProtect( + "String_pipeline", + "String", + "Array(1024).fill('data').join('')", + "TestBuilder.command`echo ${val} | cat`", + ); + + // Complex nested pipelines + memLeakTestProtect( + "ArrayBuffer_nested_pipeline", + "ArrayBuffer", + "new ArrayBuffer(256)", + "TestBuilder.command`echo ${val} | head -n 10 | tail -n 5 | wc -l`", + true, + ); + memLeakTestProtect( + "Buffer_triple_pipeline", + "Buffer", + "Buffer.alloc(256)", + "TestBuilder.command`echo ${val} | cat | grep -v nonexistent | wc -c`", + true, + ); + memLeakTestProtect( + "String_complex_pipeline", + "String", + "Array(512).fill('pipeline').join('\\n')", + "TestBuilder.command`echo ${val} | sort | uniq | head -n 3`", + true, + ); + + // Subshells with JS objects + memLeakTestProtect( + "ArrayBuffer_subshell", + "ArrayBuffer", + "new ArrayBuffer(128)", + "TestBuilder.command`echo $(echo ${val} | wc -c)`", + true, + ); + memLeakTestProtect( + "Buffer_nested_subshell", + "Buffer", + "Buffer.alloc(128)", + "TestBuilder.command`echo $(echo ${val} | head -c 10) done`", + true, + ); + memLeakTestProtect( + "String_subshell_pipeline", + "String", + "Array(256).fill('sub').join('')", + "TestBuilder.command`echo start $(echo ${val} | wc -c | cat) end`", + true, + ); + + // Mixed builtin and subprocess commands + memLeakTestProtect( + "ArrayBuffer_mixed_commands", + "ArrayBuffer", + "new ArrayBuffer(192)", + "TestBuilder.command`mkdir -p tmp && echo ${val} > tmp/test.txt && cat tmp/test.txt && rm -rf tmp`", + ); + memLeakTestProtect( + "Buffer_builtin_external_mix", + "Buffer", + "Buffer.alloc(192)", + "TestBuilder.command`echo ${val} | ${bunExe()} -e 'process.stdin.on(\"data\", d => process.stdout.write(d))' | head -c 50`", + ); + memLeakTestProtect( + "String_cd_operations", + "String", + "Array(128).fill('dir').join('')", + "TestBuilder.command`mkdir -p testdir && cd testdir && echo ${val} > file.txt && cd .. && cat testdir/file.txt && rm -rf testdir`", + ); + + // Conditional execution + memLeakTestProtect( + "ArrayBuffer_conditional", + "ArrayBuffer", + "new ArrayBuffer(64)", + "TestBuilder.command`echo ${val} && echo success || echo failure`", + ); + memLeakTestProtect( + "Buffer_test_conditional", + "Buffer", + "Buffer.alloc(64)", + "TestBuilder.command`test -n ${val} && echo 'has content' || echo 'empty'`", + true, + ); + describe("#11816", async () => { function doit(builtin: boolean) { test(builtin ? "builtin" : "external", async () => { diff --git a/test/js/bun/shell/yield.test.ts b/test/js/bun/shell/yield.test.ts new file mode 100644 index 0000000000..37f0fad75b --- /dev/null +++ b/test/js/bun/shell/yield.test.ts @@ -0,0 +1,11 @@ +import { describe } from "bun:test"; +import { createTestBuilder } from "./test_builder"; +const TestBuilder = createTestBuilder(import.meta.path); + +describe("yield", async () => { + const array = Array(10000).fill("a"); + TestBuilder.command`echo -n ${array} > myfile.txt` + .exitCode(0) + .fileEquals("myfile.txt", array.join(" ")) + .runAsTest("doesn't stackoverflow"); +}); diff --git a/test/js/bun/symbols.test.ts b/test/js/bun/symbols.test.ts index 72cc44b73a..621871b6d6 100644 --- a/test/js/bun/symbols.test.ts +++ b/test/js/bun/symbols.test.ts @@ -11,7 +11,7 @@ if (process.platform === "linux") { throw new Error("objdump executable not found. Please install it."); } - const output = await $`${objdump} -T ${BUN_EXE} | grep GLIBC_`.text(); + const output = await $`${objdump} -T ${BUN_EXE} | grep GLIBC_`.nothrow().text(); const lines = output.split("\n"); const errors = []; for (const line of lines) { diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 0714b5bca3..a97a604e7f 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -151,6 +151,9 @@ test/js/bun/s3/s3-insecure.test.ts test/js/bun/s3/s3-list-objects.test.ts test/js/bun/s3/s3-storage-class.test.ts test/js/bun/s3/s3.test.ts +test/js/bun/shell/yield.test.ts +test/js/bun/shell/file-io.test.ts +test/js/bun/shell/commands/echo.test.ts test/js/bun/shell/bunshell-default.test.ts test/js/bun/shell/bunshell-file.test.ts test/js/bun/shell/bunshell-instance.test.ts From 770c1c8327fb45f8f866c0226bdf33925bddee55 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sat, 21 Jun 2025 04:53:36 +1000 Subject: [PATCH 013/147] fix `test-child-process-fork-exec-argv.js` (#19639) Co-authored-by: 190n Co-authored-by: Dylan Conway --- src/bun.js/bindings/BunProcess.cpp | 17 +++++++ src/bun.js/bindings/headers.h | 1 + src/bun.js/node/node_process.zig | 9 ++++ src/js/node/child_process.ts | 24 ++++----- test/cli/run/run-eval.test.ts | 36 ++++++++++++++ .../test-child-process-fork-exec-argv.js | 49 +++++++++++++++++++ test/no-validate-exceptions.txt | 7 +-- 7 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 test/js/node/test/parallel/test-child-process-fork-exec-argv.js diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 5e313628ff..e979eed33e 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -2430,6 +2430,22 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessExecArgv, (JSGlobalObject * globalObject, Enc return true; } +JSC_DEFINE_CUSTOM_GETTER(processGetEval, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); + if (!process) { + return JSValue::encode(jsUndefined()); + } + + return Bun__Process__getEval(globalObject); +} + +JSC_DEFINE_CUSTOM_SETTER(setProcessGetEval, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName)) +{ + // dont allow setting eval from js + return true; +} + static JSValue constructBrowser(VM& vm, JSObject* processObject) { return jsBoolean(false); @@ -3722,6 +3738,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu @begin processObjectTable _debugEnd Process_stubEmptyFunction Function 0 _debugProcess Process_stubEmptyFunction Function 0 + _eval processGetEval CustomAccessor _fatalException Process_stubEmptyFunction Function 1 _getActiveHandles Process_stubFunctionReturningArray Function 0 _getActiveRequests Process_stubFunctionReturningArray Function 0 diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index cfbb1c7f81..c41dda57be 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -629,6 +629,7 @@ ZIG_DECL JSC::EncodedJSValue Bun__Process__getExecPath(JSC::JSGlobalObject* arg0 ZIG_DECL void Bun__Process__getTitle(JSC::JSGlobalObject* arg0, ZigString* arg1); ZIG_DECL JSC::EncodedJSValue Bun__Process__setCwd(JSC::JSGlobalObject* arg0, ZigString* arg1); ZIG_DECL JSC::EncodedJSValue Bun__Process__setTitle(JSC::JSGlobalObject* arg0, ZigString* arg1); +ZIG_DECL JSC::EncodedJSValue Bun__Process__getEval(JSC::JSGlobalObject* arg0); #endif CPP_DECL ZigException ZigException__fromException(JSC::Exception* arg0); diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index 7647189c26..f30e60cd23 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -9,6 +9,7 @@ comptime { @export(&createArgv0, .{ .name = "Bun__Process__createArgv0" }); @export(&getExecPath, .{ .name = "Bun__Process__getExecPath" }); @export(&createExecArgv, .{ .name = "Bun__Process__createExecArgv" }); + @export(&getEval, .{ .name = "Bun__Process__getEval" }); } var title_mutex = bun.Mutex{}; @@ -193,6 +194,14 @@ pub fn getExecArgv(global: *JSGlobalObject) callconv(.c) JSValue { return Bun__Process__getExecArgv(global); } +pub fn getEval(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + const vm = globalObject.bunVM(); + if (vm.module_loader.eval_source) |source| { + return JSC.ZigString.init(source.contents).toJS(globalObject); + } + return .js_undefined; +} + pub const getCwd = JSC.host_fn.wrap1(getCwd_); fn getCwd_(globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { var buf: bun.PathBuffer = undefined; diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index 7ea8e20ec0..458e3dae39 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -30,6 +30,8 @@ const ArrayPrototypeFilter = Array.prototype.filter; const ArrayPrototypeSort = Array.prototype.sort; const StringPrototypeToUpperCase = String.prototype.toUpperCase; const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeLastIndexOf = Array.prototype.lastIndexOf; +const ArrayPrototypeSplice = Array.prototype.splice; var ArrayBufferIsView = ArrayBuffer.isView; @@ -735,19 +737,19 @@ function fork(modulePath, args = [], options) { validateArgumentNullCheck(options.execPath, "options.execPath"); // Prepare arguments for fork: - // execArgv = options.execArgv || process.execArgv; - // validateArgumentsNullCheck(execArgv, "options.execArgv"); + let execArgv = options.execArgv || process.execArgv; + validateArgumentsNullCheck(execArgv, "options.execArgv"); - // if (execArgv === process.execArgv && process._eval != null) { - // const index = ArrayPrototypeLastIndexOf.$call(execArgv, process._eval); - // if (index > 0) { - // // Remove the -e switch to avoid fork bombing ourselves. - // execArgv = ArrayPrototypeSlice.$call(execArgv); - // ArrayPrototypeSplice.$call(execArgv, index - 1, 2); - // } - // } + if (execArgv === process.execArgv && process._eval != null) { + const index = ArrayPrototypeLastIndexOf.$call(execArgv, process._eval); + if (index > 0) { + // Remove the -e switch to avoid fork bombing ourselves. + execArgv = ArrayPrototypeSlice.$call(execArgv); + ArrayPrototypeSplice.$call(execArgv, index - 1, 2); + } + } - args = [/*...execArgv,*/ modulePath, ...args]; + args = [...execArgv, modulePath, ...args]; if (typeof options.stdio === "string") { options.stdio = stdioStringToArray(options.stdio, "ipc"); diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 6df01acc37..90219278ca 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -64,6 +64,15 @@ for (const flag of ["-e", "--print"]) { testProcessArgv(["--", "abc", "def"], [exe, "abc", "def"]); // testProcessArgv(["--", "abc", "--", "def"], [exe, "abc", "--", "def"]); }); + + test("process._eval", async () => { + const code = flag === "--print" ? "process._eval" : "console.log(process._eval)"; + const { stdout } = Bun.spawnSync({ + cmd: [bunExe(), flag, code], + env: bunEnv, + }); + expect(stdout.toString("utf8")).toEqual(code + "\n"); + }); }); } @@ -140,6 +149,18 @@ function group(run: (code: string) => SyncSubprocess<"pipe", "inherit">) { const exe = isWindows ? bunExe().replaceAll("/", "\\") : bunExe(); expect(JSON.parse(stdout.toString("utf8"))).toEqual([exe, "-"]); }); + + test("process._eval", async () => { + const code = "console.log(process._eval)"; + const { stdout } = run(code); + + // the file piping one on windows can include extra carriage returns + if (isWindows) { + expect(stdout.toString("utf8")).toInclude(code); + } else { + expect(stdout.toString("utf8")).toEqual(code + "\n"); + } + }); } describe("bun run - < file-path.js", () => { @@ -196,3 +217,18 @@ describe("echo | bun run -", () => { group(run); }); + +test("process._eval (undefined for normal run)", async () => { + const cwd = tmpdirSync(); + const file = join(cwd, "test.js"); + writeFileSync(file, "console.log(typeof process._eval)"); + + const { stdout } = Bun.spawnSync({ + cmd: [bunExe(), "run", file], + cwd: cwd, + env: bunEnv, + }); + expect(stdout.toString("utf8")).toEqual("undefined\n"); + + rmSync(cwd, { recursive: true, force: true }); +}); diff --git a/test/js/node/test/parallel/test-child-process-fork-exec-argv.js b/test/js/node/test/parallel/test-child-process-fork-exec-argv.js new file mode 100644 index 0000000000..f1e3bbbc58 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-exec-argv.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); +const spawn = child_process.spawn; +const fork = child_process.fork; + +if (process.argv[2] === 'fork') { + process.stdout.write(JSON.stringify(process.execArgv), function() { + process.exit(); + }); +} else if (process.argv[2] === 'child') { + fork(__filename, ['fork']); +} else { + const execArgv = ['--stack-size=256']; + const args = [__filename, 'child', 'arg0']; + + const child = spawn(process.execPath, execArgv.concat(args)); + let out = ''; + + child.stdout.on('data', function(chunk) { + out += chunk; + }); + + child.on('exit', common.mustCall(function() { + assert.deepStrictEqual(JSON.parse(out), execArgv); + })); +} diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index a97a604e7f..b313ae40dc 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -151,14 +151,12 @@ test/js/bun/s3/s3-insecure.test.ts test/js/bun/s3/s3-list-objects.test.ts test/js/bun/s3/s3-storage-class.test.ts test/js/bun/s3/s3.test.ts -test/js/bun/shell/yield.test.ts -test/js/bun/shell/file-io.test.ts -test/js/bun/shell/commands/echo.test.ts test/js/bun/shell/bunshell-default.test.ts test/js/bun/shell/bunshell-file.test.ts test/js/bun/shell/bunshell-instance.test.ts test/js/bun/shell/commands/basename.test.ts test/js/bun/shell/commands/dirname.test.ts +test/js/bun/shell/commands/echo.test.ts test/js/bun/shell/commands/exit.test.ts test/js/bun/shell/commands/false.test.ts test/js/bun/shell/commands/mv.test.ts @@ -169,6 +167,7 @@ test/js/bun/shell/commands/which.test.ts test/js/bun/shell/commands/yes.test.ts test/js/bun/shell/env.positionals.test.ts test/js/bun/shell/exec.test.ts +test/js/bun/shell/file-io.test.ts test/js/bun/shell/lazy.test.ts test/js/bun/shell/leak.test.ts test/js/bun/shell/lex.test.ts @@ -176,6 +175,7 @@ test/js/bun/shell/shell-hang.test.ts test/js/bun/shell/shell-load.test.ts test/js/bun/shell/shelloutput.test.ts test/js/bun/shell/throw.test.ts +test/js/bun/shell/yield.test.ts test/js/bun/spawn/bun-ipc-inherit.test.ts test/js/bun/spawn/job-object-bug.test.ts test/js/bun/spawn/spawn_waiter_thread.test.ts @@ -477,6 +477,7 @@ test/js/node/test/parallel/test-child-process-fork-args.js test/js/node/test/parallel/test-child-process-fork-close.js test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js test/js/node/test/parallel/test-child-process-fork-detached.js +test/js/node/test/parallel/test-child-process-fork-exec-argv.js test/js/node/test/parallel/test-child-process-fork-exec-path.js test/js/node/test/parallel/test-child-process-fork-no-shell.js test/js/node/test/parallel/test-child-process-fork-ref.js From fd5e7776396951446c0679aaa53e82d06bb612b8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Jun 2025 16:16:11 -0700 Subject: [PATCH 014/147] Remove outdated doc line --- docs/bundler/executables.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index b30057caa9..2cdad23f40 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -353,5 +353,4 @@ Currently, the `--compile` flag can only accept a single entrypoint at a time an - `--splitting` - `--public-path` - `--target=node` or `--target=browser` -- `--format` - always outputs a binary executable. Internally, it's almost esm. - `--no-bundle` - we always bundle everything into the executable. From 633f4f593db93bc751b6a6703c1507daaad2ad8c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 20 Jun 2025 19:55:52 -0700 Subject: [PATCH 015/147] Ahead-of-time frontend bundling support for HTML imports & bun build (#20511) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Dylan Conway --- src/StandaloneModuleGraph.zig | 24 +- src/bun.js/api/BunObject.zig | 2 +- src/bun.js/api/server.zig | 124 +++++- src/bun.js/api/server/FileRoute.zig | 5 +- src/bun.js/api/server/ServerConfig.zig | 9 +- src/bun.js/api/server/StaticRoute.zig | 3 +- src/bun.js/webcore/Blob.zig | 1 + src/bundler/Chunk.zig | 1 + src/bundler/HTMLImportManifest.zig | 44 ++- src/bundler/LinkerContext.zig | 9 +- src/bundler/bundle_v2.zig | 38 +- src/bundler/linker_context/computeChunks.zig | 32 +- .../generateChunksInParallel.zig | 17 +- .../linker_context/writeOutputFilesToDisk.zig | 12 +- src/cli/build_command.zig | 22 +- src/sys.zig | 8 +- test/bundler/bundler_html_server.test.ts | 121 ++++++ test/internal/ban-words.test.ts | 2 +- .../bun/http/bun-serve-html-manifest.test.ts | 372 ++++++++++++++++++ test/no-validate-exceptions.txt | 2 + 20 files changed, 789 insertions(+), 59 deletions(-) create mode 100644 test/bundler/bundler_html_server.test.ts create mode 100644 test/js/bun/http/bun-serve-html-manifest.test.ts diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 267d1f83f1..eee598ea68 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -97,6 +97,12 @@ pub const StandaloneModuleGraph = struct { encoding: Encoding = .latin1, loader: bun.options.Loader = .file, module_format: ModuleFormat = .none, + side: FileSide = .server, + }; + + pub const FileSide = enum(u8) { + server = 0, + client = 1, }; pub const Encoding = enum(u8) { @@ -141,6 +147,11 @@ pub const StandaloneModuleGraph = struct { wtf_string: bun.String = bun.String.empty, bytecode: []u8 = "", module_format: ModuleFormat = .none, + side: FileSide = .server, + + pub fn appearsInEmbeddedFilesArray(this: *const File) bool { + return this.side == .client or !this.loader.isJavaScriptLike(); + } pub fn stat(this: *const File) bun.Stat { var result = std.mem.zeroes(bun.Stat); @@ -300,6 +311,7 @@ pub const StandaloneModuleGraph = struct { .none, .bytecode = if (module.bytecode.length > 0) @constCast(sliceTo(raw_bytes, module.bytecode)) else &.{}, .module_format = module.module_format, + .side = module.side, }, ); } @@ -347,8 +359,10 @@ 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.output_kind == .@"entry-point") { - entry_point_id = module_count; + if (output_file.side == null or output_file.side.? == .server) { + if (output_file.output_kind == .@"entry-point") { + entry_point_id = module_count; + } } } @@ -421,6 +435,10 @@ pub const StandaloneModuleGraph = struct { else => .none, } else .none, .bytecode = bytecode, + .side = switch (output_file.side orelse .server) { + .server => .server, + .client => .client, + }, }; if (output_file.source_map_index != std.math.maxInt(u32)) { @@ -839,7 +857,7 @@ pub const StandaloneModuleGraph = struct { .fromStdDir(root_dir), bun.sliceTo(&(try std.posix.toPosixPath(std.fs.path.basename(outfile))), 0), ) catch |err| { - if (err == error.IsDir) { + if (err == error.IsDir or err == error.EISDIR) { Output.prettyErrorln("error: {} is a directory. Please choose a different --outfile or delete the directory", .{bun.fmt.quote(outfile)}); } else { Output.prettyErrorln("error: failed to rename {s} to {s}: {s}", .{ temp_location, outfile, @errorName(err) }); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a84db98bc3..7263c2ccec 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1315,7 +1315,7 @@ pub fn getEmbeddedFiles(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) bun.J // We don't really do that right now, but exposing the output source // code here as an easily accessible Blob is even worse for them. // So let's omit any source code files from the list. - if (unsorted_files[index].loader.isJavaScriptLike()) continue; + if (!unsorted_files[index].appearsInEmbeddedFilesArray()) continue; sort_indices.appendAssumeCapacity(@intCast(index)); } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 50b3bbbaf0..1d755a01b0 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -118,7 +118,121 @@ pub const AnyRoute = union(enum) { } } - pub fn htmlRouteFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext) ?AnyRoute { + fn bundledHTMLManifestItemFromJS(argument: JSC.JSValue, index_path: []const u8, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute { + if (!argument.isObject()) return null; + + const path_string = try bun.String.fromJS(try argument.get(init_ctx.global, "path") orelse return null, init_ctx.global); + defer path_string.deref(); + var path = JSC.Node.PathOrFileDescriptor{ .path = try JSC.Node.PathLike.fromBunString(init_ctx.global, path_string, false, bun.default_allocator) }; + defer path.deinit(); + + // Construct the route by stripping paths above the root. + // + // "./index-abc.js" -> "/index-abc.js" + // "../index-abc.js" -> "/index-abc.js" + // "/index-abc.js" -> "/index-abc.js" + // "index-abc.js" -> "/index-abc.js" + // + const cwd = if (bun.StandaloneModuleGraph.isBunStandaloneFilePath(path.path.slice())) + bun.StandaloneModuleGraph.targetBasePublicPath(bun.Environment.os, "root/") + else + bun.fs.FileSystem.instance.top_level_dir; + + const abs_path = bun.fs.FileSystem.instance.abs(&[_][]const u8{path.path.slice()}); + var relative_path = bun.fs.FileSystem.instance.relative(cwd, abs_path); + + if (strings.hasPrefixComptime(relative_path, "./")) { + relative_path = relative_path[2..]; + } else if (strings.hasPrefixComptime(relative_path, "../")) { + while (strings.hasPrefixComptime(relative_path, "../")) { + relative_path = relative_path[3..]; + } + } + const is_index_route = bun.strings.eql(path.path.slice(), index_path); + var builder = std.ArrayList(u8).init(bun.default_allocator); + defer builder.deinit(); + if (!strings.hasPrefixComptime(relative_path, "/")) { + try builder.append('/'); + } + + try builder.appendSlice(relative_path); + + const fetch_headers = JSC.WebCore.FetchHeaders.createFromJS(init_ctx.global, try argument.get(init_ctx.global, "headers") orelse return null); + defer if (fetch_headers) |headers| headers.deref(); + if (init_ctx.global.hasException()) return error.JSError; + + const route = try fromOptions(init_ctx.global, fetch_headers, &path); + + if (is_index_route) { + return route; + } + + var methods = HTTP.Method.Optional{ .method = .initEmpty() }; + methods.insert(.GET); + methods.insert(.HEAD); + + try init_ctx.user_routes.append(.{ + .path = try builder.toOwnedSlice(), + .route = route, + .method = methods, + }); + return null; + } + + /// This is the JS representation of an HTMLImportManifest + /// + /// See ./src/bundler/HTMLImportManifest.zig + fn bundledHTMLManifestFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute { + if (!argument.isObject()) return null; + + const index = try argument.getOptional(init_ctx.global, "index", ZigString.Slice) orelse return null; + defer index.deinit(); + + const files = try argument.getArray(init_ctx.global, "files") orelse return null; + var iter = try files.arrayIterator(init_ctx.global); + var html_route: ?AnyRoute = null; + while (try iter.next()) |file_entry| { + if (try bundledHTMLManifestItemFromJS(file_entry, index.slice(), init_ctx)) |item| { + html_route = item; + } + } + + return html_route; + } + + pub fn fromOptions(global: *JSC.JSGlobalObject, headers: ?*JSC.WebCore.FetchHeaders, path: *JSC.Node.PathOrFileDescriptor) !AnyRoute { + // The file/static route doesn't ref it. + var blob = Blob.findOrCreateFileFromPath(path, global, false); + + if (blob.needsToReadFile()) { + // Throw a more helpful error upfront if the file does not exist. + // + // In production, you do NOT want to find out that all the assets + // are 404'ing when the user goes to the route. You want to find + // that out immediately so that the health check on startup fails + // and the process exits with a non-zero status code. + if (blob.store) |store| { + if (store.getPath()) |store_path| { + switch (bun.sys.existsAtType(bun.FD.cwd(), store_path)) { + .result => |file_type| { + if (file_type == .directory) { + return global.throwInvalidArguments("Bundled file {} cannot be a directory. You may want to configure --asset-naming or `naming` when bundling.", .{bun.fmt.quote(store_path)}); + } + }, + .err => { + return global.throwInvalidArguments("Bundled file {} not found. You may want to configure --asset-naming or `naming` when bundling.", .{bun.fmt.quote(store_path)}); + }, + } + } + } + + return AnyRoute{ .file = FileRoute.initFromBlob(blob, .{ .server = null, .headers = headers }) }; + } + + return AnyRoute{ .static = StaticRoute.initFromAnyBlob(&.{ .Blob = blob }, .{ .server = null, .headers = headers }) }; + } + + pub fn htmlRouteFromJS(argument: JSC.JSValue, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute { if (argument.as(HTMLBundle)) |html_bundle| { const entry = init_ctx.dedupe_html_bundle_map.getOrPut(html_bundle) catch bun.outOfMemory(); if (!entry.found_existing) { @@ -129,6 +243,10 @@ pub const AnyRoute = union(enum) { } } + if (try bundledHTMLManifestFromJS(argument, init_ctx)) |html_route| { + return html_route; + } + return null; } @@ -136,7 +254,9 @@ pub const AnyRoute = union(enum) { arena: std.heap.ArenaAllocator, dedupe_html_bundle_map: std.AutoHashMap(*HTMLBundle, bun.ptr.RefPtr(HTMLBundle.Route)), js_string_allocations: bun.bake.StringRefList, + global: *JSC.JSGlobalObject, framework_router_list: std.ArrayList(bun.bake.Framework.FileSystemRouterType), + user_routes: *std.ArrayList(ServerConfig.StaticRouteEntry), }; pub fn fromJS( @@ -145,7 +265,7 @@ pub const AnyRoute = union(enum) { argument: JSC.JSValue, init_ctx: *ServerInitContext, ) bun.JSError!?AnyRoute { - if (AnyRoute.htmlRouteFromJS(argument, init_ctx)) |html_route| { + if (try AnyRoute.htmlRouteFromJS(argument, init_ctx)) |html_route| { return html_route; } diff --git a/src/bun.js/api/server/FileRoute.zig b/src/bun.js/api/server/FileRoute.zig index 2dbdca9a88..d5c1e55369 100644 --- a/src/bun.js/api/server/FileRoute.zig +++ b/src/bun.js/api/server/FileRoute.zig @@ -12,6 +12,7 @@ has_content_length_header: bool, pub const InitOptions = struct { server: ?AnyServer, status_code: u16 = 200, + headers: ?*JSC.WebCore.FetchHeaders = null, }; pub fn lastModifiedDate(this: *const FileRoute) ?u64 { @@ -34,12 +35,14 @@ pub fn lastModifiedDate(this: *const FileRoute) ?u64 { } pub fn initFromBlob(blob: Blob, opts: InitOptions) *FileRoute { - const headers = Headers.from(null, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); + const headers = Headers.from(opts.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } }) catch bun.outOfMemory(); return bun.new(FileRoute, .{ .ref_count = .init(), .server = opts.server, .blob = blob, .headers = headers, + .has_last_modified_header = headers.get("last-modified") != null, + .has_content_length_header = headers.get("content-length") != null, .status_code = opts.status_code, }); } diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 964b08b3c9..a46bead0b1 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -506,12 +506,15 @@ pub fn fromJS( }).init(global, static_obj); defer iter.deinit(); - var init_ctx: AnyRoute.ServerInitContext = .{ + var init_ctx_: AnyRoute.ServerInitContext = .{ .arena = .init(bun.default_allocator), .dedupe_html_bundle_map = .init(bun.default_allocator), .framework_router_list = .init(bun.default_allocator), .js_string_allocations = .empty, + .user_routes = &args.static_routes, + .global = global, }; + const init_ctx: *AnyRoute.ServerInitContext = &init_ctx_; errdefer { init_ctx.arena.deinit(); init_ctx.framework_router_list.deinit(); @@ -593,7 +596,7 @@ pub fn fromJS( }, .callback = .create(function.withAsyncContextIfNeeded(global), global), }) catch bun.outOfMemory(); - } else if (try AnyRoute.fromJS(global, path, function, &init_ctx)) |html_route| { + } else if (try AnyRoute.fromJS(global, path, function, init_ctx)) |html_route| { var method_set = bun.http.Method.Set.initEmpty(); method_set.insert(method); @@ -612,7 +615,7 @@ pub fn fromJS( } } - const route = try AnyRoute.fromJS(global, path, value, &init_ctx) orelse { + const route = try AnyRoute.fromJS(global, path, value, init_ctx) orelse { return global.throwInvalidArguments( \\'routes' expects a Record Response|Promise}> \\ diff --git a/src/bun.js/api/server/StaticRoute.zig b/src/bun.js/api/server/StaticRoute.zig index f1874d916f..616bb2bfec 100644 --- a/src/bun.js/api/server/StaticRoute.zig +++ b/src/bun.js/api/server/StaticRoute.zig @@ -22,11 +22,12 @@ pub const InitFromBytesOptions = struct { server: ?AnyServer, mime_type: ?*const bun.http.MimeType = null, status_code: u16 = 200, + headers: ?*JSC.WebCore.FetchHeaders = null, }; /// Ownership of `blob` is transferred to this function. pub fn initFromAnyBlob(blob: *const AnyBlob, options: InitFromBytesOptions) *StaticRoute { - var headers = Headers.from(null, bun.default_allocator, .{ .body = blob }) catch bun.outOfMemory(); + var headers = Headers.from(options.headers, bun.default_allocator, .{ .body = blob }) catch bun.outOfMemory(); if (options.mime_type) |mime_type| { if (headers.getContentType() == null) { headers.append("Content-Type", mime_type.value) catch bun.outOfMemory(); diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 22995f2f3e..aa9e30105a 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -1899,6 +1899,7 @@ pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, glob } } } + const path: JSC.Node.PathOrFileDescriptor = brk: { switch (path_or_fd.*) { .path => { diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig index f9aace07f7..fa58ce93b5 100644 --- a/src/bundler/Chunk.zig +++ b/src/bundler/Chunk.zig @@ -27,6 +27,7 @@ pub const Chunk = struct { is_executable: bool = false, has_html_chunk: bool = false, + is_browser_chunk_from_server_build: bool = false, output_source_map: sourcemap.SourceMapPieces, diff --git a/src/bundler/HTMLImportManifest.zig b/src/bundler/HTMLImportManifest.zig index 58a199cdea..a187d1f5d9 100644 --- a/src/bundler/HTMLImportManifest.zig +++ b/src/bundler/HTMLImportManifest.zig @@ -128,13 +128,26 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, try writer.writeAll("{"); + const inject_compiler_filesystem_prefix = bv2.transpiler.options.compile; + // Use the server-side public path here. + const public_path = bv2.transpiler.options.public_path; + var temp_buffer = std.ArrayList(u8).init(bun.default_allocator); + defer temp_buffer.deinit(); + for (chunks) |*ch| { if (ch.entry_point.source_index == browser_source_index and ch.entry_point.is_entry_point) { entry_point_bits.set(ch.entry_point.entry_point_id); if (ch.content == .html) { try writer.writeAll("\"index\":"); - try bun.js_printer.writeJSONString(ch.final_rel_path, @TypeOf(writer), writer, .utf8); + if (inject_compiler_filesystem_prefix) { + temp_buffer.clearRetainingCapacity(); + try temp_buffer.appendSlice(public_path); + try temp_buffer.appendSlice(bun.strings.removeLeadingDotSlash(ch.final_rel_path)); + try bun.js_printer.writeJSONString(temp_buffer.items, @TypeOf(writer), writer, .utf8); + } else { + try bun.js_printer.writeJSONString(ch.final_rel_path, @TypeOf(writer), writer, .utf8); + } try writer.writeAll(","); } } @@ -167,13 +180,20 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, .posix, false, ); - if (path_for_key.len > 2 and strings.eqlComptime(path_for_key[0..2], "./")) { - path_for_key = path_for_key[2..]; - } + + path_for_key = bun.strings.removeLeadingDotSlash(path_for_key); break :brk path_for_key; }, - ch.final_rel_path, + brk: { + if (inject_compiler_filesystem_prefix) { + temp_buffer.clearRetainingCapacity(); + try temp_buffer.appendSlice(public_path); + try temp_buffer.appendSlice(bun.strings.removeLeadingDotSlash(ch.final_rel_path)); + break :brk temp_buffer.items; + } + break :brk ch.final_rel_path; + }, ch.isolated_hash, ch.content.loader(), if (ch.entry_point.is_entry_point) @@ -203,14 +223,20 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, .posix, false, ); - if (path_for_key.len > 2 and strings.eqlComptime(path_for_key[0..2], "./")) { - path_for_key = path_for_key[2..]; - } + path_for_key = bun.strings.removeLeadingDotSlash(path_for_key); try writeEntryItem( writer, path_for_key, - output_file.dest_path, + brk: { + if (inject_compiler_filesystem_prefix) { + temp_buffer.clearRetainingCapacity(); + try temp_buffer.appendSlice(public_path); + try temp_buffer.appendSlice(bun.strings.removeLeadingDotSlash(output_file.dest_path)); + break :brk temp_buffer.items; + } + break :brk output_file.dest_path; + }, output_file.hash, output_file.loader, output_file.output_kind, diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 0a80ad2677..3389628624 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -841,13 +841,18 @@ pub const LinkerContext = struct { // any import to be considered different if the import's output path has changed. hasher.write(chunk.template.data); + const public_path = if (chunk.is_browser_chunk_from_server_build) + @as(*bundler.BundleV2, @fieldParentPtr("linker", c)).transpilerForTarget(.browser).options.public_path + else + c.options.public_path; + // Also hash the public path. If provided, this is used whenever files // reference each other such as cross-chunk imports, asset file references, // and source map comments. We always include the hash in all chunks instead // of trying to figure out which chunks will include the public path for // simplicity and for robustness to code changes in the future. - if (c.options.public_path.len > 0) { - hasher.write(c.options.public_path); + if (public_path.len > 0) { + hasher.write(public_path); } // Include the generated output content in the hash. This excludes the diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 9c7c076fdc..b38b65b9c6 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -179,27 +179,23 @@ pub const BundleV2 = struct { const this_transpiler = this.transpiler; const client_transpiler = try allocator.create(Transpiler); - const defines = this_transpiler.options.transform_options.define; client_transpiler.* = this_transpiler.*; client_transpiler.options = this_transpiler.options; client_transpiler.options.target = .browser; client_transpiler.options.main_fields = options.Target.DefaultMainFields.get(options.Target.browser); client_transpiler.options.conditions = try options.ESMConditions.init(allocator, options.Target.browser.defaultConditions()); - client_transpiler.options.define = try options.Define.init( - allocator, - if (defines) |user_defines| - try options.Define.Data.fromInput(try options.stringHashMapFromArrays( - options.defines.RawDefines, - allocator, - user_defines.keys, - user_defines.values, - ), this_transpiler.options.transform_options.drop, this_transpiler.log, allocator) - else - null, - null, - this_transpiler.options.define.drop_debugger, - ); + + // We need to make sure it has [hash] in the names so we don't get conflicts. + if (this_transpiler.options.compile) { + client_transpiler.options.asset_naming = bun.options.PathTemplate.asset.data; + client_transpiler.options.chunk_naming = bun.options.PathTemplate.chunk.data; + client_transpiler.options.entry_naming = "./[name]-[hash].[ext]"; + + // Avoid setting a public path for --compile since all the assets + // will be served relative to the server root. + client_transpiler.options.public_path = ""; + } client_transpiler.setLog(this_transpiler.log); client_transpiler.setAllocator(allocator); @@ -207,6 +203,8 @@ pub const BundleV2 = struct { client_transpiler.macro_context = js_ast.Macro.MacroContext.init(client_transpiler); const CacheSet = @import("../cache.zig"); client_transpiler.resolver.caches = CacheSet.Set.init(allocator); + + try client_transpiler.configureDefines(); client_transpiler.resolver.opts = client_transpiler.options; this.client_transpiler = client_transpiler; @@ -1525,8 +1523,12 @@ pub const BundleV2 = struct { else PathTemplate.asset; - if (this.transpiler.options.asset_naming.len > 0) - template.data = this.transpiler.options.asset_naming; + const target = targets[index]; + const asset_naming = this.transpilerForTarget(target).options.asset_naming; + if (asset_naming.len > 0) { + template.data = asset_naming; + } + const source = &sources[index]; const output_path = brk: { @@ -1546,7 +1548,7 @@ pub const BundleV2 = struct { } if (template.needs(.target)) { - template.placeholder.target = @tagName(targets[index]); + template.placeholder.target = @tagName(target); } break :brk std.fmt.allocPrint(bun.default_allocator, "{}", .{template}) catch bun.outOfMemory(); }; diff --git a/src/bundler/linker_context/computeChunks.zig b/src/bundler/linker_context/computeChunks.zig index 2bd7b79d2f..66a0be8720 100644 --- a/src/bundler/linker_context/computeChunks.zig +++ b/src/bundler/linker_context/computeChunks.zig @@ -25,8 +25,11 @@ pub noinline fn computeChunks( const css_chunking = this.options.css_chunking; var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator); const loaders = this.parse_graph.input_files.items(.loader); + const ast_targets = this.graph.ast.items(.target); const code_splitting = this.graph.code_splitting; + const could_be_browser_target_from_server_build = this.options.target.isServerSide() and this.parse_graph.html_imports.html_source_indices.len > 0; + const has_server_html_imports = this.parse_graph.html_imports.server_source_indices.len > 0; // Create chunks for entry points for (entry_source_indices, 0..) |source_index, entry_id_| { @@ -61,6 +64,7 @@ pub noinline fn computeChunks( .entry_bits = entry_bits.*, .content = .html, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; } } @@ -95,6 +99,7 @@ pub noinline fn computeChunks( }, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), .has_html_chunk = has_html_chunk, + .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; } @@ -116,6 +121,7 @@ pub noinline fn computeChunks( }, .has_html_chunk = has_html_chunk, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; { @@ -129,7 +135,8 @@ pub noinline fn computeChunks( if (css_source_indices.len > 0) { const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice()); - const hash_to_use = if (!css_chunking) + const use_content_based_key = css_chunking or has_server_html_imports; + const hash_to_use = if (!use_content_based_key) bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len))) else brk: { var hasher = std.hash.Wyhash.init(5); @@ -168,6 +175,7 @@ pub noinline fn computeChunks( .files_with_parts_in_chunk = css_files_with_parts_in_chunk, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), .has_html_chunk = has_html_chunk, + .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; } } @@ -200,6 +208,7 @@ pub noinline fn computeChunks( var js_chunk_entry = try js_chunks.getOrPut(js_chunk_key); if (!js_chunk_entry.found_existing) { + const is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index.get()] == .browser; js_chunk_entry.value_ptr.* = .{ .entry_bits = entry_bits.*, .entry_point = .{ @@ -209,6 +218,7 @@ pub noinline fn computeChunks( .javascript = .{}, }, .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), + .is_browser_chunk_from_server_build = is_browser_chunk_from_server_build, }; } @@ -305,6 +315,7 @@ pub noinline fn computeChunks( const kinds = this.graph.files.items(.entry_point_kind); const output_paths = this.graph.entry_points.items(.output_path); + const bv2: *bundler.BundleV2 = @fieldParentPtr("linker", this); for (chunks, 0..) |*chunk, chunk_id| { // Assign a unique key to each chunk. This key encodes the index directly so // we can easily recover it later without needing to look it up in a map. The @@ -317,21 +328,27 @@ pub noinline fn computeChunks( (chunk.content == .html or (kinds[chunk.entry_point.source_index] == .user_specified and !chunk.has_html_chunk))) { // Use fileWithTarget template if there are HTML imports and user hasn't manually set naming - if (this.parse_graph.html_imports.server_source_indices.len > 0 and this.resolver.opts.entry_naming.len == 0) { + if (has_server_html_imports and bv2.transpiler.options.entry_naming.len == 0) { chunk.template = PathTemplate.fileWithTarget; } else { chunk.template = PathTemplate.file; - if (this.resolver.opts.entry_naming.len > 0) - chunk.template.data = this.resolver.opts.entry_naming; + if (chunk.is_browser_chunk_from_server_build) { + chunk.template.data = bv2.transpilerForTarget(.browser).options.entry_naming; + } else { + chunk.template.data = bv2.transpiler.options.entry_naming; + } } } else { - if (this.parse_graph.html_imports.server_source_indices.len > 0 and this.resolver.opts.chunk_naming.len == 0) { + if (has_server_html_imports and bv2.transpiler.options.chunk_naming.len == 0) { chunk.template = PathTemplate.chunkWithTarget; } else { chunk.template = PathTemplate.chunk; + if (chunk.is_browser_chunk_from_server_build) { + chunk.template.data = bv2.transpilerForTarget(.browser).options.chunk_naming; + } else { + chunk.template.data = bv2.transpiler.options.chunk_naming; + } } - if (this.resolver.opts.chunk_naming.len > 0) - chunk.template.data = this.resolver.opts.chunk_naming; } const pathname = Fs.PathName.init(output_paths[chunk.entry_point.entry_point_id].slice()); @@ -340,7 +357,6 @@ pub noinline fn computeChunks( if (chunk.template.needs(.target)) { // Determine the target from the AST of the entry point source - const ast_targets = this.graph.ast.items(.target); const chunk_target = ast_targets[chunk.entry_point.source_index]; chunk.template.placeholder.target = switch (chunk_target) { .browser => "browser", diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 23a5ba8210..e09ce74aea 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -340,6 +340,8 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ return error.MultipleOutputFilesWithoutOutputDir; } + const bundler = @as(*bun.bundle_v2.BundleV2, @fieldParentPtr("linker", c)); + if (root_path.len > 0) { try c.writeOutputFilesToDisk(root_path, chunks, &output_files); } else { @@ -347,11 +349,16 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ for (chunks) |*chunk| { var display_size: usize = 0; + const public_path = if (chunk.is_browser_chunk_from_server_build) + bundler.transpilerForTarget(.browser).options.public_path + else + c.options.public_path; + const _code_result = chunk.intermediate_output.code( null, c.parse_graph, &c.graph, - c.resolver.opts.public_path, + public_path, chunk, chunks, &display_size, @@ -376,8 +383,8 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ bun.copy(u8, source_map_final_rel_path[chunk.final_rel_path.len..], ".map"); if (tag == .linked) { - const a, const b = if (c.options.public_path.len > 0) - cheapPrefixNormalizer(c.options.public_path, source_map_final_rel_path) + const a, const b = if (public_path.len > 0) + cheapPrefixNormalizer(public_path, source_map_final_rel_path) else .{ "", std.fs.path.basename(source_map_final_rel_path) }; @@ -471,7 +478,7 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ .data = .{ .buffer = .{ .data = bytecode, .allocator = cached_bytecode.allocator() }, }, - .side = null, + .side = .server, .entry_point_index = null, .is_executable = false, }); @@ -522,7 +529,7 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ .is_executable = chunk.is_executable, .source_map_index = source_map_index, .bytecode_index = bytecode_index, - .side = if (chunk.content == .css) + .side = if (chunk.content == .css or chunk.is_browser_chunk_from_server_build) .client else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { .browser => .client, diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index 745649726b..d3f5622f5c 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -39,6 +39,7 @@ pub fn writeOutputFilesToDisk( const code_with_inline_source_map_allocator = max_heap_allocator_inline_source_map.init(bun.default_allocator); var pathbuf: bun.PathBuffer = undefined; + const bv2: *bundler.BundleV2 = @fieldParentPtr("linker", c); for (chunks) |*chunk| { const trace2 = bun.perf.trace("Bundler.writeChunkToDisk"); @@ -59,11 +60,16 @@ pub fn writeOutputFilesToDisk( } } var display_size: usize = 0; + const public_path = if (chunk.is_browser_chunk_from_server_build) + bv2.transpilerForTarget(.browser).options.public_path + else + c.resolver.opts.public_path; + var code_result = chunk.intermediate_output.code( code_allocator, c.parse_graph, &c.graph, - c.resolver.opts.public_path, + public_path, chunk, chunks, &display_size, @@ -89,8 +95,8 @@ pub fn writeOutputFilesToDisk( }) catch @panic("Failed to allocate memory for external source map path"); if (tag == .linked) { - const a, const b = if (c.options.public_path.len > 0) - cheapPrefixNormalizer(c.options.public_path, source_map_final_rel_path) + const a, const b = if (public_path.len > 0) + cheapPrefixNormalizer(public_path, source_map_final_rel_path) else .{ "", std.fs.path.basename(source_map_final_rel_path) }; diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 225f50af0a..2a42e8898b 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -113,6 +113,7 @@ pub const BuildCommand = struct { } this_transpiler.options.bytecode = ctx.bundler_options.bytecode; + var was_renamed_from_index = false; if (ctx.bundler_options.compile) { if (ctx.bundler_options.code_splitting) { @@ -140,6 +141,7 @@ pub const BuildCommand = struct { if (strings.eqlComptime(outfile, "index")) { outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "index"); + was_renamed_from_index = !strings.eqlComptime(outfile, "index"); } if (strings.eqlComptime(outfile, "bun")) { @@ -353,7 +355,20 @@ pub const BuildCommand = struct { if (output_dir.len == 0 and outfile.len > 0 and will_be_one_file) { output_dir = std.fs.path.dirname(outfile) orelse "."; - output_files[0].dest_path = std.fs.path.basename(outfile); + if (ctx.bundler_options.compile) { + // If the first output file happens to be a client-side chunk imported server-side + // then don't rename it to something else, since an HTML + // import manifest might depend on the file path being the + // one we think it should be. + for (output_files) |*f| { + if (f.output_kind == .@"entry-point" and (f.side orelse .server) == .server) { + f.dest_path = std.fs.path.basename(outfile); + break; + } + } + } else { + output_files[0].dest_path = std.fs.path.basename(outfile); + } } if (!ctx.bundler_options.compile) { @@ -416,6 +431,11 @@ pub const BuildCommand = struct { if (compile_target.os == .windows and !strings.hasSuffixComptime(outfile, ".exe")) { outfile = try std.fmt.allocPrint(allocator, "{s}.exe", .{outfile}); + } else if (was_renamed_from_index and !bun.strings.eqlComptime(outfile, "index")) { + // If we're going to fail due to EISDIR, we should instead pick a different name. + if (bun.sys.directoryExistsAt(bun.FD.fromStdDir(root_dir), outfile).asValue() orelse false) { + outfile = "index"; + } } try bun.StandaloneModuleGraph.toExecutable( diff --git a/src/sys.zig b/src/sys.zig index 19eb84d83d..6d8e1e71c4 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -3434,11 +3434,17 @@ pub fn existsAtType(fd: bun.FileDescriptor, subpath: anytype) Maybe(ExistsAtType if (comptime Environment.isWindows) { const wbuf = bun.WPathBufferPool.get(); defer bun.WPathBufferPool.put(wbuf); - const path = if (std.meta.Child(@TypeOf(subpath)) == u16) + var path = if (std.meta.Child(@TypeOf(subpath)) == u16) bun.strings.toNTPath16(wbuf, subpath) else bun.strings.toNTPath(wbuf, subpath); + // trim leading .\ + // NtQueryAttributesFile expects relative paths to not start with .\ + if (path.len > 2 and path[0] == '.' and path[1] == '\\') { + path = path[2..]; + } + const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, diff --git a/test/bundler/bundler_html_server.test.ts b/test/bundler/bundler_html_server.test.ts new file mode 100644 index 0000000000..122709df27 --- /dev/null +++ b/test/bundler/bundler_html_server.test.ts @@ -0,0 +1,121 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + itBundled("compile/HTMLServerBasic", { + compile: true, + files: { + "/entry.ts": /* js */ ` + import index from "./index.html"; + + using server = Bun.serve({ + port: 0, + routes: { + "/": index, + }, + }); + + const res = await fetch(server.url); + console.log("Status:", res.status); + console.log("Content-Type:", res.headers.get("content-type")); + + const html = await res.text(); + console.log("Has HTML tag:", html.includes("")); + console.log("Has h1:", html.includes("Hello HTML")); + + `, + "/index.html": /* html */ ` + + + + Test Page + + + +

Hello HTML

+ + + + `, + "/styles.css": /* css */ ` + body { + background: blue; + } + `, + "/app.js": /* js */ ` + console.log("Client app loaded"); + `, + }, + run: { + stdout: "Status: 200\nContent-Type: text/html;charset=utf-8\nHas HTML tag: true\nHas h1: true", + }, + }); + + itBundled("compile/HTMLServerMultipleRoutes", { + compile: true, + files: { + "/entry.ts": /* js */ ` + import home from "./home.html"; + import about from "./about.html"; + + using server = Bun.serve({ + port: 0, + routes: { + "/": home, + "/about": about, + }, + }); + + // Test home route + const homeRes = await fetch(server.url); + console.log("Home status:", homeRes.status); + const homeHtml = await homeRes.text(); + console.log("Home has content:", homeHtml.includes("Home Page")); + + // Test about route + const aboutRes = await fetch(server.url + "about"); + console.log("About status:", aboutRes.status); + const aboutHtml = await aboutRes.text(); + console.log("About has content:", aboutHtml.includes("About Page")); + `, + "/home.html": /* html */ ` + + + + Home + + + +

Home Page

+ + + + `, + "/about.html": /* html */ ` + + + + About + + + +

About Page

+ + + + `, + "/styles.css": /* css */ ` + body { + margin: 0; + font-family: sans-serif; + } + `, + "/app.js": /* js */ ` + console.log("App loaded"); + `, + }, + run: { + stdout: "Home status: 200\nHome has content: true\nAbout status: 200\nAbout has content: true", + }, + }); +}); diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index f840f0987b..ac05275e59 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -44,7 +44,7 @@ const words: Record ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 284 }, "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 175 }, - "global.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 28 }, + "global.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 29 }, "globalObject.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 49 }, "globalThis.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 139 }, }; diff --git a/test/js/bun/http/bun-serve-html-manifest.test.ts b/test/js/bun/http/bun-serve-html-manifest.test.ts new file mode 100644 index 0000000000..939489eefe --- /dev/null +++ b/test/js/bun/http/bun-serve-html-manifest.test.ts @@ -0,0 +1,372 @@ +import { describe, expect, it } from "bun:test"; +import { bunEnv, bunExe, rmScope, tempDirWithFiles } from "harness"; +import { join } from "node:path"; +import { StringDecoder } from "node:string_decoder"; + +describe("Bun.serve HTML manifest", () => { + it("serves HTML import with manifest", async () => { + const dir = tempDirWithFiles("serve-html", { + "server.ts": ` + import index from "./index.html"; + + const server = Bun.serve({ + port: 0, + routes: { + "/": index, + }, + }); + + console.log("PORT=" + server.port); + + // Test the manifest structure + console.log("Manifest type:", typeof index); + console.log("Has index:", "index" in index); + console.log("Has files:", "files" in index); + if (index.files) { + console.log("File count:", index.files.length); + } + `, + "index.html": ` + + + Test + + + +

Hello World

+ + +`, + "styles.css": `body { background: red; }`, + "app.js": `console.log("Hello from app");`, + }); + + using cleanup = { [Symbol.dispose]: () => rmScope(dir) }; + + const proc = Bun.spawn({ + cmd: [bunExe(), "run", join(dir, "server.ts")], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + const { stdout, stderr, exited } = proc; + + // Read stdout line by line until we get the PORT + let port: number | undefined; + const reader = stdout.getReader(); + const decoder = new StringDecoder("utf8"); + let buffer = ""; + + while (!port) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.write(value); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const portMatch = line.match(/PORT=(\d+)/); + if (portMatch) { + port = parseInt(portMatch[1]); + break; + } + } + } + + reader.releaseLock(); + expect(port).toBeDefined(); + + if (port) { + // Test the server + const res = await fetch(`http://localhost:${port}/`); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/html"); + + const html = await res.text(); + expect(html).toContain("Hello World"); + expect(html).toContain(" { + const dir = tempDirWithFiles("serve-html-bundled", { + "build.ts": ` + const result = await Bun.build({ + entrypoints: ["./server.ts"], + target: "bun", + outdir: "./dist", + }); + + if (!result.success) { + console.error("Build failed"); + process.exit(1); + } + + console.log("Build complete"); + `, + "server.ts": ` + import index from "./index.html"; + import about from "./about.html"; + + const server = Bun.serve({ + port: 0, + routes: { + "/": index, + "/about": about, + }, + }); + + console.log("PORT=" + server.port); + `, + "index.html": ` + + + Home + + + +

Home Page

+ + +`, + "about.html": ` + + + About + + + +

About Page

+ + +`, + "shared.css": `body { margin: 0; }`, + "app.js": `console.log("App loaded");`, + }); + + using cleanup = { [Symbol.dispose]: () => rmScope(dir) }; + + // Build first + const buildProc = Bun.spawn({ + cmd: [bunExe(), "run", join(dir, "build.ts")], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + await buildProc.exited; + expect(buildProc.exitCode).toBe(0); + + // Run the built server + const serverProc = Bun.spawn({ + cmd: [bunExe(), "run", join(dir, "dist", "server.js")], + cwd: join(dir, "dist"), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + // Read stdout line by line until we get the PORT + let port: number | undefined; + const reader = serverProc.stdout.getReader(); + const decoder = new StringDecoder("utf8"); + let buffer = ""; + + while (!port) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.write(value); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const portMatch = line.match(/PORT=(\d+)/); + if (portMatch) { + port = parseInt(portMatch[1]); + break; + } + } + } + + reader.releaseLock(); + expect(port).toBeDefined(); + + if (port) { + // Test both routes + const homeRes = await fetch(`http://localhost:${port}/`); + expect(homeRes.status).toBe(200); + const homeHtml = await homeRes.text(); + expect(homeHtml).toContain("Home Page"); + + const aboutRes = await fetch(`http://localhost:${port}/about`); + expect(aboutRes.status).toBe(200); + const aboutHtml = await aboutRes.text(); + expect(aboutHtml).toContain("About Page"); + } + + serverProc.kill(); + await serverProc.exited; + }); + + it("validates manifest files exist", async () => { + const dir = tempDirWithFiles("serve-html-validate", { + "test.ts": ` + // Create a fake manifest + const fakeManifest = { + index: "./index.html", + files: [ + { + input: "index.html", + path: "./does-not-exist.html", + loader: "html", + isEntry: true, + headers: { + etag: "test123", + "content-type": "text/html;charset=utf-8" + } + } + ] + }; + + try { + const server = Bun.serve({ + port: 0, + routes: { + "/": fakeManifest, + }, + }); + console.log("ERROR: Server started when it should have failed"); + server.stop(); + } catch (error) { + console.log("SUCCESS: Manifest validation failed as expected"); + } + `, + }); + + using cleanup = { [Symbol.dispose]: () => rmScope(dir) }; + + const proc = Bun.spawn({ + cmd: [bunExe(), "run", join(dir, "test.ts")], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + const out = await new Response(proc.stdout).text(); + await proc.exited; + + expect(out).toContain("SUCCESS: Manifest validation failed as expected"); + }); + + it("serves manifest with proper headers", async () => { + const dir = tempDirWithFiles("serve-html-headers", { + "server.ts": ` + import index from "./index.html"; + + const server = Bun.serve({ + port: 0, + routes: { + "/": index, + }, + }); + + console.log("PORT=" + server.port); + + // Check manifest structure + if (index.files) { + for (const file of index.files) { + console.log("File:", file.path, "Loader:", file.loader); + if (file.headers) { + console.log(" Content-Type:", file.headers["content-type"]); + console.log(" Has ETag:", !!file.headers.etag); + } + } + } + `, + "index.html": ` + + + Test + + + +

Test

+ +`, + "test.css": `h1 { color: red; }`, + }); + + using cleanup = { [Symbol.dispose]: () => rmScope(dir) }; + + // Build first to generate the manifest + const buildProc = Bun.spawn({ + cmd: [bunExe(), "build", join(dir, "server.ts"), "--outdir", join(dir, "dist"), "--target", "bun"], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + await buildProc.exited; + expect(buildProc.exitCode).toBe(0); + + // Run the built server + const proc = Bun.spawn({ + cmd: [bunExe(), "run", join(dir, "dist", "server.js")], + cwd: join(dir, "dist"), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + // Read stdout line by line to collect all output + const reader = proc.stdout.getReader(); + const decoder = new StringDecoder("utf8"); + let buffer = ""; + let output = ""; + let etagCount = 0; + const expectedEtagLines = 2; // One for HTML, one for CSS + + while (etagCount < expectedEtagLines) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.write(value); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + output += line + "\n"; + if (line.includes("Has ETag:")) { + etagCount++; + } + } + } + + reader.releaseLock(); + + // Should have proper content types + expect(output).toContain("text/html"); + expect(output).toContain("text/css"); + expect(output).toContain("Has ETag:"); + + proc.kill(); + await proc.exited; + }); +}); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index b313ae40dc..39d038dc19 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -29,6 +29,8 @@ test/bundler/css/wpt/relative_color_out_of_gamut.test.ts test/bundler/esbuild/css.test.ts test/bundler/esbuild/dce.test.ts test/bundler/esbuild/extra.test.ts +test/bundler/bundler_html_server.test.ts +test/js/bun/http/bun-serve-html-manifest.test.ts test/bundler/esbuild/importstar_ts.test.ts test/bundler/esbuild/importstar.test.ts test/bundler/esbuild/loader.test.ts From fd91e3de0df6ef925d8fa4ddc840441c10dfab43 Mon Sep 17 00:00:00 2001 From: familyboat <84062528+familyboat@users.noreply.github.com> Date: Sat, 21 Jun 2025 10:57:36 +0800 Subject: [PATCH 016/147] fix typo (#20449) --- docs/bundler/vs-esbuild.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index 35bb62f6f7..1a57c136ca 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -125,7 +125,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot - `--target` - n/a -- No supported. Bun's bundler performs no syntactic down-leveling at this time. +- Not supported. Bun's bundler performs no syntactic down-leveling at this time. --- From 282dda62c8ea35ac5ba9482a90c95bfb592d9513 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sat, 21 Jun 2025 12:58:54 +1000 Subject: [PATCH 017/147] Add `--import` as alias to `--preload` for nodejs compat (#20523) --- src/cli/Arguments.zig | 40 +++++------ .../bunfig/fixtures/preload/many/index.ts | 1 + .../bunfig/fixtures/preload/many/preload1.ts | 1 + .../bunfig/fixtures/preload/many/preload2.ts | 1 + .../bunfig/fixtures/preload/many/preload3.ts | 1 + test/config/bunfig/preload.test.ts | 70 ++++++++++++++++++- test/internal/ban-words.test.ts | 2 +- 7 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 test/config/bunfig/fixtures/preload/many/index.ts create mode 100644 test/config/bunfig/fixtures/preload/many/preload1.ts create mode 100644 test/config/bunfig/fixtures/preload/many/preload2.ts create mode 100644 test/config/bunfig/fixtures/preload/many/preload3.ts diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index d29edde371..e6e6659a9f 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -82,6 +82,7 @@ pub const runtime_params_ = [_]ParamType{ clap.parseParam("--smol Use less memory, but run garbage collection more often") catch unreachable, clap.parseParam("-r, --preload ... Import a module before other modules are loaded") catch unreachable, clap.parseParam("--require ... Alias of --preload, for Node.js compatibility") catch unreachable, + clap.parseParam("--import ... Alias of --preload, for Node.js compatibility") catch unreachable, clap.parseParam("--inspect ? Activate Bun's debugger") catch unreachable, clap.parseParam("--inspect-wait ? Activate Bun's debugger, wait for a connection before executing") catch unreachable, clap.parseParam("--inspect-brk ? Activate Bun's debugger, set breakpoint on first line of code and wait") catch unreachable, @@ -542,13 +543,23 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C // runtime commands if (cmd == .AutoCommand or cmd == .RunCommand or cmd == .TestCommand or cmd == .RunAsNodeCommand) { - var preloads = args.options("--preload"); - if (preloads.len == 0) { - if (bun.getenvZ("BUN_INSPECT_PRELOAD")) |preload| { - preloads = bun.default_allocator.dupe([]const u8, &.{preload}) catch unreachable; + { + const preloads = args.options("--preload"); + const preloads2 = args.options("--require"); + const preloads3 = args.options("--import"); + const preload4 = bun.getenvZ("BUN_INSPECT_PRELOAD"); + + const total_preloads = ctx.preloads.len + preloads.len + preloads2.len + preloads3.len + (if (preload4 != null) @as(usize, 1) else @as(usize, 0)); + if (total_preloads > 0) { + var all = std.ArrayList(string).initCapacity(ctx.allocator, total_preloads) catch unreachable; + if (ctx.preloads.len > 0) all.appendSliceAssumeCapacity(ctx.preloads); + if (preloads.len > 0) all.appendSliceAssumeCapacity(preloads); + if (preloads2.len > 0) all.appendSliceAssumeCapacity(preloads2); + if (preloads3.len > 0) all.appendSliceAssumeCapacity(preloads3); + if (preload4) |p| all.appendAssumeCapacity(p); + ctx.preloads = all.items; } } - const preloads2 = args.options("--require"); if (args.flag("--hot")) { ctx.debug.hot_reload = .hot; @@ -645,25 +656,6 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C } } - if (ctx.preloads.len > 0 and (preloads.len > 0 or preloads2.len > 0)) { - var all = std.ArrayList(string).initCapacity(ctx.allocator, ctx.preloads.len + preloads.len + preloads2.len) catch unreachable; - all.appendSliceAssumeCapacity(ctx.preloads); - all.appendSliceAssumeCapacity(preloads); - all.appendSliceAssumeCapacity(preloads2); - ctx.preloads = all.items; - } else if (preloads.len > 0) { - if (preloads2.len > 0) { - var all = std.ArrayList(string).initCapacity(ctx.allocator, preloads.len + preloads2.len) catch unreachable; - all.appendSliceAssumeCapacity(preloads); - all.appendSliceAssumeCapacity(preloads2); - ctx.preloads = all.items; - } else { - ctx.preloads = preloads; - } - } else if (preloads2.len > 0) { - ctx.preloads = preloads2; - } - if (args.option("--print")) |script| { ctx.runtime_options.eval.script = script; ctx.runtime_options.eval.eval_and_print = true; diff --git a/test/config/bunfig/fixtures/preload/many/index.ts b/test/config/bunfig/fixtures/preload/many/index.ts new file mode 100644 index 0000000000..8f356d1362 --- /dev/null +++ b/test/config/bunfig/fixtures/preload/many/index.ts @@ -0,0 +1 @@ +console.log(globalThis.preload); \ No newline at end of file diff --git a/test/config/bunfig/fixtures/preload/many/preload1.ts b/test/config/bunfig/fixtures/preload/many/preload1.ts new file mode 100644 index 0000000000..63141e2cd2 --- /dev/null +++ b/test/config/bunfig/fixtures/preload/many/preload1.ts @@ -0,0 +1 @@ +(globalThis.preload ??= []).push("multi/preload1.ts"); diff --git a/test/config/bunfig/fixtures/preload/many/preload2.ts b/test/config/bunfig/fixtures/preload/many/preload2.ts new file mode 100644 index 0000000000..59d054a998 --- /dev/null +++ b/test/config/bunfig/fixtures/preload/many/preload2.ts @@ -0,0 +1 @@ +(globalThis.preload ??= []).push("multi/preload2.ts"); diff --git a/test/config/bunfig/fixtures/preload/many/preload3.ts b/test/config/bunfig/fixtures/preload/many/preload3.ts new file mode 100644 index 0000000000..c5da4a3366 --- /dev/null +++ b/test/config/bunfig/fixtures/preload/many/preload3.ts @@ -0,0 +1 @@ +(globalThis.preload ??= []).push("multi/preload3.ts"); diff --git a/test/config/bunfig/preload.test.ts b/test/config/bunfig/preload.test.ts index 895be5ea57..44daf6c91d 100644 --- a/test/config/bunfig/preload.test.ts +++ b/test/config/bunfig/preload.test.ts @@ -7,13 +7,17 @@ const fixturePath = (...segs: string[]) => resolve(import.meta.dirname, "fixture type Opts = { args?: string[]; cwd?: string; + env?: Record; }; type Out = [stdout: string, stderr: string, exitCode: number]; -const run = (file: string, { args = [], cwd }: Opts = {}): Promise => { +const run = (file: string, { args = [], cwd, env = {} }: Opts = {}): Promise => { const res = Bun.spawn([bunExe(), ...args, file], { cwd, stdio: ["ignore", "pipe", "pipe"], - env: bunEnv, + env: { + ...env, + ...bunEnv, + }, } satisfies SpawnOptions.OptionsObject<"ignore", "pipe", "pipe">); return Promise.all([ @@ -134,3 +138,65 @@ describe("Given a `bunfig.toml` file with a relative path without a leading './' expect(code).toBe(0); }); }); // + +describe("Test that all the aliases for --preload work", () => { + const dir = fixturePath("many"); + + it.each(["--preload=./preload1.ts", "--require=./preload1.ts", "--import=./preload1.ts"])( + "When `bun run` is run with %s, the preload is executed", + async flag => { + const [out, err, code] = await run("index.ts", { args: [flag], cwd: dir }); + expect(err).toBeEmpty(); + expect(out).toBe('[ "multi/preload1.ts" ]'); + expect(code).toBe(0); + }, + ); + + it.each(["1", "2", "3", "4"])( + "When multiple preload flags are used, they execute in order: --preload, --require, --import (#%s)", + async i => { + let args: string[] = []; + if (i === "1") args = ["--preload", "./preload1.ts", "--require", "./preload2.ts", "--import", "./preload3.ts"]; + if (i === "2") args = ["--import", "./preload3.ts", "--preload=./preload1.ts", "--require", "./preload2.ts"]; + if (i === "3") args = ["--require", "./preload2.ts", "--import", "./preload3.ts", "--preload", "./preload1.ts"]; + if (i === "4") args = ["--require", "./preload1.ts", "--import", "./preload3.ts", "--require", "./preload2.ts"]; + const [out, err, code] = await run("index.ts", { args, cwd: dir }); + expect(err).toBeEmpty(); + expect(out).toBe('[ "multi/preload1.ts", "multi/preload2.ts", "multi/preload3.ts" ]'); + expect(code).toBe(0); + }, + ); + + it("Duplicate preload flags are only executed once", async () => { + const args = ["--preload", "./preload1.ts", "--require", "./preload1.ts", "--import", "./preload1.ts"]; + const [out, err, code] = await run("index.ts", { args, cwd: dir }); + expect(err).toBeEmpty(); + expect(out).toBe('[ "multi/preload1.ts" ]'); + expect(code).toBe(0); + }); + + it("Test double preload flags", async () => { + const dir = fixturePath("many"); + const args = [ + "--preload", + "./preload1.ts", + "--preload=./preload2.ts", + "--preload", + "./preload3.ts", + "-r", + "./preload3.ts", + ]; + const [out, err, code] = await run("index.ts", { args, cwd: dir }); + expect(err).toBeEmpty(); + expect(out).toMatchInlineSnapshot(`"[ "multi/preload1.ts", "multi/preload2.ts", "multi/preload3.ts" ]"`); + expect(code).toBe(0); + }); +}); // + +test("Test BUN_INSPECT_PRELOAD is used to set preloads", async () => { + const dir = fixturePath("many"); + const [out, err, code] = await run("index.ts", { args: [], cwd: dir, env: { BUN_INSPECT_PRELOAD: "./preload1.ts" } }); + expect(err).toBeEmpty(); + expect(out).toMatchInlineSnapshot(`"[ "multi/preload1.ts" ]"`); + expect(code).toBe(0); +}); // diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index ac05275e59..3ac65c6498 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 242, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1860 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1859 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 179 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 102 }, From 0b5363099b3a01057106b6582937c465611d539c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 21 Jun 2025 01:00:48 -0700 Subject: [PATCH 018/147] Some docs --- docs/api/http.md | 10 +++-- docs/bundler/executables.md | 77 +++++++++++++++++++++++++++++++- docs/bundler/fullstack.md | 89 +++++++++++++++++++++++++++++++++++-- docs/bundler/index.md | 3 +- docs/bundler/loaders.md | 14 ++++++ 5 files changed, 185 insertions(+), 8 deletions(-) diff --git a/docs/api/http.md b/docs/api/http.md index 4e4fd43e62..0a0c959e48 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -326,7 +326,11 @@ Bun.serve({ ### HTML imports -To add a client-side single-page app, you can use an HTML import: +Bun supports importing HTML files directly into your server code, enabling full-stack applications with both server-side and client-side code. HTML imports work in two modes: + +**Development (`bun --hot`):** Assets are bundled on-demand at runtime, enabling hot module replacement (HMR) for a fast, iterative development experience. When you change your frontend code, the browser automatically updates without a full page reload. + +**Production (`bun build`):** When building with `bun build --target=bun`, the `import index from "./index.html"` statement resolves to a pre-built manifest object containing all bundled client assets. `Bun.serve` consumes this manifest to serve optimized assets with zero runtime bundling overhead. This is ideal for deploying to production. ```ts import myReactSinglePageApp from "./index.html"; @@ -338,9 +342,9 @@ Bun.serve({ }); ``` -HTML imports don't just serve HTML. It's a full-featured frontend bundler, transpiler, and toolkit built using Bun's [bundler](https://bun.sh/docs/bundler), JavaScript transpiler and CSS parser. +HTML imports don't just serve HTML — it's a full-featured frontend bundler, transpiler, and toolkit built using Bun's [bundler](https://bun.sh/docs/bundler), JavaScript transpiler and CSS parser. You can use this to build full-featured frontends with React, TypeScript, Tailwind CSS, and more. -You can use this to build a full-featured frontend with React, TypeScript, Tailwind CSS, and more. Check out [/docs/bundler/fullstack](https://bun.sh/docs/bundler/fullstack) to learn more. +For a complete guide on building full-stack applications with HTML imports, including detailed examples and best practices, see [/docs/bundler/fullstack](https://bun.sh/docs/bundler/fullstack). ### Practical example: REST API diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 2cdad23f40..76d37954cf 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -126,6 +126,81 @@ The `--sourcemap` argument embeds a sourcemap compressed with zstd, so that erro The `--bytecode` argument enables bytecode compilation. Every time you run JavaScript code in Bun, JavaScriptCore (the engine) will compile your source code into bytecode. We can move this parsing work from runtime to bundle time, saving you startup time. +## Full-stack executables + +{% note %} + +New in Bun v1.2.17 + +{% /note %} + +Bun's `--compile` flag can create standalone executables that contain both server and client code, making it ideal for full-stack applications. When you import an HTML file in your server code, Bun automatically bundles all frontend assets (JavaScript, CSS, etc.) and embeds them into the executable. When Bun sees the HTML import on the server, it kicks off a frontend build process to bundle JavaScript, CSS, and other assets. + +{% codetabs %} + +```ts#server.ts +import { serve } from "bun"; +import index from "./index.html"; + +const server = serve({ + routes: { + "/": index, + "/api/hello": { GET: () => Response.json({ message: "Hello from API" }) }, + }, +}); + +console.log(`Server running at http://localhost:${server.port}`); +``` + +```html#index.html + + + + My App + + + +

Hello World

+ + + +``` + +```js#app.js +console.log("Hello from the client!"); +``` + +```css#styles.css +body { + background-color: #f0f0f0; +} +``` + +{% /codetabs %} + +To build this into a single executable: + +```sh +bun build --compile ./server.ts --outfile myapp +``` + +This creates a self-contained binary that includes: + +- Your server code +- The Bun runtime +- All frontend assets (HTML, CSS, JavaScript) +- Any npm packages used by your server + +The result is a single file that can be deployed anywhere without needing Node.js, Bun, or any dependencies installed. Just run: + +```sh +./myapp +``` + +Bun automatically handles serving the frontend assets with proper MIME types and cache headers. The HTML import is replaced with a manifest object that `Bun.serve` uses to efficiently serve pre-bundled assets. + +For more details on building full-stack applications with Bun, see the [full-stack guide](/docs/bundler/fullstack). + ## Worker To use workers in a standalone executable, add the worker's entrypoint to the CLI arguments: @@ -174,7 +249,7 @@ $ ./hello Standalone executables support embedding files. -To embed files into an executable with `bun build --compile`, import the file in your code +To embed files into an executable with `bun build --compile`, import the file in your code. ```ts // this becomes an internal file path diff --git a/docs/bundler/fullstack.md b/docs/bundler/fullstack.md index e89b29af2e..f46c1aef58 100644 --- a/docs/bundler/fullstack.md +++ b/docs/bundler/fullstack.md @@ -1,5 +1,3 @@ -Using `Bun.serve()`'s `routes` option, you can run your frontend and backend in the same app with no extra steps. - To get started, import HTML files and pass them to the `routes` option in `Bun.serve()`. ```ts @@ -234,7 +232,92 @@ When `console: true` is set, Bun will stream console logs from the browser to th #### Production mode -When serving your app in production, set `development: false` in `Bun.serve()`. +Hot reloading and `development: true` helps you iterate quickly, but in production, your server should be as fast as possible and have as few external dependencies as possible. + +##### Ahead of time bundling (recommended) + +As of Bun v1.2.17, you can use `Bun.build` or `bun build` to bundle your full-stack application ahead of time. + +```sh +$ bun build --target=bun --production --outdir=dist ./src/index.ts +``` + +When Bun's bundler sees an HTML import from server-side code, it will bundle the referenced JavaScript/TypeScript/TSX/JSX and CSS files into a manifest object that Bun.serve() can use to serve the assets. + +```ts +import { serve } from "bun"; +import index from "./index.html"; + +serve({ + routes: { "/": index }, +}); +``` + +{% details summary="Internally, the `index` variable is a manifest object that looks something like this" %} + +```json +{ + "index": "./index.html", + "files": [ + { + "input": "index.html", + "path": "./index-f2me3qnf.js", + "loader": "js", + "isEntry": true, + "headers": { + "etag": "eet6gn75", + "content-type": "text/javascript;charset=utf-8" + } + }, + { + "input": "index.html", + "path": "./index.html", + "loader": "html", + "isEntry": true, + "headers": { + "etag": "r9njjakd", + "content-type": "text/html;charset=utf-8" + } + }, + { + "input": "index.html", + "path": "./index-gysa5fmk.css", + "loader": "css", + "isEntry": true, + "headers": { + "etag": "50zb7x61", + "content-type": "text/css;charset=utf-8" + } + }, + { + "input": "logo.svg", + "path": "./logo-kygw735p.svg", + "loader": "file", + "isEntry": false, + "headers": { + "etag": "kygw735p", + "content-type": "application/octet-stream" + } + }, + { + "input": "react.svg", + "path": "./react-ck11dneg.svg", + "loader": "file", + "isEntry": false, + "headers": { + "etag": "ck11dneg", + "content-type": "application/octet-stream" + } + } + ] +} +``` + +{% /details %} + +##### Runtime bundling + +When adding a build step is too complicated, you can set `development: false` in `Bun.serve()`. - Enable in-memory caching of bundled assets. Bun will bundle assets lazily on the first request to an `.html` file, and cache the result in memory until the server restarts. - Enables `Cache-Control` headers and `ETag` headers diff --git a/docs/bundler/index.md b/docs/bundler/index.md index 73987cc472..a889343eee 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -26,6 +26,7 @@ The bundler is a key piece of infrastructure in the JavaScript ecosystem. As a b - **Reducing HTTP requests.** A single package in `node_modules` may consist of hundreds of files, and large applications may have dozens of such dependencies. Loading each of these files with a separate HTTP request becomes untenable very quickly, so bundlers are used to convert our application source code into a smaller number of self-contained "bundles" that can be loaded with a single request. - **Code transforms.** Modern apps are commonly built with languages or tools like TypeScript, JSX, and CSS modules, all of which must be converted into plain JavaScript and CSS before they can be consumed by a browser. The bundler is the natural place to configure these transformations. - **Framework features.** Frameworks rely on bundler plugins & code transformations to implement common patterns like file-system routing, client-server code co-location (think `getServerSideProps` or Remix loaders), and server components. +- **Full-stack Applications.** Bun's bundler can handle both server and client code in a single command, enabling optimized production builds and single-file executables. With build-time HTML imports, you can bundle your entire application — frontend assets and backend server — into a single deployable unit. Let's jump into the bundler API. @@ -324,7 +325,7 @@ Depending on the target, Bun will apply different module resolution rules and op --- - `bun` -- For generating bundles that are intended to be run by the Bun runtime. In many cases, it isn't necessary to bundle server-side code; you can directly execute the source code without modification. However, bundling your server code can reduce startup times and improve running performance. +- For generating bundles that are intended to be run by the Bun runtime. In many cases, it isn't necessary to bundle server-side code; you can directly execute the source code without modification. However, bundling your server code can reduce startup times and improve running performance. This is the target to use for building full-stack applications with build-time HTML imports, where both server and client code are bundled together. All bundles generated with `target: "bun"` are marked with a special `// @bun` pragma, which indicates to the Bun runtime that there's no need to re-transpile the file before execution. diff --git a/docs/bundler/loaders.md b/docs/bundler/loaders.md index 6fac8cdaa6..440e579274 100644 --- a/docs/bundler/loaders.md +++ b/docs/bundler/loaders.md @@ -262,6 +262,20 @@ Currently, the list of selectors is: - `video[poster]` - `video[src]` +{% callout %} + +**HTML Loader Behavior in Different Contexts** + +The `html` loader behaves differently depending on how it's used: + +1. **Static Build:** When you run `bun build ./index.html`, Bun produces a static site with all assets bundled and hashed. + +2. **Runtime:** When you run `bun run server.ts` (where `server.ts` imports an HTML file), Bun bundles assets on-the-fly during development, enabling features like hot module replacement. + +3. **Full-stack Build:** When you run `bun build --target=bun server.ts` (where `server.ts` imports an HTML file), the import resolves to a manifest object that `Bun.serve` uses to efficiently serve pre-bundled assets in production. + +{% /callout %} + ### `sh` loader **Bun Shell loader**. Default for `.sh` files From 29dd4166f21221b59b25d6669c465d4d58a68348 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sat, 21 Jun 2025 01:48:09 -0700 Subject: [PATCH 019/147] Bump --- LATEST | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LATEST b/LATEST index a96f385f15..21344eb17a 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.2.16 \ No newline at end of file +1.2.17 \ No newline at end of file diff --git a/package.json b/package.json index 06acf99b10..a360184bbe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "bun", - "version": "1.2.17", + "version": "1.2.18", "workspaces": [ "./packages/bun-types", "./packages/@types/bun" From c40468ea39299b63032f2bf70268eb102f8641a1 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sat, 21 Jun 2025 18:53:33 -0800 Subject: [PATCH 020/147] install: fix crash researching #5949 (#20461) --- src/install/install.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/install/install.zig b/src/install/install.zig index 555513b573..e9e3e1d2c1 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -799,6 +799,9 @@ pub const Task = struct { return; } + this.err = err; + this.status = Status.fail; + this.data = .{ .git_clone = bun.invalid_fd }; attempt += 1; break :brk null; }; @@ -816,12 +819,12 @@ pub const Task = struct { this.err = err; this.status = Status.fail; this.data = .{ .git_clone = bun.invalid_fd }; - return; } else { return; }; + this.err = null; this.data = .{ .git_clone = .fromStdDir(dir) }; this.status = Status.success; }, From 37505ad955da9f5b2ebba168154e6d31b8ecb674 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 21 Jun 2025 23:43:55 -0700 Subject: [PATCH 021/147] Deflake test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts on Windows --- test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts b/test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts index 891591e408..dbfb71c7c8 100644 --- a/test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts +++ b/test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts @@ -33,6 +33,6 @@ if (numAbortSignalObjects > 10) { } const rss = (process.memoryUsage().rss / 1024 / 1024) | 0; -if (rss > 170) { +if (rss > 200) { throw new Error(`Memory leak detected: ${rss} MB, expected < 170 MB`); } From 064d7bb56e41254c1163f120498f7024114eb89d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 21 Jun 2025 23:44:28 -0700 Subject: [PATCH 022/147] Fixes #10675 (#20553) --- src/bun.js/bindings/ZigGlobalObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index ef26ff8471..48da391232 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2106,7 +2106,7 @@ extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JS JSValue target = object; JSValue fn = JSValue(); auto* function = jsDynamicCast(object); - if (function && function->jsExecutable() && function->jsExecutable()->isAsyncGenerator()) { + if (function && !function->isHostFunction() && function->jsExecutable() && function->jsExecutable()->isAsyncGenerator()) { fn = object; target = jsUndefined(); } else if (auto iterable = object->getIfPropertyExists(globalObject, vm.propertyNames->asyncIteratorSymbol)) { From 2cbb196f293fa5429ac01167d7d89b7fd0d0262c Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:44:59 -0700 Subject: [PATCH 023/147] Fix crash with garbage environment variables (#20527) Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Zack Radisic --- .../bindings/JSEnvironmentVariableMap.cpp | 11 +- test/cli/run/garbage-env.c | 133 ++++++++++++++++++ test/cli/run/garbage-env.test.ts | 25 ++++ test/no-validate-exceptions.txt | 1 + 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 test/cli/run/garbage-env.c create mode 100644 test/cli/run/garbage-env.test.ts diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp index d3f1baafb9..7c1781298f 100644 --- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp +++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp @@ -305,10 +305,13 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) bool hasNodeTLSRejectUnauthorized = false; bool hasBunConfigVerboseFetch = false; + auto* cached_getter_setter = JSC::CustomGetterSetter::create(vm, jsGetterEnvironmentVariable, nullptr); + for (size_t i = 0; i < count; i++) { unsigned char* chars; size_t len = Bun__getEnvKey(list, i, &chars); - auto name = String::fromUTF8(std::span { chars, len }); + // We can't really trust that the OS gives us valid UTF-8 + auto name = String::fromUTF8ReplacingInvalidSequences(std::span { chars, len }); #if OS(WINDOWS) keyArray->putByIndexInline(globalObject, (unsigned)i, jsString(vm, name), false); #endif @@ -347,7 +350,11 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) } } - object->putDirectCustomAccessor(vm, identifier, JSC::CustomGetterSetter::create(vm, jsGetterEnvironmentVariable, jsSetterEnvironmentVariable), JSC::PropertyAttribute::CustomAccessor | 0); + // JSC::PropertyAttribute::CustomValue calls the getter ONCE (the first + // time) and then sets it onto the object, subsequent calls to the + // getter will not go through the getter and instead will just do the + // property lookup. + object->putDirectCustomAccessor(vm, identifier, cached_getter_setter, JSC::PropertyAttribute::CustomValue | 0); } unsigned int TZAttrs = JSC::PropertyAttribute::CustomAccessor | 0; diff --git a/test/cli/run/garbage-env.c b/test/cli/run/garbage-env.c new file mode 100644 index 0000000000..de651b305b --- /dev/null +++ b/test/cli/run/garbage-env.c @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include + +int main() { + int stdout_pipe[2], stderr_pipe[2]; + pid_t pid; + int status; + char stdout_buffer[4096] = {0}; + char stderr_buffer[4096] = {0}; + + // Create pipes for stdout and stderr + if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1) { + perror("pipe"); + return 1; + } + + // Create garbage environment variables with stack buffers containing + // arbitrary bytes + char garbage1[64]; + char garbage2[64]; + char garbage3[64]; + char garbage4[64]; + char garbage5[64]; + + // Fill with arbitrary non-ASCII/UTF-8 bytes + for (int i = 0; i < 63; i++) { + garbage1[i] = (char)(0x80 + (i % 128)); // Invalid UTF-8 start bytes + garbage2[i] = (char)(0xFF - (i % 256)); // High bytes + garbage3[i] = (char)(i * 3 + 128); // Mixed garbage + garbage4[i] = (char)(0xC0 | (i & 0x1F)); // Invalid UTF-8 sequences + } + garbage1[63] = '\0'; + garbage2[63] = '\0'; + garbage3[63] = '\0'; + garbage4[63] = '\0'; + + for (int i = 0; i < 10; i++) { + garbage5[i] = (char)(0x80 + (i % 128)); + } + garbage5[10] = '='; + garbage5[11] = 0x81; + garbage5[12] = 0xF5; + garbage5[13] = 0xC1; + garbage5[14] = 0xC2; + + char *garbage_env[] = { + garbage5, + // garbage1, + // garbage2, + // garbage3, + // garbage4, + "PATH=/usr/bin:/bin", // Keep PATH so we can find commands + "BUN_DEBUG_QUIET_LOGS=1", "OOGA=booga", "OOGA=laskdjflsdf", NULL}; + + pid = fork(); + + if (pid == -1) { + perror("fork"); + return 1; + } + + if (pid == 0) { + // Child process + close(stdout_pipe[0]); // Close read end + close(stderr_pipe[0]); // Close read end + + // Redirect stdout and stderr to pipes + dup2(stdout_pipe[1], STDOUT_FILENO); + dup2(stderr_pipe[1], STDERR_FILENO); + + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + char *BUN_PATH = getenv("BUN_PATH"); + if (BUN_PATH == NULL) { + fprintf(stderr, "Missing BUN_PATH!\n"); + fflush(stderr); + exit(1); + } + execve(BUN_PATH, + (char *[]){"bun-debug", "-e", "console.log(process.env)", NULL}, + garbage_env); + + // If both fail, exit with error + perror("execve"); + exit(127); + } else { + // Parent process + close(stdout_pipe[1]); // Close write end + close(stderr_pipe[1]); // Close write end + + // Read from stdout pipe + ssize_t stdout_bytes = + read(stdout_pipe[0], stdout_buffer, sizeof(stdout_buffer) - 1); + if (stdout_bytes > 0) { + stdout_buffer[stdout_bytes] = '\0'; + } + + // Read from stderr pipe + ssize_t stderr_bytes = + read(stderr_pipe[0], stderr_buffer, sizeof(stderr_buffer) - 1); + if (stderr_bytes > 0) { + stderr_buffer[stderr_bytes] = '\0'; + } + + close(stdout_pipe[0]); + close(stderr_pipe[0]); + + // Wait for child process + waitpid(pid, &status, 0); + + // Print results + printf("=== PROCESS OUTPUT ===\n"); + printf("Exit code: %d\n", WEXITSTATUS(status)); + + printf("\n=== STDOUT ===\n"); + printf("%s", stdout_buffer); + fflush(stdout); + + if (stderr_bytes > 0) { + fprintf(stderr, "\n=== STDERR ===\n"); + fprintf(stderr, "%s", stderr_buffer); + fflush(stderr); + } + exit(status); + } + + return 0; +} \ No newline at end of file diff --git a/test/cli/run/garbage-env.test.ts b/test/cli/run/garbage-env.test.ts new file mode 100644 index 0000000000..299e0f213b --- /dev/null +++ b/test/cli/run/garbage-env.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { bunExe, isPosix } from "harness"; +import path from "path"; + +describe.if(isPosix)("garbage env", () => { + test("garbage env", async () => { + const cfile = path.join(import.meta.dirname, "garbage-env.c"); + { + const cc = Bun.which("clang") || Bun.which("gcc") || Bun.which("cc"); + const { exitCode, stderr } = await Bun.$`${cc} -o garbage-env ${cfile}`; + const stderrText = stderr.toString(); + if (stderrText.length > 0) { + console.error(stderrText); + } + expect(exitCode).toBe(0); + } + + const { exitCode, stderr } = await Bun.$`./garbage-env`.env({ BUN_PATH: bunExe() }); + const stderrText = stderr.toString(); + if (stderrText.length > 0) { + console.error(stderrText); + } + expect(exitCode).toBe(0); + }); +}); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 39d038dc19..ebd4d546bb 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -71,6 +71,7 @@ test/cli/install/catalogs.test.ts test/cli/install/npmrc.test.ts test/cli/install/overrides.test.ts test/cli/run/env.test.ts +test/cli/run/garbage-env.test.ts test/cli/run/esm-defineProperty.test.ts test/cli/run/jsx-namespaced-attributes.test.ts test/cli/run/log-test.test.ts From 25dbe5cf3f05102d6107f088000c0f173e91e779 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 22 Jun 2025 16:45:38 +1000 Subject: [PATCH 024/147] `fs.glob` set `onlyFiles: false` so it can match folders (#20510) --- src/js/internal/fs/glob.ts | 2 ++ test/js/node/fs/glob.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/js/internal/fs/glob.ts b/src/js/internal/fs/glob.ts index 03d2601f94..d78b419bc6 100644 --- a/src/js/internal/fs/glob.ts +++ b/src/js/internal/fs/glob.ts @@ -65,6 +65,8 @@ function mapOptions(options: GlobOptions): ExtendedGlobOptions { cwd: options?.cwd ?? process.cwd(), // https://github.com/nodejs/node/blob/a9546024975d0bfb0a8ae47da323b10fb5cbb88b/lib/internal/fs/glob.js#L655 followSymlinks: true, + // https://github.com/oven-sh/bun/issues/20507 + onlyFiles: false, exclude, }; } diff --git a/test/js/node/fs/glob.test.ts b/test/js/node/fs/glob.test.ts index dc5d70a243..0383efae19 100644 --- a/test/js/node/fs/glob.test.ts +++ b/test/js/node/fs/glob.test.ts @@ -14,6 +14,10 @@ beforeAll(() => { "bar.txt": "bar", "baz.js": "baz", }, + "folder.test": { + "file.txt": "content", + "another-folder": {}, + }, }); }); @@ -61,6 +65,11 @@ describe("fs.glob", () => { expect(() => fs.glob("*.txt", { cwd: tmp }, undefined)).toThrow(TypeError); }); }); + + it("matches directories", () => { + const paths = fs.globSync("*.test", { cwd: tmp }); + expect(paths).toContain("folder.test"); + }); }); // describe("fs.globSync", () => { @@ -120,6 +129,11 @@ describe("fs.globSync", () => { expect(() => fs.globSync(["*.txt"])).toThrow(TypeError); }); }); + + it("matches directories", () => { + const paths = fs.globSync("*.test", { cwd: tmp }); + expect(paths).toContain("folder.test"); + }); }); // describe("fs.promises.glob", () => { @@ -160,4 +174,15 @@ describe("fs.promises.glob", () => { process.cwd = oldProcessCwd; } }); + + it("matches directories", async () => { + const iter = fs.promises.glob("*.test", { cwd: tmp }); + expect(iter[Symbol.asyncIterator]).toBeDefined(); + let count = 0; + for await (const path of iter) { + expect(path).toBe("folder.test"); + count++; + } + expect(count).toBe(1); + }); }); // From 653c45966018bd81ec7823411a2201c0bbdf0551 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 21 Jun 2025 23:56:40 -0700 Subject: [PATCH 025/147] Fix asan failure in napi_async_work caused by accessing napi_async_work after freed (#20554) --- src/bun.js/event_loop/Task.zig | 2 +- src/napi/napi.zig | 67 ++++++++++++++++++---------------- test/napi/napi.test.ts | 5 ++- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/bun.js/event_loop/Task.zig b/src/bun.js/event_loop/Task.zig index 533c9ac879..9f6e5d7104 100644 --- a/src/bun.js/event_loop/Task.zig +++ b/src/bun.js/event_loop/Task.zig @@ -223,7 +223,7 @@ pub fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u3 }, @field(Task.Tag, @typeName(bun.api.napi.napi_async_work)) => { const transform_task: *bun.api.napi.napi_async_work = task.get(bun.api.napi.napi_async_work).?; - transform_task.*.runFromJS(); + transform_task.runFromJS(virtual_machine, global); }, @field(Task.Tag, @typeName(ThreadSafeFunction)) => { var transform_task: *ThreadSafeFunction = task.as(ThreadSafeFunction); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index c54cd8d883..5b25a597b5 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -1047,7 +1047,12 @@ pub const napi_async_work = struct { data: ?*anyopaque = null, status: std.atomic.Value(Status) = .init(.pending), scheduled: bool = false, - ref: Async.KeepAlive = .{}, + poll_ref: Async.KeepAlive = .{}, + ref_count: RefCount, + + const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", destroy, .{}); + pub const ref = RefCount.ref; + pub const deref = RefCount.deref; pub const Status = enum(u32) { pending = 0, @@ -1057,29 +1062,37 @@ pub const napi_async_work = struct { }; pub fn new(env: *NapiEnv, execute: napi_async_execute_callback, complete: ?napi_async_complete_callback, data: ?*anyopaque) !*napi_async_work { - const work = try bun.default_allocator.create(napi_async_work); const global = env.toJS(); - work.* = .{ + + const work = bun.new(napi_async_work, .{ .global = global, .env = env, .execute = execute, .event_loop = global.bunVM().eventLoop(), .complete = complete, .data = data, - }; + .ref_count = .initExactRefs(1), + }); return work; } pub fn destroy(this: *napi_async_work) void { - bun.default_allocator.destroy(this); + bun.debugAssert(!this.poll_ref.isActive()); // we must always have unref'd it. + bun.destroy(this); + } + + pub fn schedule(this: *napi_async_work) void { + if (this.scheduled) return; + this.scheduled = true; + this.poll_ref.ref(this.global.bunVM()); + WorkPool.schedule(&this.task); } pub fn runFromThreadPool(task: *WorkPoolTask) void { var this: *napi_async_work = @fieldParentPtr("task", task); - this.run(); } - pub fn run(this: *napi_async_work) void { + fn run(this: *napi_async_work) void { if (this.status.cmpxchgStrong(.pending, .started, .seq_cst, .seq_cst)) |state| { if (state == .cancelled) { this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); @@ -1092,26 +1105,15 @@ pub const napi_async_work = struct { this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); } - pub fn schedule(this: *napi_async_work) void { - if (this.scheduled) return; - this.scheduled = true; - this.ref.ref(this.global.bunVM()); - WorkPool.schedule(&this.task); - } - pub fn cancel(this: *napi_async_work) bool { return this.status.cmpxchgStrong(.pending, .cancelled, .seq_cst, .seq_cst) == null; } - fn runFromJSWithError(this: *napi_async_work) bun.JSError!void { - - // likely `complete` will call `napi_delete_async_work`, so take a copy - // of `ref` beforehand - var ref = this.ref; - const env = this.env; + fn runFromJSWithError(this: *napi_async_work, vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject) bun.JSError!void { + this.ref(); defer { - const global = env.toJS(); - ref.unref(global.bunVM()); + this.poll_ref.unref(vm); + this.deref(); } // https://github.com/nodejs/node/blob/a2de5b9150da60c77144bb5333371eaca3fab936/src/node_api.cc#L1201 @@ -1119,7 +1121,8 @@ pub const napi_async_work = struct { return; }; - const handle_scope = NapiHandleScope.open(this.env, false); + const env = this.env; + const handle_scope = NapiHandleScope.open(env, false); defer if (handle_scope) |scope| scope.close(env); const status: NapiStatus = if (this.status.load(.seq_cst) == .cancelled) @@ -1128,20 +1131,20 @@ pub const napi_async_work = struct { .ok; complete( - this.env, + env, @intFromEnum(status), this.data, ); - const global = env.toJS(); if (global.hasException()) { return error.JSError; } } - pub fn runFromJS(this: *napi_async_work) void { - this.runFromJSWithError() catch |e| { - this.global.reportActiveExceptionAsUnhandled(e); + pub fn runFromJS(this: *napi_async_work, vm: *JSC.VirtualMachine, global: *JSC.JSGlobalObject) void { + this.runFromJSWithError(vm, global) catch |e| { + // Note: the "this" value here may already be freed. + global.reportActiveExceptionAsUnhandled(e); }; } }; @@ -1306,8 +1309,8 @@ pub export fn napi_delete_async_work(env_: napi_env, work_: ?*napi_async_work) n const work = work_ orelse { return env.invalidArg(); }; - bun.assert(env.toJS() == work.global); - work.destroy(); + if (comptime bun.Environment.allow_assert) bun.assert(env.toJS() == work.global); + work.deref(); return env.ok(); } pub export fn napi_queue_async_work(env_: napi_env, work_: ?*napi_async_work) napi_status { @@ -1318,7 +1321,7 @@ pub export fn napi_queue_async_work(env_: napi_env, work_: ?*napi_async_work) na const work = work_ orelse { return env.invalidArg(); }; - bun.assert(env.toJS() == work.global); + if (comptime bun.Environment.allow_assert) bun.assert(env.toJS() == work.global); work.schedule(); return env.ok(); } @@ -1330,7 +1333,7 @@ pub export fn napi_cancel_async_work(env_: napi_env, work_: ?*napi_async_work) n const work = work_ orelse { return env.invalidArg(); }; - bun.assert(env.toJS() == work.global); + if (comptime bun.Environment.allow_assert) bun.assert(env.toJS() == work.global); if (work.cancel()) { return env.ok(); } diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index c5c2169f37..02c7d78d1c 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -294,8 +294,9 @@ describe("napi", () => { checkSameOutput("test_get_exception", [5]); checkSameOutput("test_get_exception", [{ foo: "bar" }]); }); - it("can throw an exception from an async_complete_callback", () => { - checkSameOutput("create_promise", [true]); + it("can throw an exception from an async_complete_callback", async () => { + const count = 10; + await Promise.all(Array.from({ length: count }, () => checkSameOutput("create_promise", [true]))); }); }); From 197c7abe7ddfc9ee1bf7365fa7c27d0e58afd05e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 21 Jun 2025 23:57:04 -0700 Subject: [PATCH 026/147] Make the napi/v8 tests compile faster (#20555) --- test/napi/napi-app/package.json | 4 ++-- test/napi/napi.test.ts | 2 ++ test/napi/node-napi.test.ts | 2 +- test/v8/v8.test.ts | 13 +++++++++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/test/napi/napi-app/package.json b/test/napi/napi-app/package.json index ebc48bd9e2..5f2c942861 100644 --- a/test/napi/napi-app/package.json +++ b/test/napi/napi-app/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "gypfile": true, "scripts": { - "install": "node-gyp rebuild --debug", - "build": "node-gyp rebuild --debug", + "install": "node-gyp rebuild --debug -j max", + "build": "node-gyp rebuild --debug -j max", "clean": "node-gyp clean" }, "devDependencies": { diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 02c7d78d1c..4143cf0d7d 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -7,6 +7,7 @@ import { join } from "path"; describe("napi", () => { beforeAll(() => { // build gyp + console.time("Building node-gyp"); const install = spawnSync({ cmd: [bunExe(), "install", "--verbose"], cwd: join(__dirname, "napi-app"), @@ -19,6 +20,7 @@ describe("napi", () => { console.error("build failed, bailing out!"); process.exit(1); } + console.timeEnd("Building node-gyp"); }); describe.each(["esm", "cjs"])("bundle .node files to %s via", format => { diff --git a/test/napi/node-napi.test.ts b/test/napi/node-napi.test.ts index 5f043e98ef..5efeb52154 100644 --- a/test/napi/node-napi.test.ts +++ b/test/napi/node-napi.test.ts @@ -98,7 +98,7 @@ beforeAll(async () => { async function buildOne(dir: string) { const child = spawn({ - cmd: [bunExe(), "x", "node-gyp", "rebuild", "--debug"], + cmd: [bunExe(), "x", "node-gyp", "rebuild", "--debug", "-j", "max"], cwd: dir, stderr: "pipe", stdout: "ignore", diff --git a/test/v8/v8.test.ts b/test/v8/v8.test.ts index 0cdcc276e1..353bcdab87 100644 --- a/test/v8/v8.test.ts +++ b/test/v8/v8.test.ts @@ -62,8 +62,17 @@ async function build( const build = spawn({ cmd: runtime == Runtime.bun - ? [bunExe(), "x", "--bun", "node-gyp", "rebuild", buildMode == BuildMode.debug ? "--debug" : "--release"] - : [bunExe(), "x", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode + ? [ + bunExe(), + "x", + "--bun", + "node-gyp", + "rebuild", + buildMode == BuildMode.debug ? "--debug" : "--release", + "-j", + "max", + ] + : [bunExe(), "x", "node-gyp", "rebuild", "--release", "-j", "max"], // for node.js we don't bother with debug mode cwd: tmpDir, env: bunEnv, stdin: "inherit", From 444b9d188384732a574c5b376b3e5a9c4f24a7b4 Mon Sep 17 00:00:00 2001 From: Grigory Date: Sun, 22 Jun 2025 17:33:15 +0500 Subject: [PATCH 027/147] build(windows/resources): specify company name (#20534) --- src/windows-app-info.rc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/windows-app-info.rc b/src/windows-app-info.rc index ed6473fbeb..1e50bc1a0c 100644 --- a/src/windows-app-info.rc +++ b/src/windows-app-info.rc @@ -18,7 +18,8 @@ BEGIN VALUE "InternalName", "bun\0" VALUE "OriginalFilename", "bun.exe\0" VALUE "ProductName", "Bun\0" - VALUE "ProductVersion", "@Bun_VERSION_WITH_TAG@\0" + VALUE "ProductVersion", "@Bun_VERSION_WITH_TAG@\0", + VALUE "CompanyName", "Oven\0" VALUE "LegalCopyright", "https://bun.sh/docs/project/licensing\0" END END From c7b1e5c709a14dc53422a02de59ff63aa836e86c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 22 Jun 2025 18:22:41 -0700 Subject: [PATCH 028/147] Fix fs.watchFile ignoring atime (#20575) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/node/node_fs_stat_watcher.zig | 12 +++++++++++- test/js/node/watch/fs.watchFile.test.ts | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index a58cb8aacf..8fcb3fe33a 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -430,7 +430,17 @@ pub const StatWatcher = struct { .err => std.mem.zeroes(bun.Stat), }; - if (std.mem.eql(u8, std.mem.asBytes(&res), std.mem.asBytes(&this.last_stat))) return; + var compare = res; + const StatT = @TypeOf(compare); + if (@hasField(StatT, "st_atim")) { + compare.st_atim = this.last_stat.st_atim; + } else if (@hasField(StatT, "st_atimespec")) { + compare.st_atimespec = this.last_stat.st_atimespec; + } else if (@hasField(StatT, "atim")) { + compare.atim = this.last_stat.atim; + } + + if (std.mem.eql(u8, std.mem.asBytes(&compare), std.mem.asBytes(&this.last_stat))) return; this.last_stat = res; this.enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(this, swapAndCallListenerOnMainThread)); diff --git a/test/js/node/watch/fs.watchFile.test.ts b/test/js/node/watch/fs.watchFile.test.ts index ec7166259c..dc84364d92 100644 --- a/test/js/node/watch/fs.watchFile.test.ts +++ b/test/js/node/watch/fs.watchFile.test.ts @@ -1,4 +1,4 @@ -import { tempDirWithFiles } from "harness"; +import { isWindows, tempDirWithFiles } from "harness"; import fs from "node:fs"; import path from "path"; @@ -113,6 +113,18 @@ describe("fs.watchFile", () => { expect(typeof entries[0][0].mtimeMs === "bigint").toBe(true); }); + test.if(isWindows)("does not fire on atime-only update", async () => { + let called = false; + const file = path.join(testDir, "watch.txt"); + fs.watchFile(file, { interval: 50 }, () => { + called = true; + }); + fs.readFileSync(file); + await Bun.sleep(100); + fs.unwatchFile(file); + expect(called).toBe(false); + }); + test("StatWatcherScheduler stress test (1000 watchers with random times)", async () => { const EventEmitter = require("events"); let defaultMaxListeners = EventEmitter.defaultMaxListeners; From 5416155449be83fbc61d123d1b72ce49ca06c6ae Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 22 Jun 2025 19:23:15 -0700 Subject: [PATCH 029/147] Enable Math.sumPrecise (#20569) --- src/bun.js/bindings/ZigGlobalObject.cpp | 1 + test/js/web/web-globals.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 48da391232..615d9486de 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -295,6 +295,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c JSC::Options::useJITCage() = false; JSC::Options::useShadowRealm() = true; JSC::Options::useV8DateParser() = true; + JSC::Options::useMathSumPreciseMethod() = true; JSC::Options::evalMode() = evalMode; JSC::Options::heapGrowthSteepnessFactor() = 1.0; JSC::Options::heapGrowthMaxIncrease() = 2.0; diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index 1270c0a810..551578d925 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -30,6 +30,7 @@ test("exists", () => { expect(typeof PerformanceResourceTiming !== "undefined").toBe(true); expect(typeof PerformanceServerTiming !== "undefined").toBe(true); expect(typeof PerformanceTiming !== "undefined").toBe(true); + expect(typeof Math.sumPrecise !== "undefined").toBe(true); }); const globalSetters = [ From 4cc61a1b8c681537cf7cc8ae47ea426d514cb42a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 22 Jun 2025 20:51:45 -0700 Subject: [PATCH 030/147] Fix NODE_PATH for bun build (#20576) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- docs/bundler/fullstack.md | 5 -- docs/cli/bun-create.md | 3 - docs/test/mocks.md | 2 - packages/bun-types/authoring.md | 2 - src/bundler/bundle_v2.zig | 4 +- src/bundler/linker_context/README.md | 3 - src/cli/build_command.zig | 3 + src/js/internal/streams/native-readable.ts | 3 +- src/js/node/wasi.ts | 2 +- src/node-fallbacks/util.js | 2 +- src/runtime.js | 2 +- test/bundler/bun-build-api.test.ts | 64 ++++++++++++++++++++++ test/bundler/bundler_regressions.test.ts | 20 +++++++ test/bundler/cli.test.ts | 2 +- test/bundler/esbuild/default.test.ts | 4 +- test/bundler/expectBundled.ts | 7 ++- test/cli/install/bun-add.test.ts | 4 +- test/cli/install/bun-install.test.ts | 4 +- test/js/bun/shell/bunshell.test.ts | 56 +++++++++---------- test/js/bun/shell/commands/cp.test.ts | 2 +- test/js/bun/test/expect.test.js | 4 +- test/js/bun/util/fuzzy-wuzzy.test.ts | 26 ++++----- 22 files changed, 150 insertions(+), 74 deletions(-) diff --git a/docs/bundler/fullstack.md b/docs/bundler/fullstack.md index f46c1aef58..587afe8c60 100644 --- a/docs/bundler/fullstack.md +++ b/docs/bundler/fullstack.md @@ -381,7 +381,6 @@ Note: this is currently in `bunfig.toml` to make it possible to know statically Bun uses [`HTMLRewriter`](/docs/api/html-rewriter) to scan for ` + + + `, + "script.js": /* js */ ` + console.log("Script loaded"); + `, + "image.jpeg": "actual image data would be here", + }, + entryPoints: ["./index.html"], + outdir: "/out", + plugins(build) { + // This plugin intercepts .jpeg files and returns them with custom contents + // This previously caused a crash because additional_files wasn't populated + build.onLoad({ filter: /\.jpe?g$/ }, async args => { + return { + loader: "file", + contents: "custom image contents", + }; + }); + }, + onAfterBundle(api) { + // Verify the build succeeded and files were created + api.assertFileExists("index.html"); + // The image should be copied with a hashed name + const html = api.readFile("index.html"); + expect(html).toContain('src="'); + expect(html).toContain('.jpeg"'); + }, + }); + + itBundled("plugin/FileLoaderMultipleAssets", { + files: { + "index.js": /* js */ ` + import imgUrl from "./image.png"; + import wasmUrl from "./module.wasm"; + console.log(imgUrl, wasmUrl); + `, + "image.png": "png data", + "module.wasm": "wasm data", + }, + entryPoints: ["./index.js"], + outdir: "/out", + plugins(build) { + // Test multiple file types with custom contents + build.onLoad({ filter: /\.(png|wasm)$/ }, async args => { + const ext = args.path.split(".").pop(); + return { + loader: "file", + contents: `custom ${ext} contents`, + }; + }); + }, + run: { + stdout: /\.(png|wasm)/, + }, + onAfterBundle(api) { + // Verify the build succeeded and files were created + api.assertFileExists("index.js"); + const js = api.readFile("index.js"); + // Should contain references to the copied files + expect(js).toContain('.png"'); + expect(js).toContain('.wasm"'); + }, + }); }); From 0399ae0ee96e885daeee7bb120dc36aa969d645a Mon Sep 17 00:00:00 2001 From: Michael H Date: Tue, 8 Jul 2025 04:21:36 +1000 Subject: [PATCH 128/147] followup for `bun pm version` (#20799) --- docs/cli/pm.md | 7 +- src/cli/pm_version_command.zig | 289 +++-- test/cli/install/bun-pm-version.test.ts | 1392 ++++++++++++----------- 3 files changed, 888 insertions(+), 800 deletions(-) diff --git a/docs/cli/pm.md b/docs/cli/pm.md index 0739de7ae9..577ab383cd 100644 --- a/docs/cli/pm.md +++ b/docs/cli/pm.md @@ -175,13 +175,14 @@ Increment: Options: --no-git-tag-version Skip git operations --allow-same-version Prevents throwing error if version is the same - --message=, -m Custom commit message - --preid= Prerelease identifier + --message=, -m Custom commit message, use %s for version substitution + --preid= Prerelease identifier (i.e beta → 1.0.1-beta.0) + --force, -f Bypass dirty git history check Examples: $ bun pm version patch $ bun pm version 1.2.3 --no-git-tag-version - $ bun pm version prerelease --preid beta + $ bun pm version prerelease --preid beta --message "Release beta: %s" ``` To bump the version in `package.json`: diff --git a/src/cli/pm_version_command.zig b/src/cli/pm_version_command.zig index 2dc75823b6..5bc8511b9b 100644 --- a/src/cli/pm_version_command.zig +++ b/src/cli/pm_version_command.zig @@ -11,6 +11,7 @@ const logger = bun.logger; const JSON = bun.JSON; const RunCommand = bun.RunCommand; const Environment = bun.Environment; +const JSPrinter = bun.js_printer; pub const PmVersionCommand = struct { const VersionType = enum { @@ -59,30 +60,45 @@ pub const PmVersionCommand = struct { defer ctx.allocator.free(package_json_contents); const package_json_source = logger.Source.initPathString(package_json_path, package_json_contents); - const json = JSON.parsePackageJSONUTF8(&package_json_source, ctx.log, ctx.allocator) catch |err| { + const json_result = JSON.parsePackageJSONUTF8WithOpts( + &package_json_source, + ctx.log, + ctx.allocator, + .{ + .is_json = true, + .allow_comments = true, + .allow_trailing_commas = true, + .guess_indentation = true, + }, + ) catch |err| { Output.errGeneric("Failed to parse package.json: {s}", .{@errorName(err)}); Global.exit(1); }; - const scripts = json.asProperty("scripts"); + var json = json_result.root; + + if (json.data != .e_object) { + Output.errGeneric("Failed to parse package.json: root must be an object", .{}); + Global.exit(1); + } + + const scripts = if (pm.options.do.run_scripts) json.asProperty("scripts") else null; const scripts_obj = if (scripts) |s| if (s.expr.data == .e_object) s.expr else null else null; - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("preversion")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "preversion", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("preversion")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "preversion", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -96,49 +112,63 @@ pub const PmVersionCommand = struct { else => {}, } } - Output.errGeneric("No version field found in package.json", .{}); - Global.exit(1); + break :brk_version null; }; - const new_version_str = try calculateNewVersion(ctx.allocator, current_version, version_type, new_version, pm.options.preid, package_json_dir); + const new_version_str = try calculateNewVersion(ctx.allocator, current_version orelse "0.0.0", version_type, new_version, pm.options.preid, package_json_dir); defer ctx.allocator.free(new_version_str); - if (!pm.options.allow_same_version and strings.eql(current_version, new_version_str)) { - Output.errGeneric("Version not changed", .{}); - Global.exit(1); + if (current_version) |version| { + if (!pm.options.allow_same_version and strings.eql(version, new_version_str)) { + Output.errGeneric("Version not changed", .{}); + Global.exit(1); + } } { - const updated_contents = try updateVersionString(ctx.allocator, package_json_contents, current_version, new_version_str); - defer ctx.allocator.free(updated_contents); + try json.data.e_object.putString(ctx.allocator, "version", new_version_str); - const file = std.fs.cwd().openFile(package_json_path, .{ .mode = .write_only }) catch |err| { - Output.errGeneric("Failed to open package.json for writing: {s}", .{@errorName(err)}); + var buffer_writer = JSPrinter.BufferWriter.init(ctx.allocator); + buffer_writer.append_newline = package_json_contents.len > 0 and package_json_contents[package_json_contents.len - 1] == '\n'; + var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); + + _ = JSPrinter.printJSON( + @TypeOf(&package_json_writer), + &package_json_writer, + json, + &package_json_source, + .{ + .indent = json_result.indentation, + .mangled_props = null, + }, + ) catch |err| { + Output.errGeneric("Failed to save package.json: {s}", .{@errorName(err)}); Global.exit(1); }; - defer file.close(); - try file.seekTo(0); - try file.setEndPos(0); - try file.writeAll(updated_contents); + std.fs.cwd().writeFile(.{ + .sub_path = package_json_path, + .data = package_json_writer.ctx.writtenWithoutTrailingZero(), + }) catch |err| { + Output.errGeneric("Failed to write package.json: {s}", .{@errorName(err)}); + Global.exit(1); + }; } - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("version")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "version", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("version")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "version", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -147,22 +177,20 @@ pub const PmVersionCommand = struct { try gitCommitAndTag(ctx.allocator, new_version_str, pm.options.message, package_json_dir); } - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("postversion")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "postversion", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("postversion")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "postversion", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -201,7 +229,7 @@ pub const PmVersionCommand = struct { return; } - if (!try isGitClean(cwd) and !pm.options.force) { + if (!pm.options.force and !try isGitClean(cwd)) { Output.errGeneric("Git working directory not clean.", .{}); Global.exit(1); } @@ -256,6 +284,15 @@ pub const PmVersionCommand = struct { Output.prettyln("Current package version: v{s}", .{version}); } + const patch_version = try calculateNewVersion(ctx.allocator, current_version, .patch, null, pm.options.preid, cwd); + const minor_version = try calculateNewVersion(ctx.allocator, current_version, .minor, null, pm.options.preid, cwd); + const major_version = try calculateNewVersion(ctx.allocator, current_version, .major, null, pm.options.preid, cwd); + const prerelease_version = try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, pm.options.preid, cwd); + defer ctx.allocator.free(patch_version); + defer ctx.allocator.free(minor_version); + defer ctx.allocator.free(major_version); + defer ctx.allocator.free(prerelease_version); + const increment_help_text = \\ \\Increment: @@ -266,13 +303,20 @@ pub const PmVersionCommand = struct { \\ ; Output.pretty(increment_help_text, .{ - current_version, try calculateNewVersion(ctx.allocator, current_version, .patch, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .minor, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .major, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, pm.options.preid, cwd), + current_version, patch_version, + current_version, minor_version, + current_version, major_version, + current_version, prerelease_version, }); if (strings.indexOfChar(current_version, '-') != null or pm.options.preid.len > 0) { + const prepatch_version = try calculateNewVersion(ctx.allocator, current_version, .prepatch, null, pm.options.preid, cwd); + const preminor_version = try calculateNewVersion(ctx.allocator, current_version, .preminor, null, pm.options.preid, cwd); + const premajor_version = try calculateNewVersion(ctx.allocator, current_version, .premajor, null, pm.options.preid, cwd); + defer ctx.allocator.free(prepatch_version); + defer ctx.allocator.free(preminor_version); + defer ctx.allocator.free(premajor_version); + const prerelease_help_text = \\ prepatch {s} → {s} \\ preminor {s} → {s} @@ -280,12 +324,15 @@ pub const PmVersionCommand = struct { \\ ; Output.pretty(prerelease_help_text, .{ - current_version, try calculateNewVersion(ctx.allocator, current_version, .prepatch, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .preminor, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .premajor, null, pm.options.preid, cwd), + current_version, prepatch_version, + current_version, preminor_version, + current_version, premajor_version, }); } + const beta_prerelease_version = try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, "beta", cwd); + defer ctx.allocator.free(beta_prerelease_version); + const set_specific_version_help_text = \\ from-git Use version from latest git tag \\ 1.2.3 Set specific version @@ -293,78 +340,22 @@ pub const PmVersionCommand = struct { \\Options: \\ --no-git-tag-version Skip git operations \\ --allow-same-version Prevents throwing error if version is the same - \\ --message=\, -m Custom commit message - \\ --preid=\ Prerelease identifier + \\ --message=\, -m Custom commit message, use %s for version substitution + \\ --preid=\ Prerelease identifier (i.e beta → {s}) + \\ --force, -f Bypass dirty git history check \\ \\Examples: \\ $ bun pm version patch \\ $ bun pm version 1.2.3 --no-git-tag-version - \\ $ bun pm version prerelease --preid beta + \\ $ bun pm version prerelease --preid beta --message "Release beta: %s" \\ \\More info: https://bun.sh/docs/cli/pm#version \\ ; - Output.pretty(set_specific_version_help_text, .{}); + Output.pretty(set_specific_version_help_text, .{beta_prerelease_version}); Output.flush(); } - fn updateVersionString(allocator: std.mem.Allocator, contents: []const u8, old_version: []const u8, new_version: []const u8) ![]const u8 { - const version_key = "\"version\""; - - var search_start: usize = 0; - while (std.mem.indexOfPos(u8, contents, search_start, version_key)) |key_pos| { - var colon_pos = key_pos + version_key.len; - while (colon_pos < contents.len and (contents[colon_pos] == ' ' or contents[colon_pos] == '\t')) { - colon_pos += 1; - } - - if (colon_pos >= contents.len or contents[colon_pos] != ':') { - search_start = key_pos + 1; - continue; - } - - colon_pos += 1; - while (colon_pos < contents.len and (contents[colon_pos] == ' ' or contents[colon_pos] == '\t')) { - colon_pos += 1; - } - - if (colon_pos >= contents.len or contents[colon_pos] != '"') { - search_start = key_pos + 1; - continue; - } - - const value_start = colon_pos + 1; - - var value_end = value_start; - while (value_end < contents.len and contents[value_end] != '"') { - if (contents[value_end] == '\\' and value_end + 1 < contents.len) { - value_end += 2; - } else { - value_end += 1; - } - } - - if (value_end >= contents.len) { - search_start = key_pos + 1; - continue; - } - - const current_value = contents[value_start..value_end]; - if (strings.eql(current_value, old_version)) { - var result = std.ArrayList(u8).init(allocator); - try result.appendSlice(contents[0..value_start]); - try result.appendSlice(new_version); - try result.appendSlice(contents[value_end..]); - return result.toOwnedSlice(); - } - - search_start = value_end + 1; - } - - Output.errGeneric("Version not found in package.json", .{}); - Global.exit(1); - } - fn calculateNewVersion(allocator: std.mem.Allocator, current_str: []const u8, version_type: VersionType, specific_version: ?[]const u8, preid: []const u8, cwd: []const u8) bun.OOM![]const u8 { if (version_type == .specific) { return try allocator.dupe(u8, specific_version.?); @@ -487,12 +478,15 @@ pub const PmVersionCommand = struct { .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), }, - }) catch return false; + }) catch |err| { + Output.errGeneric("Failed to spawn git process: {s}", .{@errorName(err)}); + Global.exit(1); + }; switch (proc) { .err => |err| { Output.err(err, "Failed to spawn git process", .{}); - return false; + Global.exit(1); }, .result => |result| { return result.isOK() and result.stdout.items.len == 0; @@ -566,24 +560,27 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git add failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (stage_proc) { .err => |err| { Output.err(err, "Git add failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git add failed with exit code {d}", .{result.status.exited.code}); - return; + Global.exit(1); } }, } - const commit_message = custom_message orelse try std.fmt.allocPrint(allocator, "v{s}", .{version}); - defer if (custom_message == null) allocator.free(commit_message); + const commit_message = if (custom_message) |msg| + try std.mem.replaceOwned(u8, allocator, msg, "%s", version) + else + try std.fmt.allocPrint(allocator, "v{s}", .{version}); + defer allocator.free(commit_message); const commit_proc = bun.spawnSync(&.{ .argv = &.{ git_path, "commit", "-m", commit_message }, @@ -597,18 +594,18 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git commit failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (commit_proc) { .err => |err| { Output.err(err, "Git commit failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git commit failed", .{}); - return; + Global.exit(1); } }, } @@ -628,18 +625,18 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git tag failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (tag_proc) { .err => |err| { Output.err(err, "Git tag failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git tag failed", .{}); - return; + Global.exit(1); } }, } diff --git a/test/cli/install/bun-pm-version.test.ts b/test/cli/install/bun-pm-version.test.ts index 1734e8a0c3..6f37ad2a9e 100644 --- a/test/cli/install/bun-pm-version.test.ts +++ b/test/cli/install/bun-pm-version.test.ts @@ -107,681 +107,771 @@ describe("bun pm version", () => { return { output, error, code }; } - it("should show help when no arguments provided", async () => { - const testDir = setupTest(); + describe("help and version previews", () => { + it("should show help when no arguments provided", async () => { + const testDir = setupTest(); - const { output, code } = await runCommand([bunExe(), "pm", "version"], testDir); + const { output, code } = await runCommand([bunExe(), "pm", "version"], testDir); - expect(code).toBe(0); - expect(output).toContain("bun pm version"); - expect(output).toContain("Current package version: v1.0.0"); - expect(output).toContain("patch"); - expect(output).toContain("minor"); - expect(output).toContain("major"); + expect(code).toBe(0); + expect(output).toContain("bun pm version"); + expect(output).toContain("Current package version: v1.0.0"); + expect(output).toContain("patch"); + expect(output).toContain("minor"); + expect(output).toContain("major"); + }); + + it("shows help with version previews", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "2.5.3" }, null, 2), + }); + + const { output: output1, code: code1 } = await runCommand([bunExe(), "pm", "version"], testDir1); + + expect(code1).toBe(0); + expect(output1).toContain("Current package version: v2.5.3"); + expect(output1).toContain("patch 2.5.3 → 2.5.4"); + expect(output1).toContain("minor 2.5.3 → 2.6.0"); + expect(output1).toContain("major 2.5.3 → 3.0.0"); + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha.0" }, null, 2), + }); + + const { output: output2, code: code2 } = await runCommand([bunExe(), "pm", "version"], testDir2); + + expect(code2).toBe(0); + expect(output2).toContain("prepatch"); + expect(output2).toContain("preminor"); + expect(output2).toContain("premajor"); + expect(output2).toContain("1.0.1-alpha.0"); + expect(output2).toContain("1.1.0-alpha.0"); + expect(output2).toContain("2.0.0-alpha.0"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "--preid", "beta"], + testDir3, + ); + + expect(code3).toBe(0); + expect(output3).toContain("prepatch"); + expect(output3).toContain("preminor"); + expect(output3).toContain("premajor"); + expect(output3).toContain("1.0.1-beta.0"); + expect(output3).toContain("1.1.0-beta.0"); + expect(output3).toContain("2.0.0-beta.0"); + + const testDir4 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test" }, null, 2), + }); + + const { output: output4 } = await runCommand([bunExe(), "pm", "version"], testDir4); + + expect(output4).not.toContain("Current package version:"); + expect(output4).toContain("patch 1.0.0 → 1.0.1"); + }); }); - it("should increment versions correctly", async () => { - const testDir = setupTest(); + describe("basic version incrementing", () => { + it("should increment versions correctly", async () => { + const testDir = setupTest(); - const { output: patchOutput, code: patchCode } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir, - ); - expect(patchCode).toBe(0); - expect(patchOutput.trim()).toBe("v1.0.1"); + const { output: patchOutput, code: patchCode } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + ); + expect(patchCode).toBe(0); + expect(patchOutput.trim()).toBe("v1.0.1"); - const { output: minorOutput, code: minorCode } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - testDir, - ); - expect(minorCode).toBe(0); - expect(minorOutput.trim()).toBe("v1.1.0"); + const { output: minorOutput, code: minorCode } = await runCommand( + [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], + testDir, + ); + expect(minorCode).toBe(0); + expect(minorOutput.trim()).toBe("v1.1.0"); - const { output: majorOutput, code: majorCode } = await runCommand( - [bunExe(), "pm", "version", "major", "--no-git-tag-version"], - testDir, - ); - expect(majorCode).toBe(0); - expect(majorOutput.trim()).toBe("v2.0.0"); + const { output: majorOutput, code: majorCode } = await runCommand( + [bunExe(), "pm", "version", "major", "--no-git-tag-version"], + testDir, + ); + expect(majorCode).toBe(0); + expect(majorOutput.trim()).toBe("v2.0.0"); - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("2.0.0"); - }); - - it("should set specific version", async () => { - const testDir = setupTest(); - - const { output, code } = await runCommand([bunExe(), "pm", "version", "3.2.1", "--no-git-tag-version"], testDir); - - expect(code).toBe(0); - expect(output.trim()).toBe("v3.2.1"); - - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("3.2.1"); - }); - - it("handles various error conditions", async () => { - const testDir1 = setupTest(); - await Bun.write(`${testDir1}/package.json`, ""); - - const { error: error1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir1, - false, - ); - expect(code1).toBe(1); - expect(error1).toContain("No version field found in package.json"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "invalid-version" }, null, 2), + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("2.0.0"); }); - const { error: error2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir2, - false, - ); - expect(code2).toBe(1); - expect(error2).toContain("is not a valid semver"); + it("should set specific version", async () => { + const testDir = setupTest(); - const testDir3 = setupTest(); + const { output, code } = await runCommand([bunExe(), "pm", "version", "3.2.1", "--no-git-tag-version"], testDir); - const { error: error3, code: code3 } = await runCommand( - [bunExe(), "pm", "version", "invalid-arg", "--no-git-tag-version"], - testDir3, - false, - ); - expect(code3).toBe(1); - expect(error3).toContain("Invalid version argument"); + expect(code).toBe(0); + expect(output.trim()).toBe("v3.2.1"); - const testDir4 = setupTest(); - - const { error: error4, code: code4 } = await runCommand( - [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version"], - testDir4, - false, - ); - expect(code4).toBe(1); - expect(error4).toContain("Version not changed"); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version", "--allow-same-version"], - testDir4, - ); - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.0"); - }); - - it("handles git operations correctly", async () => { - const testDir1 = setupGitTest(); - - const { - output: output1, - code: code1, - error: stderr1, - } = await runCommand([bunExe(), "pm", "version", "patch"], testDir1); - - expect(stderr1.trim()).toBe(""); - expect(output1.trim()).toBe("v1.0.1"); - expect(code1).toBe(0); - - const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir1); - expect(tagOutput).toContain("v1.0.1"); - - const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir1); - expect(logOutput).toContain("v1.0.1"); - - const testDir2 = setupGitTest(); - - const { - output: output2, - error: error2, - code: code2, - } = await runCommand([bunExe(), "pm", "version", "patch", "--message", "Custom release message"], testDir2); - expect(error2).toBe(""); - - const { output: gitLogOutput } = await runCommand(["git", "log", "--oneline"], testDir2); - expect(gitLogOutput).toContain("Custom release message"); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v1.0.1"); - - const testDir3 = setupGitTest(); - - await Bun.write(join(testDir3, "untracked.txt"), "untracked content"); - - const { error: error3, code: code3 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir3, false); - - expect(code3).toBe(1); - expect(error3).toContain("Git working directory not clean"); - - const testDir4 = setupTest(); - - const { output: output4, code: code4 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir4); - - expect(code4).toBe(0); - expect(output4.trim()).toBe("v1.0.1"); - - const packageJson = await Bun.file(`${testDir4}/package.json`).json(); - expect(packageJson.version).toBe("1.0.1"); - - const testDir5 = setupGitTest(); - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.1"); - - const packageJson5 = await Bun.file(`${testDir5}/package.json`).json(); - expect(packageJson5.version).toBe("1.0.1"); - - const { output: tagOutput5 } = await runCommand(["git", "tag", "-l"], testDir5); - expect(tagOutput5.trim()).toBe(""); - - const { output: logOutput5 } = await runCommand(["git", "log", "--oneline"], testDir5); - expect(logOutput5).toContain("Initial commit"); - expect(logOutput5).not.toContain("v1.0.1"); - - const testDir6 = setupGitTest(); - const { output: output6, code: code6 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--git-tag-version=false"], - testDir6, - ); - - expect(code6).toBe(0); - expect(output6.trim()).toBe("v1.0.1"); - - const packageJson6 = await Bun.file(`${testDir6}/package.json`).json(); - expect(packageJson6.version).toBe("1.0.1"); - - const { output: tagOutput6 } = await runCommand(["git", "tag", "-l"], testDir6); - expect(tagOutput6.trim()).toBe(""); - - const { output: logOutput6 } = await runCommand(["git", "log", "--oneline"], testDir6); - expect(logOutput6).toContain("Initial commit"); - expect(logOutput6).not.toContain("v1.0.1"); - - const testDir7 = setupGitTest(); - const { output: output7, code: code7 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--git-tag-version=true"], - testDir7, - ); - - expect(code7).toBe(0); - expect(output7.trim()).toBe("v1.0.1"); - - const packageJson7 = await Bun.file(`${testDir7}/package.json`).json(); - expect(packageJson7.version).toBe("1.0.1"); - - const { output: tagOutput7 } = await runCommand(["git", "tag", "-l"], testDir7); - expect(tagOutput7).toContain("v1.0.1"); - - const { output: logOutput7 } = await runCommand(["git", "log", "--oneline"], testDir7); - expect(logOutput7).toContain("v1.0.1"); - }); - - it("preserves JSON formatting correctly", async () => { - const originalJson1 = `{ - "name": "test", - "version": "1.0.0", - "scripts": { - "test": "echo test" - }, - "dependencies": { - "lodash": "^4.17.21" - } -}`; - - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": originalJson1, + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("3.2.1"); }); - const { output: output1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir1, - ); - - expect(code1).toBe(0); - expect(output1.trim()).toBe("v1.0.1"); - - const updatedJson1 = await Bun.file(`${testDir1}/package.json`).text(); - - expect(updatedJson1).toContain('"version": "1.0.1"'); - expect(updatedJson1).toContain('"name": "test"'); - expect(updatedJson1).toContain(' "test": "echo test"'); - - const originalJson2 = `{ - "name": "test-package", - "version" : "2.5.0" , - "description": "A test package with weird formatting", - "main": "index.js", - "scripts":{ - "test":"npm test", - "build" : "webpack" - }, -"keywords":["test","package"], - "author": "Test Author" -}`; - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": originalJson2, - }); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - testDir2, - ); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v2.6.0"); - - const updatedJson2 = await Bun.file(`${testDir2}/package.json`).text(); - - expect(updatedJson2).toContain('"version" : "2.6.0" ,'); - expect(updatedJson2).toContain('"name": "test-package"'); - expect(updatedJson2).toContain('"main": "index.js"'); - expect(updatedJson2).toContain('"scripts":{'); - expect(updatedJson2).toContain('"build" : "webpack"'); - - const originalLines1 = originalJson1.split("\n"); - const updatedLines1 = updatedJson1.split("\n"); - expect(updatedLines1.length).toBe(originalLines1.length); - - const originalLines2 = originalJson2.split("\n"); - const updatedLines2 = updatedJson2.split("\n"); - expect(updatedLines2.length).toBe(originalLines2.length); - }); - - it("shows help with version previews", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "2.5.3" }, null, 2), - }); - - const { output: output1, code: code1 } = await runCommand([bunExe(), "pm", "version"], testDir1); - - expect(code1).toBe(0); - expect(output1).toContain("Current package version: v2.5.3"); - expect(output1).toContain("patch 2.5.3 → 2.5.4"); - expect(output1).toContain("minor 2.5.3 → 2.6.0"); - expect(output1).toContain("major 2.5.3 → 3.0.0"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha.0" }, null, 2), - }); - - const { output: output2, code: code2 } = await runCommand([bunExe(), "pm", "version"], testDir2); - - expect(code2).toBe(0); - expect(output2).toContain("prepatch"); - expect(output2).toContain("preminor"); - expect(output2).toContain("premajor"); - expect(output2).toContain("1.0.1-alpha.0"); - expect(output2).toContain("1.1.0-alpha.0"); - expect(output2).toContain("2.0.0-alpha.0"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output3, code: code3 } = await runCommand([bunExe(), "pm", "version", "--preid", "beta"], testDir3); - - expect(code3).toBe(0); - expect(output3).toContain("prepatch"); - expect(output3).toContain("preminor"); - expect(output3).toContain("premajor"); - expect(output3).toContain("1.0.1-beta.0"); - expect(output3).toContain("1.1.0-beta.0"); - expect(output3).toContain("2.0.0-beta.0"); - - const testDir4 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test" }, null, 2), - }); - - const { output: output4 } = await runCommand([bunExe(), "pm", "version"], testDir4); - - expect(output4).not.toContain("Current package version:"); - expect(output4).toContain("patch 1.0.0 → 1.0.1"); - }); - - it("handles custom preid and prerelease scenarios", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--preid", "beta", "--no-git-tag-version"], - testDir1, - ); - - expect(code1).toBe(0); - expect(output1.trim()).toBe("v1.0.1-beta.0"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output3, code: code3 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir3, - ); - - expect(code3).toBe(0); - expect(output3.trim()).toBe("v1.0.1-0"); - - const testDir5 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha" }, null, 2), - }); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.0-alpha.1"); - - const testDir6 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-3" }, null, 2), - }); - - const { output: output6, code: code6 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir6, - ); - - expect(code6).toBe(0); - expect(output6.trim()).toBe("v1.0.0-4"); - }); - - it("runs lifecycle scripts in correct order and handles failures", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo 'step1' >> lifecycle.log", - version: "echo 'step2' >> lifecycle.log", - postversion: "echo 'step3' >> lifecycle.log", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir1, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(await Bun.file(join(testDir1, "lifecycle.log")).exists()).toBe(true); - const logContent = await Bun.file(join(testDir1, "lifecycle.log")).text(); - expect(logContent.trim()).toBe("step1\nstep2\nstep3"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo $npm_lifecycle_event > event.log && echo $npm_lifecycle_script > script.log", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir2, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); - - const eventContent = await Bun.file(join(testDir2, "event.log")).text(); - const scriptContent = await Bun.file(join(testDir2, "script.log")).text(); - - expect(eventContent.trim()).toBe("preversion"); - expect(scriptContent.trim()).toContain("echo $npm_lifecycle_event"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "exit 1", - }, - }, - null, - 2, - ), - }); - - const proc = Bun.spawn([bunExe(), "pm", "version", "minor", "--no-git-tag-version"], { - cwd: testDir3, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }); - - await proc.exited; - expect(proc.exitCode).toBe(1); - - const packageJson = await Bun.file(join(testDir3, "package.json")).json(); - expect(packageJson.version).toBe("1.0.0"); - - const testDir4 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "mkdir -p build && echo 'built' > build/output.txt", - version: "cp build/output.txt version-output.txt", - postversion: "rm -rf build", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir4, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); - - const content = await Bun.file(join(testDir4, "version-output.txt")).text(); - expect(content.trim()).toBe("built"); - - const testDir5 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo 'should not run' >> ignored.log", - version: "echo 'should not run' >> ignored.log", - postversion: "echo 'should not run' >> ignored.log", - }, - }, - null, - 2, - ), - }); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version", "--ignore-scripts"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.1"); - - const packageJson5 = await Bun.file(join(testDir5, "package.json")).json(); - expect(packageJson5.version).toBe("1.0.1"); - - expect(await Bun.file(join(testDir5, "ignored.log")).exists()).toBe(false); - }); - - it("should version workspace packages individually", async () => { - const testDir = setupMonorepoTest(); - - const { output: outputA, code: codeA } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - join(testDir, "packages", "pkg-a"), - ); - - expect(codeA).toBe(0); - expect(outputA.trim()).toBe("v2.0.1"); - - const rootPackageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(rootPackageJson.version).toBe("1.0.0"); - - const pkgAJson = await Bun.file(`${testDir}/packages/pkg-a/package.json`).json(); - const pkgBJson = await Bun.file(`${testDir}/packages/pkg-b/package.json`).json(); - - expect(pkgAJson.version).toBe("2.0.1"); - expect(pkgBJson.version).toBe("3.0.0"); - }); - - it("should work from subdirectories", async () => { - const testDir = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - "src/index.js": "console.log('hello');", - }); - - const { output, code } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - join(testDir, "src"), - ); - - expect(code).toBe(0); - expect(output.trim()).toBe("v1.0.1"); - - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("1.0.1"); - - const monorepoDir = setupMonorepoTest(); - - await Bun.write(join(monorepoDir, "packages", "pkg-a", "lib", "index.js"), ""); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - join(monorepoDir, "packages", "pkg-a", "lib"), - ); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v2.1.0"); - - const rootJson = await Bun.file(`${monorepoDir}/package.json`).json(); - const pkgAJson = await Bun.file(`${monorepoDir}/packages/pkg-a/package.json`).json(); - const pkgBJson = await Bun.file(`${monorepoDir}/packages/pkg-b/package.json`).json(); - - expect(rootJson.version).toBe("1.0.0"); - expect(pkgAJson.version).toBe("2.1.0"); - expect(pkgBJson.version).toBe("3.0.0"); - }); - - it("should preserve prerelease identifiers correctly", async () => { - const scenarios = [ - { - version: "1.0.3-alpha.1", - preid: "beta", - expected: { - patch: "1.0.3-alpha.1 → 1.0.4", - minor: "1.0.3-alpha.1 → 1.1.0", - major: "1.0.3-alpha.1 → 2.0.0", - prerelease: "1.0.3-alpha.1 → 1.0.3-beta.2", - prepatch: "1.0.3-alpha.1 → 1.0.4-beta.0", - preminor: "1.0.3-alpha.1 → 1.1.0-beta.0", - premajor: "1.0.3-alpha.1 → 2.0.0-beta.0", - }, - }, - { - version: "1.0.3-1", - preid: "abcd", - expected: { - patch: "1.0.3-1 → 1.0.4", - minor: "1.0.3-1 → 1.1.0", - major: "1.0.3-1 → 2.0.0", - prerelease: "1.0.3-1 → 1.0.3-abcd.2", - prepatch: "1.0.3-1 → 1.0.4-abcd.0", - preminor: "1.0.3-1 → 1.1.0-abcd.0", - premajor: "1.0.3-1 → 2.0.0-abcd.0", - }, - }, - { - version: "2.5.0-rc.3", - preid: "next", - expected: { - patch: "2.5.0-rc.3 → 2.5.1", - minor: "2.5.0-rc.3 → 2.6.0", - major: "2.5.0-rc.3 → 3.0.0", - prerelease: "2.5.0-rc.3 → 2.5.0-next.4", - prepatch: "2.5.0-rc.3 → 2.5.1-next.0", - preminor: "2.5.0-rc.3 → 2.6.0-next.0", - premajor: "2.5.0-rc.3 → 3.0.0-next.0", - }, - }, - { - version: "1.0.0-a", - preid: "b", - expected: { - patch: "1.0.0-a → 1.0.1", - minor: "1.0.0-a → 1.1.0", - major: "1.0.0-a → 2.0.0", - prerelease: "1.0.0-a → 1.0.0-b.1", - prepatch: "1.0.0-a → 1.0.1-b.0", - preminor: "1.0.0-a → 1.1.0-b.0", - premajor: "1.0.0-a → 2.0.0-b.0", - }, - }, - ]; - - for (const scenario of scenarios) { + it("handles empty package.json", async () => { const testDir = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: scenario.version }, null, 2), + "package.json": "{}", + }); + + const { output, code } = await runCommand([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], testDir); + + expect(code).toBe(0); + expect(output.trim()).toBe("v0.0.1"); + + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("0.0.1"); + }); + }); + + describe("error handling", () => { + it("handles various error conditions", async () => { + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "invalid-version" }, null, 2), + }); + + const { error: error2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir2, + false, + ); + expect(error2).toContain("is not a valid semver"); + expect(code2).toBe(1); + + const testDir3 = setupTest(); + + const { error: error3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "invalid-arg", "--no-git-tag-version"], + testDir3, + false, + ); + expect(error3).toContain("Invalid version argument"); + expect(code3).toBe(1); + + const testDir4 = setupTest(); + + const { error: error4, code: code4 } = await runCommand( + [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version"], + testDir4, + false, + ); + expect(error4).toContain("Version not changed"); + expect(code4).toBe(1); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version", "--allow-same-version"], + testDir4, + ); + expect(output5.trim()).toBe("v1.0.0"); + expect(code5).toBe(0); + }); + + it("handles missing package.json like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "README.md": "# Test project", + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("package.json"); + expect(code).toBe(1); + // its an ealier check that "bun pm *" commands do so not "bun pm version" specific + // expect(error.includes("ENOENT") || error.includes("no such file")).toBe(true); + }); + + it("handles empty string package.json like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": '""', + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("Failed to parse package.json"); + expect(code).toBe(1); + }); + + it("handles malformed JSON like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": '{ "name": "test", invalid json }', + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("Failed to parse package.json"); + expect(code).toBe(1); + }); + }); + + describe("git integration", () => { + it("creates git commits and tags by default", async () => { + const testDir1 = setupGitTest(); + + const { + output: output1, + code: code1, + error: stderr1, + } = await runCommand([bunExe(), "pm", "version", "patch"], testDir1); + + expect(stderr1.trim()).toBe(""); + expect(output1.trim()).toBe("v1.0.1"); + expect(code1).toBe(0); + + const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir1); + expect(tagOutput).toContain("v1.0.1"); + + const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir1); + expect(logOutput).toContain("v1.0.1"); + }); + + it("supports custom commit messages", async () => { + const testDir2 = setupGitTest(); + + const { + output: output2, + error: error2, + code: code2, + } = await runCommand([bunExe(), "pm", "version", "patch", "--message", "Custom release message"], testDir2); + expect(error2).toBe(""); + + const { output: gitLogOutput } = await runCommand(["git", "log", "--oneline"], testDir2); + expect(gitLogOutput).toContain("Custom release message"); + + expect(code2).toBe(0); + expect(output2.trim()).toBe("v1.0.1"); + }); + + it("fails when git working directory is not clean", async () => { + const testDir3 = setupGitTest(); + + await Bun.write(join(testDir3, "untracked.txt"), "untracked content"); + + const { error: error3, code: code3 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir3, false); + + expect(error3).toContain("Git working directory not clean"); + expect(code3).toBe(1); + }); + + it("allows dirty working directory with --force flag", async () => { + const testDir = setupGitTest(); + + await Bun.write(join(testDir, "untracked.txt"), "untracked content"); + + const { output, code, error } = await runCommand([bunExe(), "pm", "version", "patch", "--force"], testDir); + + expect(code).toBe(0); + expect(error.trim()).toBe(""); + expect(output.trim()).toBe("v1.0.1"); + + const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir); + expect(tagOutput).toContain("v1.0.1"); + + const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir); + expect(logOutput).toContain("v1.0.1"); + }); + + it("works without git when no repo is present", async () => { + const testDir4 = setupTest(); + + const { output: output4, code: code4 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir4); + + expect(code4).toBe(0); + expect(output4.trim()).toBe("v1.0.1"); + + const packageJson = await Bun.file(`${testDir4}/package.json`).json(); + expect(packageJson.version).toBe("1.0.1"); + }); + + it("respects --no-git-tag-version flag", async () => { + const testDir5 = setupGitTest(); + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.1"); + + const packageJson5 = await Bun.file(`${testDir5}/package.json`).json(); + expect(packageJson5.version).toBe("1.0.1"); + + const { output: tagOutput5 } = await runCommand(["git", "tag", "-l"], testDir5); + expect(tagOutput5.trim()).toBe(""); + + const { output: logOutput5 } = await runCommand(["git", "log", "--oneline"], testDir5); + expect(logOutput5).toContain("Initial commit"); + expect(logOutput5).not.toContain("v1.0.1"); + }); + + it("respects --git-tag-version=false flag", async () => { + const testDir6 = setupGitTest(); + const { output: output6, code: code6 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--git-tag-version=false"], + testDir6, + ); + + expect(code6).toBe(0); + expect(output6.trim()).toBe("v1.0.1"); + + const packageJson6 = await Bun.file(`${testDir6}/package.json`).json(); + expect(packageJson6.version).toBe("1.0.1"); + + const { output: tagOutput6 } = await runCommand(["git", "tag", "-l"], testDir6); + expect(tagOutput6.trim()).toBe(""); + + const { output: logOutput6 } = await runCommand(["git", "log", "--oneline"], testDir6); + expect(logOutput6).toContain("Initial commit"); + expect(logOutput6).not.toContain("v1.0.1"); + }); + + it("respects --git-tag-version=true flag", async () => { + const testDir7 = setupGitTest(); + const { output: output7, code: code7 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--git-tag-version=true"], + testDir7, + ); + + expect(code7).toBe(0); + expect(output7.trim()).toBe("v1.0.1"); + + const packageJson7 = await Bun.file(`${testDir7}/package.json`).json(); + expect(packageJson7.version).toBe("1.0.1"); + + const { output: tagOutput7 } = await runCommand(["git", "tag", "-l"], testDir7); + expect(tagOutput7).toContain("v1.0.1"); + + const { output: logOutput7 } = await runCommand(["git", "log", "--oneline"], testDir7); + expect(logOutput7).toContain("v1.0.1"); + }); + + it("supports %s substitution in commit messages", async () => { + const testDir8 = setupGitTest(); + const { output: output8, code: code8 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--message", "Bump version to %s"], + testDir8, + ); + + expect(code8).toBe(0); + expect(output8.trim()).toBe("v1.0.1"); + + const { output: logOutput8 } = await runCommand(["git", "log", "--oneline", "-1"], testDir8); + expect(logOutput8).toContain("Bump version to 1.0.1"); + + const testDir9 = setupGitTest(); + const { output: output9, code: code9 } = await runCommand( + [bunExe(), "pm", "version", "2.5.0", "-m", "Release %s with fixes"], + testDir9, + ); + + expect(code9).toBe(0); + expect(output9.trim()).toBe("v2.5.0"); + + const { output: logOutput9 } = await runCommand(["git", "log", "--oneline", "-1"], testDir9); + expect(logOutput9).toContain("Release 2.5.0 with fixes"); + }); + }); + + describe("JSON formatting preservation", () => { + it("preserves JSON formatting correctly", async () => { + const originalJson1 = `{ + "name": "test", + "version": "1.0.0", + "scripts": { + "test": "echo test" + }, + "dependencies": { + "lodash": "^4.17.21" + } + }`; + + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": originalJson1, + }); + + const { output: output1, code: code1 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir1, + ); + + expect(code1).toBe(0); + expect(output1.trim()).toBe("v1.0.1"); + + const updatedJson1 = await Bun.file(`${testDir1}/package.json`).text(); + + expect(updatedJson1).toContain(' "version": "1.0.1"'); + expect(updatedJson1).toContain('"name": "test"'); + expect(updatedJson1).toContain(' "test": "echo test"'); + + expect(JSON.parse(updatedJson1)).toMatchObject({ + name: "test", + version: "1.0.1", + scripts: { + test: "echo test", + }, + }); + }); + }); + + describe("prerelease handling", () => { + it("handles custom preid and prerelease scenarios", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output1, code: code1 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--preid", "beta", "--no-git-tag-version"], + testDir1, + ); + + expect(code1).toBe(0); + expect(output1.trim()).toBe("v1.0.1-beta.0"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir3, + ); + + expect(code3).toBe(0); + expect(output3.trim()).toBe("v1.0.1-0"); + + const testDir5 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha" }, null, 2), + }); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.0-alpha.1"); + + const testDir6 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-3" }, null, 2), + }); + + const { output: output6, code: code6 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir6, + ); + + expect(code6).toBe(0); + expect(output6.trim()).toBe("v1.0.0-4"); + }); + + it("should preserve prerelease identifiers correctly", async () => { + const scenarios = [ + { + version: "1.0.3-alpha.1", + preid: "beta", + expected: { + patch: "1.0.3-alpha.1 → 1.0.4", + minor: "1.0.3-alpha.1 → 1.1.0", + major: "1.0.3-alpha.1 → 2.0.0", + prerelease: "1.0.3-alpha.1 → 1.0.3-beta.2", + prepatch: "1.0.3-alpha.1 → 1.0.4-beta.0", + preminor: "1.0.3-alpha.1 → 1.1.0-beta.0", + premajor: "1.0.3-alpha.1 → 2.0.0-beta.0", + }, + }, + { + version: "1.0.3-1", + preid: "abcd", + expected: { + patch: "1.0.3-1 → 1.0.4", + minor: "1.0.3-1 → 1.1.0", + major: "1.0.3-1 → 2.0.0", + prerelease: "1.0.3-1 → 1.0.3-abcd.2", + prepatch: "1.0.3-1 → 1.0.4-abcd.0", + preminor: "1.0.3-1 → 1.1.0-abcd.0", + premajor: "1.0.3-1 → 2.0.0-abcd.0", + }, + }, + { + version: "2.5.0-rc.3", + preid: "next", + expected: { + patch: "2.5.0-rc.3 → 2.5.1", + minor: "2.5.0-rc.3 → 2.6.0", + major: "2.5.0-rc.3 → 3.0.0", + prerelease: "2.5.0-rc.3 → 2.5.0-next.4", + prepatch: "2.5.0-rc.3 → 2.5.1-next.0", + preminor: "2.5.0-rc.3 → 2.6.0-next.0", + premajor: "2.5.0-rc.3 → 3.0.0-next.0", + }, + }, + { + version: "1.0.0-a", + preid: "b", + expected: { + patch: "1.0.0-a → 1.0.1", + minor: "1.0.0-a → 1.1.0", + major: "1.0.0-a → 2.0.0", + prerelease: "1.0.0-a → 1.0.0-b.1", + prepatch: "1.0.0-a → 1.0.1-b.0", + preminor: "1.0.0-a → 1.1.0-b.0", + premajor: "1.0.0-a → 2.0.0-b.0", + }, + }, + ]; + + for (const scenario of scenarios) { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: scenario.version }, null, 2), + }); + + const { output, code } = await runCommand( + [bunExe(), "pm", "version", "--no-git-tag-version", `--preid=${scenario.preid}`], + testDir, + ); + + expect(code).toBe(0); + expect(output).toContain(`Current package version: v${scenario.version}`); + + for (const [incrementType, expectedTransformation] of Object.entries(scenario.expected)) { + expect(output).toContain(`${incrementType.padEnd(10)} ${expectedTransformation}`); + } + } + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.3-alpha.1" }, null, 2), + }); + + const { output: output2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "--no-git-tag-version"], + testDir2, + ); + + expect(code2).toBe(0); + expect(output2).toContain("prerelease 1.0.3-alpha.1 → 1.0.3-alpha.2"); + }); + }); + + describe("lifecycle scripts", () => { + it("runs lifecycle scripts in correct order and handles failures", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo 'step1' >> lifecycle.log", + version: "echo 'step2' >> lifecycle.log", + postversion: "echo 'step3' >> lifecycle.log", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir1, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(await Bun.file(join(testDir1, "lifecycle.log")).exists()).toBe(true); + const logContent = await Bun.file(join(testDir1, "lifecycle.log")).text(); + expect(logContent.trim()).toBe("step1\nstep2\nstep3"); + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo $npm_lifecycle_event > event.log && echo $npm_lifecycle_script > script.log", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir2, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); + expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); + + const eventContent = await Bun.file(join(testDir2, "event.log")).text(); + const scriptContent = await Bun.file(join(testDir2, "script.log")).text(); + + expect(eventContent.trim()).toBe("preversion"); + expect(scriptContent.trim()).toContain("echo $npm_lifecycle_event"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "exit 1", + }, + }, + null, + 2, + ), + }); + + const proc = Bun.spawn([bunExe(), "pm", "version", "minor", "--no-git-tag-version"], { + cwd: testDir3, + env: bunEnv, + stderr: "pipe", + stdout: "ignore", + }); + + await proc.exited; + expect(proc.exitCode).toBe(1); + expect(await proc.stderr.text()).toContain('script "preversion" exited with code 1'); + + const packageJson = await Bun.file(join(testDir3, "package.json")).json(); + expect(packageJson.version).toBe("1.0.0"); + + const testDir4 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "mkdir -p build && echo 'built' > build/output.txt", + version: "cp build/output.txt version-output.txt", + postversion: "rm -rf build", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir4, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); + expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); + + const content = await Bun.file(join(testDir4, "version-output.txt")).text(); + expect(content.trim()).toBe("built"); + + const testDir5 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo 'should not run' >> ignored.log", + version: "echo 'should not run' >> ignored.log", + postversion: "echo 'should not run' >> ignored.log", + }, + }, + null, + 2, + ), + }); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version", "--ignore-scripts"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.1"); + + const packageJson5 = await Bun.file(join(testDir5, "package.json")).json(); + expect(packageJson5.version).toBe("1.0.1"); + + expect(await Bun.file(join(testDir5, "ignored.log")).exists()).toBe(false); + }); + }); + + describe("workspace and directory handling", () => { + it("should version workspace packages individually", async () => { + const testDir = setupMonorepoTest(); + + const { output: outputA, code: codeA } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + join(testDir, "packages", "pkg-a"), + ); + + expect(codeA).toBe(0); + expect(outputA.trim()).toBe("v2.0.1"); + + const rootPackageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(rootPackageJson.version).toBe("1.0.0"); + + const pkgAJson = await Bun.file(`${testDir}/packages/pkg-a/package.json`).json(); + const pkgBJson = await Bun.file(`${testDir}/packages/pkg-b/package.json`).json(); + + expect(pkgAJson.version).toBe("2.0.1"); + expect(pkgBJson.version).toBe("3.0.0"); + }); + + it("should work from subdirectories", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + "src/index.js": "console.log('hello');", }); const { output, code } = await runCommand( - [bunExe(), "pm", "version", "--no-git-tag-version", `--preid=${scenario.preid}`], - testDir, + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + join(testDir, "src"), ); expect(code).toBe(0); - expect(output).toContain(`Current package version: v${scenario.version}`); + expect(output.trim()).toBe("v1.0.1"); - for (const [incrementType, expectedTransformation] of Object.entries(scenario.expected)) { - expect(output).toContain(`${incrementType.padEnd(10)} ${expectedTransformation}`); - } - } + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("1.0.1"); - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.3-alpha.1" }, null, 2), + const monorepoDir = setupMonorepoTest(); + + await Bun.write(join(monorepoDir, "packages", "pkg-a", "lib", "index.js"), ""); + + const { output: output2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], + join(monorepoDir, "packages", "pkg-a", "lib"), + ); + + expect(code2).toBe(0); + expect(output2.trim()).toBe("v2.1.0"); + + const rootJson = await Bun.file(`${monorepoDir}/package.json`).json(); + const pkgAJson = await Bun.file(`${monorepoDir}/packages/pkg-a/package.json`).json(); + const pkgBJson = await Bun.file(`${monorepoDir}/packages/pkg-b/package.json`).json(); + + expect(rootJson.version).toBe("1.0.0"); + expect(pkgAJson.version).toBe("2.1.0"); + expect(pkgBJson.version).toBe("3.0.0"); }); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "--no-git-tag-version"], - testDir2, - ); - - expect(code2).toBe(0); - expect(output2).toContain("prerelease 1.0.3-alpha.1 → 1.0.3-alpha.2"); }); }); From b1a0502c0cb98ce841af1c08b8f9c08d775c58fa Mon Sep 17 00:00:00 2001 From: 190n Date: Mon, 7 Jul 2025 16:16:47 -0700 Subject: [PATCH 129/147] [publish images] fix v8.test.ts timeouts (#20871) --- scripts/bootstrap.sh | 70 ++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 14b26452f2..2ac531ef5f 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Version: 12 +# Version: 13 # A script that installs the dependencies needed to build and test Bun. # This should work on macOS and Linux with a POSIX shell. @@ -741,13 +741,8 @@ install_nodejs() { ;; esac - # Some distros do not install the node headers by default. - # These are needed for certain FFI tests, such as: `cc.test.ts` - case "$distro" in - alpine | amzn) - install_nodejs_headers - ;; - esac + # Ensure that Node.js headers are always pre-downloaded so that we don't rely on node-gyp + install_nodejs_headers } install_nodejs_headers() { @@ -768,47 +763,34 @@ setup_node_gyp_cache() { nodejs_version="$1" headers_source="$2" - # Common node-gyp cache locations - cache_locations=" - $HOME/.node-gyp/$nodejs_version - $HOME/.cache/node-gyp/$nodejs_version - $HOME/.npm/_cacache/node-gyp/$nodejs_version - $current_home/.node-gyp/$nodejs_version - $current_home/.cache/node-gyp/$nodejs_version - " + cache_dir="$home/.cache/node-gyp/$nodejs_version" - for cache_dir in $cache_locations; do - if ! [ -z "$cache_dir" ]; then - create_directory "$cache_dir" + create_directory "$cache_dir" - # Copy headers - if [ -d "$headers_source/include" ]; then - cp -R "$headers_source/include" "$cache_dir/" 2>/dev/null || true - fi + # Copy headers + if [ -d "$headers_source/include" ]; then + cp -R "$headers_source/include" "$cache_dir/" 2>/dev/null || true + fi - # Create installVersion file (node-gyp expects this) - echo "11" > "$cache_dir/installVersion" 2>/dev/null || true + # Create installVersion file (node-gyp expects this) + echo "11" > "$cache_dir/installVersion" 2>/dev/null || true - # For Linux, we don't need .lib files like Windows - # but create the directory structure node-gyp expects - case "$arch" in - x86_64|amd64) - create_directory "$cache_dir/lib/x64" 2>/dev/null || true - ;; - aarch64|arm64) - create_directory "$cache_dir/lib/arm64" 2>/dev/null || true - ;; - *) - create_directory "$cache_dir/lib" 2>/dev/null || true - ;; - esac + # For Linux, we don't need .lib files like Windows + # but create the directory structure node-gyp expects + case "$arch" in + x86_64|amd64) + create_directory "$cache_dir/lib/x64" 2>/dev/null || true + ;; + aarch64|arm64) + create_directory "$cache_dir/lib/arm64" 2>/dev/null || true + ;; + *) + create_directory "$cache_dir/lib" 2>/dev/null || true + ;; + esac - # Set proper ownership for buildkite user - if [ "$ci" = "1" ] && [ "$user" = "buildkite-agent" ]; then - execute_sudo chown -R "$user:$user" "$cache_dir" 2>/dev/null || true - fi - fi - done + # Ensure entire path is accessible, not just last component + grant_to_user "$home/.cache" } bun_version_exact() { From 19a855e02b8e103d7b28c6bd091ce16daeaaa5e9 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 7 Jul 2025 17:47:51 -0700 Subject: [PATCH 130/147] types: Introduce SQL.Helper in Bun.sql (#20809) Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- packages/bun-types/bun.d.ts | 487 +++++++++++++----- packages/bun-types/deprecated.d.ts | 12 + src/js/bun/sql.ts | 38 +- test/integration/bun-types/fixture/sql.ts | 183 +++++-- .../bun-types/fixture/utilities.ts | 4 +- test/js/sql/sql.test.ts | 40 +- 6 files changed, 531 insertions(+), 233 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 633597bd72..9c3dc25b00 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1315,116 +1315,285 @@ declare module "bun" { stat(): Promise; } - /** - * Configuration options for SQL client connection and behavior - * @example - * const config: SQLOptions = { - * host: 'localhost', - * port: 5432, - * user: 'dbuser', - * password: 'secretpass', - * database: 'myapp', - * idleTimeout: 30, - * max: 20, - * onconnect: (client) => { - * console.log('Connected to database'); - * } - * }; - */ + namespace SQL { + type AwaitPromisesArray>> = { + [K in keyof T]: Awaited; + }; - interface SQLOptions { - /** Connection URL (can be string or URL object) */ - url?: URL | string; - /** Database server hostname */ - host?: string; - /** Database server hostname (alias for host) */ - hostname?: string; - /** Database server port number */ - port?: number | string; - /** Database user for authentication */ - username?: string; - /** Database user for authentication (alias for username) */ - user?: string; - /** Database password for authentication */ - password?: string | (() => Promise); - /** Database password for authentication (alias for password) */ - pass?: string | (() => Promise); - /** Name of the database to connect to */ - database?: string; - /** Name of the database to connect to (alias for database) */ - db?: string; - /** Database adapter/driver to use */ - adapter?: string; - /** Maximum time in seconds to wait for connection to become available */ - idleTimeout?: number; - /** Maximum time in seconds to wait for connection to become available (alias for idleTimeout) */ - idle_timeout?: number; - /** Maximum time in seconds to wait when establishing a connection */ - connectionTimeout?: number; - /** Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout) */ - connection_timeout?: number; - /** Maximum lifetime in seconds of a connection */ - maxLifetime?: number; - /** Maximum lifetime in seconds of a connection (alias for maxLifetime) */ - max_lifetime?: number; - /** Whether to use TLS/SSL for the connection */ - tls?: TLSOptions | boolean; - /** Whether to use TLS/SSL for the connection (alias for tls) */ - ssl?: TLSOptions | boolean; - /** Callback function executed when a connection is established */ - onconnect?: (client: SQL) => void; - /** Callback function executed when a connection is closed */ - onclose?: (client: SQL) => void; - /** Maximum number of connections in the pool */ - max?: number; - /** By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts. */ - bigint?: boolean; - /** Automatic creation of prepared statements, defaults to true */ - prepare?: boolean; + type ContextCallbackResult = T extends Array> ? AwaitPromisesArray : Awaited; + type ContextCallback = (sql: SQL) => Promise; + + /** + * Configuration options for SQL client connection and behavior + * + * @example + * ```ts + * const config: Bun.SQL.Options = { + * host: 'localhost', + * port: 5432, + * user: 'dbuser', + * password: 'secretpass', + * database: 'myapp', + * idleTimeout: 30, + * max: 20, + * onconnect: (client) => { + * console.log('Connected to database'); + * } + * }; + * ``` + */ + interface Options { + /** + * Connection URL (can be string or URL object) + */ + url?: URL | string | undefined; + + /** + * Database server hostname + * @default "localhost" + */ + host?: string | undefined; + + /** + * Database server hostname (alias for host) + * @deprecated Prefer {@link host} + * @default "localhost" + */ + hostname?: string | undefined; + + /** + * Database server port number + * @default 5432 + */ + port?: number | string | undefined; + + /** + * Database user for authentication + * @default "postgres" + */ + username?: string | undefined; + + /** + * Database user for authentication (alias for username) + * @deprecated Prefer {@link username} + * @default "postgres" + */ + user?: string | undefined; + + /** + * Database password for authentication + * @default "" + */ + password?: string | (() => MaybePromise) | undefined; + + /** + * Database password for authentication (alias for password) + * @deprecated Prefer {@link password} + * @default "" + */ + pass?: string | (() => MaybePromise) | undefined; + + /** + * Name of the database to connect to + * @default The username value + */ + database?: string | undefined; + + /** + * Name of the database to connect to (alias for database) + * @deprecated Prefer {@link database} + * @default The username value + */ + db?: string | undefined; + + /** + * Database adapter/driver to use + * @default "postgres" + */ + adapter?: "postgres" /*| "sqlite" | "mysql"*/ | (string & {}) | undefined; + + /** + * Maximum time in seconds to wait for connection to become available + * @default 0 (no timeout) + */ + idleTimeout?: number | undefined; + + /** + * Maximum time in seconds to wait for connection to become available (alias for idleTimeout) + * @deprecated Prefer {@link idleTimeout} + * @default 0 (no timeout) + */ + idle_timeout?: number | undefined; + + /** + * Maximum time in seconds to wait when establishing a connection + * @default 30 + */ + connectionTimeout?: number | undefined; + + /** + * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout) + * @deprecated Prefer {@link connectionTimeout} + * @default 30 + */ + connection_timeout?: number | undefined; + + /** + * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout) + * @deprecated Prefer {@link connectionTimeout} + * @default 30 + */ + connectTimeout?: number | undefined; + + /** + * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout) + * @deprecated Prefer {@link connectionTimeout} + * @default 30 + */ + connect_timeout?: number | undefined; + + /** + * Maximum lifetime in seconds of a connection + * @default 0 (no maximum lifetime) + */ + maxLifetime?: number | undefined; + + /** + * Maximum lifetime in seconds of a connection (alias for maxLifetime) + * @deprecated Prefer {@link maxLifetime} + * @default 0 (no maximum lifetime) + */ + max_lifetime?: number | undefined; + + /** + * Whether to use TLS/SSL for the connection + * @default false + */ + tls?: TLSOptions | boolean | undefined; + + /** + * Whether to use TLS/SSL for the connection (alias for tls) + * @default false + */ + ssl?: TLSOptions | boolean | undefined; + + // `.path` is currently unsupported in Bun, the implementation is incomplete. + // + // /** + // * Unix domain socket path for connection + // * @default "" + // */ + // path?: string | undefined; + + /** + * Callback function executed when a connection is established + */ + onconnect?: ((client: SQL) => void) | undefined; + + /** + * Callback function executed when a connection is closed + */ + onclose?: ((client: SQL) => void) | undefined; + + /** + * Postgres client runtime configuration options + * + * @see https://www.postgresql.org/docs/current/runtime-config-client.html + */ + connection?: Record | undefined; + + /** + * Maximum number of connections in the pool + * @default 10 + */ + max?: number | undefined; + + /** + * By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts. + * @default false + */ + bigint?: boolean | undefined; + + /** + * Automatic creation of prepared statements + * @default true + */ + prepare?: boolean | undefined; + } + + /** + * Represents a SQL query that can be executed, with additional control methods + * Extends Promise to allow for async/await usage + */ + interface Query extends Promise { + /** + * Indicates if the query is currently executing + */ + active: boolean; + + /** + * Indicates if the query has been cancelled + */ + cancelled: boolean; + + /** + * Cancels the executing query + */ + cancel(): Query; + + /** + * Executes the query as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons + */ + simple(): Query; + + /** + * Executes the query + */ + execute(): Query; + + /** + * Returns the raw query result + */ + raw(): Query; + + /** + * Returns only the values from the query result + */ + values(): Query; + } + + /** + * Callback function type for transaction contexts + * @param sql Function to execute SQL queries within the transaction + */ + type TransactionContextCallback = ContextCallback; + + /** + * Callback function type for savepoint contexts + * @param sql Function to execute SQL queries within the savepoint + */ + type SavepointContextCallback = ContextCallback; + + /** + * SQL.Helper represents a parameter or serializable + * value inside of a query. + * + * @example + * ```ts + * const helper = sql(users, 'id'); + * await sql`insert into users ${helper}`; + * ``` + */ + interface Helper { + readonly value: T[]; + readonly columns: (keyof T)[]; + } } - /** - * Represents a SQL query that can be executed, with additional control methods - * Extends Promise to allow for async/await usage - */ - interface SQLQuery extends Promise { - /** Indicates if the query is currently executing */ - active: boolean; - - /** Indicates if the query has been cancelled */ - cancelled: boolean; - - /** Cancels the executing query */ - cancel(): SQLQuery; - - /** Execute as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons */ - simple(): SQLQuery; - - /** Executes the query */ - execute(): SQLQuery; - - /** Returns the raw query result */ - raw(): SQLQuery; - - /** Returns only the values from the query result */ - values(): SQLQuery; - } - - /** - * Callback function type for transaction contexts - * @param sql Function to execute SQL queries within the transaction - */ - type SQLTransactionContextCallback = (sql: TransactionSQL) => Promise | Array; - /** - * Callback function type for savepoint contexts - * @param sql Function to execute SQL queries within the savepoint - */ - type SQLSavepointContextCallback = (sql: SavepointSQL) => Promise | Array; - /** * Main SQL client interface providing connection and transaction management */ - interface SQL { + interface SQL extends AsyncDisposable { /** * Executes a SQL query using template literals * @example @@ -1432,7 +1601,12 @@ declare module "bun" { * const [user] = await sql`select * from users where id = ${1}`; * ``` */ - (strings: string[] | TemplateStringsArray, ...values: any[]): SQLQuery; + (strings: TemplateStringsArray, ...values: unknown[]): SQL.Query; + + /** + * Execute a SQL query using a string + */ + (string: string): SQL.Query; /** * Helper function for inserting an object into a query @@ -1440,16 +1614,19 @@ declare module "bun" { * @example * ```ts * // Insert an object - * const result = await sql`insert into users ${sql(users)} RETURNING *`; + * const result = await sql`insert into users ${sql(users)} returning *`; * * // Or pick specific columns - * const result = await sql`insert into users ${sql(users, "id", "name")} RETURNING *`; + * const result = await sql`insert into users ${sql(users, "id", "name")} returning *`; * * // Or a single object - * const result = await sql`insert into users ${sql(user)} RETURNING *`; + * const result = await sql`insert into users ${sql(user)} returning *`; * ``` */ - (obj: T | T[] | readonly T[], ...columns: (keyof T)[]): SQLQuery; + ( + obj: T | T[] | readonly T[], + ...columns: readonly Keys[] + ): SQL.Helper>; /** * Helper function for inserting any serializable value into a query @@ -1459,7 +1636,7 @@ declare module "bun" { * const result = await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`; * ``` */ - (obj: unknown): SQLQuery; + (value: T): SQL.Helper; /** * Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL @@ -1531,6 +1708,7 @@ declare module "bun" { /** * The reserve method pulls out a connection from the pool, and returns a client that wraps the single connection. + * * This can be used for running queries on an isolated connection. * Calling reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package). * @@ -1556,7 +1734,10 @@ declare module "bun" { * ``` */ reserve(): Promise; - /** Begins a new transaction + + /** + * Begins a new transaction. + * * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function. * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue. * @example @@ -1580,8 +1761,11 @@ declare module "bun" { * return [user, account] * }) */ - begin(fn: SQLTransactionContextCallback): Promise; - /** Begins a new transaction with options + begin(fn: SQL.TransactionContextCallback): Promise>; + + /** + * Begins a new transaction with options. + * * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function. * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue. * @example @@ -1605,8 +1789,11 @@ declare module "bun" { * return [user, account] * }) */ - begin(options: string, fn: SQLTransactionContextCallback): Promise; - /** Alternative method to begin a transaction + begin(options: string, fn: SQL.TransactionContextCallback): Promise>; + + /** + * Alternative method to begin a transaction. + * * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function. * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue. * @alias begin @@ -1631,11 +1818,15 @@ declare module "bun" { * return [user, account] * }) */ - transaction(fn: SQLTransactionContextCallback): Promise; - /** Alternative method to begin a transaction with options + transaction(fn: SQL.TransactionContextCallback): Promise>; + + /** + * Alternative method to begin a transaction with options * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function. * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue. - * @alias begin + * + * @alias {@link begin} + * * @example * const [user, account] = await sql.transaction("read write", async sql => { * const [user] = await sql` @@ -1655,15 +1846,18 @@ declare module "bun" { * returning * * ` * return [user, account] - * }) + * }); */ - transaction(options: string, fn: SQLTransactionContextCallback): Promise; - /** Begins a distributed transaction + transaction(options: string, fn: SQL.TransactionContextCallback): Promise>; + + /** + * Begins a distributed transaction * Also know as Two-Phase Commit, in a distributed transaction, Phase 1 involves the coordinator preparing nodes by ensuring data is written and ready to commit, while Phase 2 finalizes with nodes committing or rolling back based on the coordinator's decision, ensuring durability and releasing locks. * In PostgreSQL and MySQL distributed transactions persist beyond the original session, allowing privileged users or coordinators to commit/rollback them, ensuring support for distributed transactions, recovery, and administrative tasks. * beginDistributed will automatic rollback if any exception are not caught, and you can commit and rollback later if everything goes well. * PostgreSQL natively supports distributed transactions using PREPARE TRANSACTION, while MySQL uses XA Transactions, and MSSQL also supports distributed/XA transactions. However, in MSSQL, distributed transactions are tied to the original session, the DTC coordinator, and the specific connection. * These transactions are automatically committed or rolled back following the same rules as regular transactions, with no option for manual intervention from other sessions, in MSSQL distributed transactions are used to coordinate transactions using Linked Servers. + * * @example * await sql.beginDistributed("numbers", async sql => { * await sql`create table if not exists numbers (a int)`; @@ -1673,31 +1867,38 @@ declare module "bun" { * await sql.commitDistributed("numbers"); * // or await sql.rollbackDistributed("numbers"); */ - beginDistributed(name: string, fn: SQLTransactionContextCallback): Promise; + beginDistributed( + name: string, + fn: SQL.TransactionContextCallback, + ): Promise>; + /** Alternative method to begin a distributed transaction - * @alias beginDistributed + * @alias {@link beginDistributed} */ - distributed(name: string, fn: SQLTransactionContextCallback): Promise; + distributed(name: string, fn: SQL.TransactionContextCallback): Promise>; + /**If you know what you're doing, you can use unsafe to pass any string you'd like. * Please note that this can lead to SQL injection if you're not careful. * You can also nest sql.unsafe within a safe sql expression. This is useful if only part of your fraction has unsafe elements. * @example * const result = await sql.unsafe(`select ${danger} from users where id = ${dragons}`) */ - unsafe(string: string, values?: any[]): SQLQuery; + unsafe(string: string, values?: any[]): SQL.Query; + /** * Reads a file and uses the contents as a query. * Optional parameters can be used if the file includes $1, $2, etc * @example * const result = await sql.file("query.sql", [1, 2, 3]); */ - file(filename: string, values?: any[]): SQLQuery; + file(filename: string, values?: any[]): SQL.Query; - /** Current client options */ - options: SQLOptions; - - [Symbol.asyncDispose](): Promise; + /** + * Current client options + */ + options: SQL.Options; } + const SQL: { /** * Creates a new SQL client instance @@ -1723,7 +1924,7 @@ declare module "bun" { * const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 }); * ``` */ - new (connectionString: string | URL, options: Omit): SQL; + new (connectionString: string | URL, options: Omit): SQL; /** * Creates a new SQL client instance with options @@ -1735,17 +1936,18 @@ declare module "bun" { * const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 }); * ``` */ - new (options?: SQLOptions): SQL; + new (options?: SQL.Options): SQL; }; /** * Represents a reserved connection from the connection pool * Extends SQL with additional release functionality */ - interface ReservedSQL extends SQL { - /** Releases the client back to the connection pool */ + interface ReservedSQL extends SQL, Disposable { + /** + * Releases the client back to the connection pool + */ release(): void; - [Symbol.dispose](): void; } /** @@ -1754,26 +1956,30 @@ declare module "bun" { */ interface TransactionSQL extends SQL { /** Creates a savepoint within the current transaction */ - savepoint(name: string, fn: SQLSavepointContextCallback): Promise; - savepoint(fn: SQLSavepointContextCallback): Promise; + savepoint(name: string, fn: SQLSavepointContextCallback): Promise; + savepoint(fn: SQLSavepointContextCallback): Promise; } + /** * Represents a savepoint within a transaction */ interface SavepointSQL extends SQL {} type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256"; + interface CSRFGenerateOptions { /** * The number of milliseconds until the token expires. 0 means the token never expires. * @default 24 * 60 * 60 * 1000 (24 hours) */ expiresIn?: number; + /** * The encoding of the token. * @default "base64url" */ encoding?: "base64" | "base64url" | "hex"; + /** * The algorithm to use for the token. * @default "sha256" @@ -1786,16 +1992,19 @@ declare module "bun" { * The secret to use for the token. If not provided, a random default secret will be generated in memory and used. */ secret?: string; + /** * The encoding of the token. * @default "base64url" */ encoding?: "base64" | "base64url" | "hex"; + /** * The algorithm to use for the token. * @default "sha256" */ algorithm?: CSRFAlgorithm; + /** * The number of milliseconds until the token expires. 0 means the token never expires. * @default 24 * 60 * 60 * 1000 (24 hours) @@ -1805,15 +2014,11 @@ declare module "bun" { /** * SQL client - * - * @category Database */ const sql: SQL; /** * SQL client for PostgreSQL - * - * @category Database */ const postgres: SQL; diff --git a/packages/bun-types/deprecated.d.ts b/packages/bun-types/deprecated.d.ts index 2fb502e4ad..4fa3294a26 100644 --- a/packages/bun-types/deprecated.d.ts +++ b/packages/bun-types/deprecated.d.ts @@ -14,6 +14,18 @@ declare module "bun" { ): void; } + /** @deprecated Use {@link SQL.Query Bun.SQL.Query} */ + type SQLQuery = SQL.Query; + + /** @deprecated Use {@link SQL.TransactionContextCallback Bun.SQL.TransactionContextCallback} */ + type SQLTransactionContextCallback = SQL.TransactionContextCallback; + + /** @deprecated Use {@link SQL.SavepointContextCallback Bun.SQL.SavepointContextCallback} */ + type SQLSavepointContextCallback = SQL.SavepointContextCallback; + + /** @deprecated Use {@link SQL.Options Bun.SQL.Options} */ + type SQLOptions = SQL.Options; + /** * @deprecated Renamed to `ErrorLike` */ diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts index dd88169c35..84aa23211e 100644 --- a/src/js/bun/sql.ts +++ b/src/js/bun/sql.ts @@ -280,13 +280,13 @@ function normalizeQuery(strings, values, binding_idx = 1) { binding_values.push(sub_values[j]); } binding_idx += sub_values.length; - } else if (value instanceof SQLArrayParameter) { + } else if (value instanceof SQLHelper) { const command = detectCommand(query); // only selectIn, insert, update, updateSet are allowed if (command === SQLCommand.none || command === SQLCommand.where) { - throw new SyntaxError("Helper are only allowed for INSERT, UPDATE and WHERE IN commands"); + throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands"); } - const { columns, value: items } = value as SQLArrayParameter; + const { columns, value: items } = value as SQLHelper; const columnCount = columns.length; if (columnCount === 0 && command !== SQLCommand.whereIn) { throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); @@ -1300,7 +1300,7 @@ function doCreateQuery(strings, values, allowUnsafeTransaction, poolSize, bigint return createQuery(sqlString, final_values, new SQLResultArray(), undefined, !!bigint, !!simple); } -class SQLArrayParameter { +class SQLHelper { value: any; columns: string[]; constructor(value, keys) { @@ -1339,7 +1339,7 @@ function decodeIfValid(value) { } return null; } -function loadOptions(o) { +function loadOptions(o: Bun.SQL.Options) { var hostname, port, username, @@ -1453,6 +1453,8 @@ function loadOptions(o) { idleTimeout ??= o.idle_timeout; connectionTimeout ??= o.connectionTimeout; connectionTimeout ??= o.connection_timeout; + connectionTimeout ??= o.connectTimeout; + connectionTimeout ??= o.connect_timeout; maxLifetime ??= o.maxLifetime; maxLifetime ??= o.max_lifetime; bigint ??= o.bigint; @@ -1746,14 +1748,10 @@ function SQL(o, e = {}) { if ($isArray(strings)) { // detect if is tagged template if (!$isArray((strings as unknown as TemplateStringsArray).raw)) { - return new SQLArrayParameter(strings, values); + return new SQLHelper(strings, values); } - } else if ( - typeof strings === "object" && - !(strings instanceof Query) && - !(strings instanceof SQLArrayParameter) - ) { - return new SQLArrayParameter([strings], values); + } else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) { + return new SQLHelper([strings], values); } // we use the same code path as the transaction sql return queryFromTransaction(strings, values, pooledConnection, state.queries); @@ -2079,14 +2077,10 @@ function SQL(o, e = {}) { if ($isArray(strings)) { // detect if is tagged template if (!$isArray((strings as unknown as TemplateStringsArray).raw)) { - return new SQLArrayParameter(strings, values); + return new SQLHelper(strings, values); } - } else if ( - typeof strings === "object" && - !(strings instanceof Query) && - !(strings instanceof SQLArrayParameter) - ) { - return new SQLArrayParameter([strings], values); + } else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) { + return new SQLHelper([strings], values); } return queryFromTransaction(strings, values, pooledConnection, state.queries); @@ -2313,10 +2307,10 @@ function SQL(o, e = {}) { if ($isArray(strings)) { // detect if is tagged template if (!$isArray((strings as unknown as TemplateStringsArray).raw)) { - return new SQLArrayParameter(strings, values); + return new SQLHelper(strings, values); } - } else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLArrayParameter)) { - return new SQLArrayParameter([strings], values); + } else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) { + return new SQLHelper([strings], values); } return queryFromPool(strings, values); diff --git a/test/integration/bun-types/fixture/sql.ts b/test/integration/bun-types/fixture/sql.ts index 245d4d1602..20aab93e96 100644 --- a/test/integration/bun-types/fixture/sql.ts +++ b/test/integration/bun-types/fixture/sql.ts @@ -29,12 +29,13 @@ const sql2 = new Bun.SQL("postgres://localhost:5432/mydb"); const sql3 = new Bun.SQL(new URL("postgres://localhost:5432/mydb")); const sql4 = new Bun.SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 }); -const query1 = sql1`SELECT * FROM users WHERE id = ${1}`; +const query1 = sql1`SELECT * FROM users WHERE id = ${1}`; const query2 = sql2({ foo: "bar" }); query1.cancel().simple().execute().raw().values(); -const _promise: Promise = query1; +expectType(query1).extends>(); +expectType(query1).extends>(); sql1.connect(); sql1.close(); @@ -50,33 +51,74 @@ sql1.begin(async txn => { }); }); -sql1.transaction(async txn => { - txn`SELECT 3`; -}); +expectType( + sql1.transaction(async txn => { + txn`SELECT 3`; + }), +).is>(); -sql1.begin("read write", async txn => { - txn`SELECT 4`; -}); +expectType( + sql1.begin("read write", async txn => { + txn`SELECT 4`; + }), +).is>(); -sql1.transaction("read write", async txn => { - txn`SELECT 5`; -}); +expectType( + sql1.transaction("read write", async txn => { + txn`SELECT 5`; + }), +).is>(); -sql1.beginDistributed("foo", async txn => { - txn`SELECT 6`; -}); +expectType( + sql1.beginDistributed("foo", async txn => { + txn`SELECT 6`; + }), +).is>(); -sql1.distributed("bar", async txn => { - txn`SELECT 7`; -}); +expectType( + sql1.distributed("bar", async txn => { + txn`SELECT 7`; + }), +).is>(); -sql1.unsafe("SELECT * FROM users"); -sql1.file("query.sql", [1, 2, 3]); +expectType( + sql1.beginDistributed("foo", async txn => { + txn`SELECT 8`; + }), +).is>(); + +{ + const tx = await sql1.transaction(async txn => { + return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`]; + }); + + expectType(tx).is(); +} + +{ + const tx = await sql1.begin(async txn => { + return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`]; + }); + + expectType(tx).is(); +} + +{ + const tx = await sql1.distributed("name", async txn => { + return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`]; + }); + + expectType(tx).is(); +} + +expectType(sql1.unsafe("SELECT * FROM users")).is>(); +expectType(sql1.unsafe<{ id: string }[]>("SELECT * FROM users")).is>(); +expectType(sql1.file("query.sql", [1, 2, 3])).is>(); sql1.reserve().then(reserved => { reserved.release(); - reserved[Symbol.dispose]?.(); - reserved`SELECT 8`; + + expectType(reserved<[8]>`SELECT 8`).is>(); }); sql1.begin(async txn => { @@ -109,45 +151,48 @@ sql1.begin("read write", 123); // @ts-expect-error sql1.transaction("read write", 123); -const sqlQueryAny: Bun.SQLQuery = {} as any; -const sqlQueryNumber: Bun.SQLQuery = {} as any; -const sqlQueryString: Bun.SQLQuery = {} as any; +const sqlQueryAny: Bun.SQL.Query = {} as any; +const sqlQueryNumber: Bun.SQL.Query = {} as any; +const sqlQueryString: Bun.SQL.Query = {} as any; expectAssignable>(sqlQueryAny); expectAssignable>(sqlQueryNumber); expectAssignable>(sqlQueryString); -expectType(sqlQueryNumber).is>(); -expectType(sqlQueryString).is>(); -expectType(sqlQueryNumber).is>(); +expectType(sqlQueryNumber).is>(); +expectType(sqlQueryString).is>(); +expectType(sqlQueryNumber).is>(); const queryA = sql`SELECT 1`; -expectType(queryA).is(); +expectType(queryA).is>(); +expectType(await queryA).is(); + const queryB = sql({ foo: "bar" }); -expectType(queryB).is(); +expectType(queryB).is>(); expectType(sql).is(); -const opts2: Bun.SQLOptions = { url: "postgres://localhost" }; -expectType(opts2).is(); +const opts2 = { url: "postgres://localhost" } satisfies Bun.SQL.Options; +expectType(opts2).extends(); -const txCb: Bun.SQLTransactionContextCallback = async sql => [sql`SELECT 1`]; -const spCb: Bun.SQLSavepointContextCallback = async sql => [sql`SELECT 2`]; -expectType(txCb).is(); -expectType(spCb).is(); +const txCb = (async sql => [sql<[1]>`SELECT 1`]) satisfies Bun.SQL.TransactionContextCallback; +const spCb = (async sql => [sql<[2]>`SELECT 2`]) satisfies Bun.SQL.SavepointContextCallback; -expectType(queryA.cancel()).is(); -expectType(queryA.simple()).is(); -expectType(queryA.execute()).is(); -expectType(queryA.raw()).is(); -expectType(queryA.values()).is(); +expectType(await sql.begin(txCb)).is<[1][]>(); +expectType(await sql.begin(spCb)).is<[2][]>(); -declare const queryNum: Bun.SQLQuery; -expectType(queryNum.cancel()).is>(); -expectType(queryNum.simple()).is>(); -expectType(queryNum.execute()).is>(); -expectType(queryNum.raw()).is>(); -expectType(queryNum.values()).is>(); +expectType(queryA.cancel()).is>(); +expectType(queryA.simple()).is>(); +expectType(queryA.execute()).is>(); +expectType(queryA.raw()).is>(); +expectType(queryA.values()).is>(); + +declare const queryNum: Bun.SQL.Query; +expectType(queryNum.cancel()).is>(); +expectType(queryNum.simple()).is>(); +expectType(queryNum.execute()).is>(); +expectType(queryNum.raw()).is>(); +expectType(queryNum.values()).is>(); expectType(await queryNum.cancel()).is(); expectType(await queryNum.simple()).is(); @@ -155,29 +200,54 @@ expectType(await queryNum.execute()).is(); expectType(await queryNum.raw()).is(); expectType(await queryNum.values()).is(); -const _sqlInstance: Bun.SQL = Bun.sql; +expectType({ + password: () => "hey", + pass: async () => "hey", +}); -expectType(sql({ name: "Alice", email: "alice@example.com" })).is(); +expectType({ + password: "hey", +}); + +expectType(sql({ name: "Alice", email: "alice@example.com" })).is< + Bun.SQL.Helper<{ + name: string; + email: string; + }> +>(); expectType( sql([ { name: "Alice", email: "alice@example.com" }, { name: "Bob", email: "bob@example.com" }, ]), -).is(); +).is< + Bun.SQL.Helper<{ + name: string; + email: string; + }> +>(); -const user = { name: "Alice", email: "alice@example.com", age: 25 }; -expectType(sql(user, "name", "email")).is(); +const userWithAge = { name: "Alice", email: "alice@example.com", age: 25 }; + +expectType(sql(userWithAge, "name", "email")).is< + Bun.SQL.Helper<{ + name: string; + email: string; + }> +>(); const users = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, ]; -expectType(sql(users, "id")).is(); +expectType(sql(users, "id")).is>(); -expectType(sql([1, 2, 3])).is(); +expectType(sql([1, 2, 3])).is>(); +expectType(sql([1, 2, 3] as const)).is>(); -expectType(sql("users")).is(); +expectType(sql("users")).is>(); +expectType(sql<1>("users")).is>(); // @ts-expect-error - missing key in object sql(user, "notAKey"); @@ -190,3 +260,8 @@ sql(users, "notAKey"); // @ts-expect-error - array of numbers, extra key argument sql([1, 2, 3], "notAKey"); + +// check the deprecated stuff still exists +expectType>(); +expectType>(); +expectType>(); diff --git a/test/integration/bun-types/fixture/utilities.ts b/test/integration/bun-types/fixture/utilities.ts index ba282c6c60..881f4df3e4 100644 --- a/test/integration/bun-types/fixture/utilities.ts +++ b/test/integration/bun-types/fixture/utilities.ts @@ -27,9 +27,11 @@ export function expectType(arg: T): { * ``` */ is(...args: IfEquals extends true ? [] : [expected: X, but_got: T]): void; + extends(...args: T extends X ? [] : [expected: T, but_got: X]): void; }; + export function expectType(arg?: T) { - return { is() {} }; + return { is() {}, extends() {} }; } export declare const expectAssignable: (expression: T) => void; diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 4a15541e20..ba5b4f022b 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -2318,21 +2318,31 @@ if (isDockerEnabled()) { // ] // }) - // t('connect_timeout', { timeout: 20 }, async() => { - // const connect_timeout = 0.2 - // const server = net.createServer() - // server.listen() - // const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout }) - // const start = Date.now() - // let end - // await sql`select 1`.catch((e) => { - // if (e.code !== 'CONNECT_TIMEOUT') - // throw e - // end = Date.now() - // }) - // server.close() - // return [connect_timeout, Math.floor((end - start) / 100) / 10] - // }) + test.each(["connect_timeout", "connectTimeout", "connectionTimeout", "connection_timeout"] as const)( + "connection timeout key %p throws", + async key => { + const server = net.createServer().listen(); + + const port = (server.address() as import("node:net").AddressInfo).port; + + const sql = postgres({ port, host: "127.0.0.1", [key]: 0.2 }); + + try { + await sql`select 1`; + throw new Error("should not reach"); + } catch (e) { + expect(e).toBeInstanceOf(Error); + expect(e.code).toBe("ERR_POSTGRES_CONNECTION_TIMEOUT"); + expect(e.message).toMatch(/Connection timed out after 200ms/); + } finally { + sql.close(); + server.close(); + } + }, + { + timeout: 1000, + }, + ); // t('connect_timeout throws proper error', async() => [ // 'CONNECT_TIMEOUT', From aa0645598717c8a4c6bbe180ba6315914f6b9029 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 7 Jul 2025 18:41:01 -0700 Subject: [PATCH 131/147] refactor(postgres) improve postgres code base (#20808) Co-authored-by: cirospaciari <6379399+cirospaciari@users.noreply.github.com> --- cmake/sources/ZigSources.txt | 73 +- src/sql/postgres.zig | 3283 +---------------- src/sql/postgres/AnyPostgresError.zig | 89 + src/sql/postgres/AuthenticationState.zig | 21 + src/sql/postgres/CommandTag.zig | 107 + src/sql/postgres/ConnectionFlags.zig | 7 + src/sql/postgres/Data.zig | 67 + src/sql/{ => postgres}/DataCell.zig | 28 +- src/sql/postgres/DebugSocketMonitorReader.zig | 25 + src/sql/postgres/DebugSocketMonitorWriter.zig | 25 + src/sql/postgres/ObjectIterator.zig | 64 + src/sql/postgres/PostgresCachedStructure.zig | 34 + src/sql/postgres/PostgresProtocol.zig | 63 + src/sql/postgres/PostgresRequest.zig | 348 ++ src/sql/postgres/PostgresSQLConnection.zig | 1574 ++++++++ src/sql/postgres/PostgresSQLContext.zig | 23 + src/sql/postgres/PostgresSQLQuery.zig | 499 +++ .../postgres/PostgresSQLQueryResultMode.zig | 7 + src/sql/postgres/PostgresSQLStatement.zig | 192 + src/sql/postgres/PostgresTypes.zig | 20 + src/sql/postgres/QueryBindingIterator.zig | 66 + src/sql/postgres/SASL.zig | 95 + src/sql/postgres/SSLMode.zig | 9 + src/sql/postgres/Signature.zig | 113 + src/sql/postgres/SocketMonitor.zig | 23 + src/sql/postgres/Status.zig | 11 + src/sql/postgres/TLSStatus.zig | 11 + src/sql/postgres/postgres_protocol.zig | 1551 -------- src/sql/postgres/protocol/ArrayList.zig | 22 + src/sql/postgres/protocol/Authentication.zig | 182 + src/sql/postgres/protocol/BackendKeyData.zig | 20 + src/sql/postgres/protocol/Close.zig | 39 + .../postgres/protocol/ColumnIdentifier.zig | 40 + src/sql/postgres/protocol/CommandComplete.zig | 25 + src/sql/postgres/protocol/CopyData.zig | 40 + src/sql/postgres/protocol/CopyFail.zig | 42 + src/sql/postgres/protocol/CopyInResponse.zig | 14 + src/sql/postgres/protocol/CopyOutResponse.zig | 14 + src/sql/postgres/protocol/DataRow.zig | 33 + src/sql/postgres/protocol/DecoderWrap.zig | 14 + src/sql/postgres/protocol/Describe.zig | 28 + src/sql/postgres/protocol/ErrorResponse.zig | 159 + src/sql/postgres/protocol/Execute.zig | 28 + .../postgres/protocol/FieldDescription.zig | 70 + src/sql/postgres/protocol/FieldMessage.zig | 87 + src/sql/postgres/protocol/FieldType.zig | 57 + .../protocol/NegotiateProtocolVersion.zig | 44 + src/sql/postgres/protocol/NewReader.zig | 118 + src/sql/postgres/protocol/NewWriter.zig | 125 + src/sql/postgres/protocol/NoticeResponse.zig | 53 + .../protocol/NotificationResponse.zig | 31 + .../protocol/ParameterDescription.zig | 37 + src/sql/postgres/protocol/ParameterStatus.zig | 27 + src/sql/postgres/protocol/Parse.zig | 43 + src/sql/postgres/protocol/PasswordMessage.zig | 31 + .../protocol/PortalOrPreparedStatement.zig | 18 + src/sql/postgres/protocol/ReadyForQuery.zig | 18 + src/sql/postgres/protocol/RowDescription.zig | 44 + .../postgres/protocol/SASLInitialResponse.zig | 36 + src/sql/postgres/protocol/SASLResponse.zig | 31 + src/sql/postgres/protocol/StackReader.zig | 66 + src/sql/postgres/protocol/StartupMessage.zig | 52 + .../protocol/TransactionStatusIndicator.zig | 12 + src/sql/postgres/protocol/WriteWrap.zig | 14 + src/sql/postgres/protocol/zHelpers.zig | 11 + src/sql/postgres/types/PostgresString.zig | 52 + .../{postgres_types.zig => types/Tag.zig} | 171 +- src/sql/postgres/types/bool.zig | 20 + src/sql/postgres/types/bytea.zig | 25 + src/sql/postgres/types/date.zig | 57 + src/sql/postgres/types/int_types.zig | 10 + src/sql/postgres/types/json.zig | 29 + src/sql/postgres/types/numeric.zig | 20 + 73 files changed, 5542 insertions(+), 4995 deletions(-) create mode 100644 src/sql/postgres/AnyPostgresError.zig create mode 100644 src/sql/postgres/AuthenticationState.zig create mode 100644 src/sql/postgres/CommandTag.zig create mode 100644 src/sql/postgres/ConnectionFlags.zig create mode 100644 src/sql/postgres/Data.zig rename src/sql/{ => postgres}/DataCell.zig (99%) create mode 100644 src/sql/postgres/DebugSocketMonitorReader.zig create mode 100644 src/sql/postgres/DebugSocketMonitorWriter.zig create mode 100644 src/sql/postgres/ObjectIterator.zig create mode 100644 src/sql/postgres/PostgresCachedStructure.zig create mode 100644 src/sql/postgres/PostgresProtocol.zig create mode 100644 src/sql/postgres/PostgresRequest.zig create mode 100644 src/sql/postgres/PostgresSQLConnection.zig create mode 100644 src/sql/postgres/PostgresSQLContext.zig create mode 100644 src/sql/postgres/PostgresSQLQuery.zig create mode 100644 src/sql/postgres/PostgresSQLQueryResultMode.zig create mode 100644 src/sql/postgres/PostgresSQLStatement.zig create mode 100644 src/sql/postgres/PostgresTypes.zig create mode 100644 src/sql/postgres/QueryBindingIterator.zig create mode 100644 src/sql/postgres/SASL.zig create mode 100644 src/sql/postgres/SSLMode.zig create mode 100644 src/sql/postgres/Signature.zig create mode 100644 src/sql/postgres/SocketMonitor.zig create mode 100644 src/sql/postgres/Status.zig create mode 100644 src/sql/postgres/TLSStatus.zig delete mode 100644 src/sql/postgres/postgres_protocol.zig create mode 100644 src/sql/postgres/protocol/ArrayList.zig create mode 100644 src/sql/postgres/protocol/Authentication.zig create mode 100644 src/sql/postgres/protocol/BackendKeyData.zig create mode 100644 src/sql/postgres/protocol/Close.zig create mode 100644 src/sql/postgres/protocol/ColumnIdentifier.zig create mode 100644 src/sql/postgres/protocol/CommandComplete.zig create mode 100644 src/sql/postgres/protocol/CopyData.zig create mode 100644 src/sql/postgres/protocol/CopyFail.zig create mode 100644 src/sql/postgres/protocol/CopyInResponse.zig create mode 100644 src/sql/postgres/protocol/CopyOutResponse.zig create mode 100644 src/sql/postgres/protocol/DataRow.zig create mode 100644 src/sql/postgres/protocol/DecoderWrap.zig create mode 100644 src/sql/postgres/protocol/Describe.zig create mode 100644 src/sql/postgres/protocol/ErrorResponse.zig create mode 100644 src/sql/postgres/protocol/Execute.zig create mode 100644 src/sql/postgres/protocol/FieldDescription.zig create mode 100644 src/sql/postgres/protocol/FieldMessage.zig create mode 100644 src/sql/postgres/protocol/FieldType.zig create mode 100644 src/sql/postgres/protocol/NegotiateProtocolVersion.zig create mode 100644 src/sql/postgres/protocol/NewReader.zig create mode 100644 src/sql/postgres/protocol/NewWriter.zig create mode 100644 src/sql/postgres/protocol/NoticeResponse.zig create mode 100644 src/sql/postgres/protocol/NotificationResponse.zig create mode 100644 src/sql/postgres/protocol/ParameterDescription.zig create mode 100644 src/sql/postgres/protocol/ParameterStatus.zig create mode 100644 src/sql/postgres/protocol/Parse.zig create mode 100644 src/sql/postgres/protocol/PasswordMessage.zig create mode 100644 src/sql/postgres/protocol/PortalOrPreparedStatement.zig create mode 100644 src/sql/postgres/protocol/ReadyForQuery.zig create mode 100644 src/sql/postgres/protocol/RowDescription.zig create mode 100644 src/sql/postgres/protocol/SASLInitialResponse.zig create mode 100644 src/sql/postgres/protocol/SASLResponse.zig create mode 100644 src/sql/postgres/protocol/StackReader.zig create mode 100644 src/sql/postgres/protocol/StartupMessage.zig create mode 100644 src/sql/postgres/protocol/TransactionStatusIndicator.zig create mode 100644 src/sql/postgres/protocol/WriteWrap.zig create mode 100644 src/sql/postgres/protocol/zHelpers.zig create mode 100644 src/sql/postgres/types/PostgresString.zig rename src/sql/postgres/{postgres_types.zig => types/Tag.zig} (72%) create mode 100644 src/sql/postgres/types/bool.zig create mode 100644 src/sql/postgres/types/bytea.zig create mode 100644 src/sql/postgres/types/date.zig create mode 100644 src/sql/postgres/types/int_types.zig create mode 100644 src/sql/postgres/types/json.zig create mode 100644 src/sql/postgres/types/numeric.zig diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index aa592e5957..08d07b7249 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -732,10 +732,77 @@ src/sourcemap/CodeCoverage.zig src/sourcemap/LineOffsetTable.zig src/sourcemap/sourcemap.zig src/sourcemap/VLQ.zig -src/sql/DataCell.zig src/sql/postgres.zig -src/sql/postgres/postgres_protocol.zig -src/sql/postgres/postgres_types.zig +src/sql/postgres/AnyPostgresError.zig +src/sql/postgres/AuthenticationState.zig +src/sql/postgres/CommandTag.zig +src/sql/postgres/ConnectionFlags.zig +src/sql/postgres/Data.zig +src/sql/postgres/DataCell.zig +src/sql/postgres/DebugSocketMonitorReader.zig +src/sql/postgres/DebugSocketMonitorWriter.zig +src/sql/postgres/ObjectIterator.zig +src/sql/postgres/PostgresCachedStructure.zig +src/sql/postgres/PostgresProtocol.zig +src/sql/postgres/PostgresRequest.zig +src/sql/postgres/PostgresSQLConnection.zig +src/sql/postgres/PostgresSQLContext.zig +src/sql/postgres/PostgresSQLQuery.zig +src/sql/postgres/PostgresSQLQueryResultMode.zig +src/sql/postgres/PostgresSQLStatement.zig +src/sql/postgres/PostgresTypes.zig +src/sql/postgres/protocol/ArrayList.zig +src/sql/postgres/protocol/Authentication.zig +src/sql/postgres/protocol/BackendKeyData.zig +src/sql/postgres/protocol/Close.zig +src/sql/postgres/protocol/ColumnIdentifier.zig +src/sql/postgres/protocol/CommandComplete.zig +src/sql/postgres/protocol/CopyData.zig +src/sql/postgres/protocol/CopyFail.zig +src/sql/postgres/protocol/CopyInResponse.zig +src/sql/postgres/protocol/CopyOutResponse.zig +src/sql/postgres/protocol/DataRow.zig +src/sql/postgres/protocol/DecoderWrap.zig +src/sql/postgres/protocol/Describe.zig +src/sql/postgres/protocol/ErrorResponse.zig +src/sql/postgres/protocol/Execute.zig +src/sql/postgres/protocol/FieldDescription.zig +src/sql/postgres/protocol/FieldMessage.zig +src/sql/postgres/protocol/FieldType.zig +src/sql/postgres/protocol/NegotiateProtocolVersion.zig +src/sql/postgres/protocol/NewReader.zig +src/sql/postgres/protocol/NewWriter.zig +src/sql/postgres/protocol/NoticeResponse.zig +src/sql/postgres/protocol/NotificationResponse.zig +src/sql/postgres/protocol/ParameterDescription.zig +src/sql/postgres/protocol/ParameterStatus.zig +src/sql/postgres/protocol/Parse.zig +src/sql/postgres/protocol/PasswordMessage.zig +src/sql/postgres/protocol/PortalOrPreparedStatement.zig +src/sql/postgres/protocol/ReadyForQuery.zig +src/sql/postgres/protocol/RowDescription.zig +src/sql/postgres/protocol/SASLInitialResponse.zig +src/sql/postgres/protocol/SASLResponse.zig +src/sql/postgres/protocol/StackReader.zig +src/sql/postgres/protocol/StartupMessage.zig +src/sql/postgres/protocol/TransactionStatusIndicator.zig +src/sql/postgres/protocol/WriteWrap.zig +src/sql/postgres/protocol/zHelpers.zig +src/sql/postgres/QueryBindingIterator.zig +src/sql/postgres/SASL.zig +src/sql/postgres/Signature.zig +src/sql/postgres/SocketMonitor.zig +src/sql/postgres/SSLMode.zig +src/sql/postgres/Status.zig +src/sql/postgres/TLSStatus.zig +src/sql/postgres/types/bool.zig +src/sql/postgres/types/bytea.zig +src/sql/postgres/types/date.zig +src/sql/postgres/types/int_types.zig +src/sql/postgres/types/json.zig +src/sql/postgres/types/numeric.zig +src/sql/postgres/types/PostgresString.zig +src/sql/postgres/types/Tag.zig src/StandaloneModuleGraph.zig src/StaticHashMap.zig src/string_immutable.zig diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 18877fdfc3..728eeba420 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -1,3273 +1,3 @@ -const bun = @import("bun"); -const JSC = bun.JSC; -const String = bun.String; -const uws = bun.uws; -const std = @import("std"); -pub const debug = bun.Output.scoped(.Postgres, false); -pub const int4 = u32; -pub const PostgresInt32 = int4; -pub const int8 = i64; -pub const PostgresInt64 = int8; -pub const short = u16; -pub const PostgresShort = u16; -const Crypto = JSC.API.Bun.Crypto; -const JSValue = JSC.JSValue; -const BoringSSL = bun.BoringSSL; -pub const AnyPostgresError = error{ - ConnectionClosed, - ExpectedRequest, - ExpectedStatement, - InvalidBackendKeyData, - InvalidBinaryData, - InvalidByteSequence, - InvalidByteSequenceForEncoding, - InvalidCharacter, - InvalidMessage, - InvalidMessageLength, - InvalidQueryBinding, - InvalidServerKey, - InvalidServerSignature, - JSError, - MultidimensionalArrayNotSupportedYet, - NullsInArrayNotSupportedYet, - OutOfMemory, - Overflow, - PBKDFD2, - SASL_SIGNATURE_MISMATCH, - SASL_SIGNATURE_INVALID_BASE64, - ShortRead, - TLSNotAvailable, - TLSUpgradeFailed, - UnexpectedMessage, - UNKNOWN_AUTHENTICATION_METHOD, - UNSUPPORTED_AUTHENTICATION_METHOD, - UnsupportedByteaFormat, - UnsupportedIntegerSize, - UnsupportedArrayFormat, - UnsupportedNumericFormat, - UnknownFormatCode, -}; - -pub fn postgresErrorToJS(globalObject: *JSC.JSGlobalObject, message: ?[]const u8, err: AnyPostgresError) JSValue { - const error_code: JSC.Error = switch (err) { - error.ConnectionClosed => .POSTGRES_CONNECTION_CLOSED, - error.ExpectedRequest => .POSTGRES_EXPECTED_REQUEST, - error.ExpectedStatement => .POSTGRES_EXPECTED_STATEMENT, - error.InvalidBackendKeyData => .POSTGRES_INVALID_BACKEND_KEY_DATA, - error.InvalidBinaryData => .POSTGRES_INVALID_BINARY_DATA, - error.InvalidByteSequence => .POSTGRES_INVALID_BYTE_SEQUENCE, - error.InvalidByteSequenceForEncoding => .POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING, - error.InvalidCharacter => .POSTGRES_INVALID_CHARACTER, - error.InvalidMessage => .POSTGRES_INVALID_MESSAGE, - error.InvalidMessageLength => .POSTGRES_INVALID_MESSAGE_LENGTH, - error.InvalidQueryBinding => .POSTGRES_INVALID_QUERY_BINDING, - error.InvalidServerKey => .POSTGRES_INVALID_SERVER_KEY, - error.InvalidServerSignature => .POSTGRES_INVALID_SERVER_SIGNATURE, - error.MultidimensionalArrayNotSupportedYet => .POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET, - error.NullsInArrayNotSupportedYet => .POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET, - error.Overflow => .POSTGRES_OVERFLOW, - error.PBKDFD2 => .POSTGRES_AUTHENTICATION_FAILED_PBKDF2, - error.SASL_SIGNATURE_MISMATCH => .POSTGRES_SASL_SIGNATURE_MISMATCH, - error.SASL_SIGNATURE_INVALID_BASE64 => .POSTGRES_SASL_SIGNATURE_INVALID_BASE64, - error.TLSNotAvailable => .POSTGRES_TLS_NOT_AVAILABLE, - error.TLSUpgradeFailed => .POSTGRES_TLS_UPGRADE_FAILED, - error.UnexpectedMessage => .POSTGRES_UNEXPECTED_MESSAGE, - error.UNKNOWN_AUTHENTICATION_METHOD => .POSTGRES_UNKNOWN_AUTHENTICATION_METHOD, - error.UNSUPPORTED_AUTHENTICATION_METHOD => .POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD, - error.UnsupportedByteaFormat => .POSTGRES_UNSUPPORTED_BYTEA_FORMAT, - error.UnsupportedArrayFormat => .POSTGRES_UNSUPPORTED_ARRAY_FORMAT, - error.UnsupportedIntegerSize => .POSTGRES_UNSUPPORTED_INTEGER_SIZE, - error.UnsupportedNumericFormat => .POSTGRES_UNSUPPORTED_NUMERIC_FORMAT, - error.UnknownFormatCode => .POSTGRES_UNKNOWN_FORMAT_CODE, - error.JSError => { - return globalObject.takeException(error.JSError); - }, - error.OutOfMemory => { - // TODO: add binding for creating an out of memory error? - return globalObject.takeException(globalObject.throwOutOfMemory()); - }, - error.ShortRead => { - bun.unreachablePanic("Assertion failed: ShortRead should be handled by the caller in postgres", .{}); - }, - }; - if (message) |msg| { - return error_code.fmt(globalObject, "{s}", .{msg}); - } - return error_code.fmt(globalObject, "Failed to bind query: {s}", .{@errorName(err)}); -} - -pub const SSLMode = enum(u8) { - disable = 0, - prefer = 1, - require = 2, - verify_ca = 3, - verify_full = 4, -}; - -pub const Data = union(enum) { - owned: bun.ByteList, - temporary: []const u8, - empty: void, - - pub const Empty: Data = .{ .empty = {} }; - - pub fn toOwned(this: @This()) !bun.ByteList { - return switch (this) { - .owned => this.owned, - .temporary => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.temporary)), - .empty => bun.ByteList.init(&.{}), - }; - } - - pub fn deinit(this: *@This()) void { - switch (this.*) { - .owned => this.owned.deinitWithAllocator(bun.default_allocator), - .temporary => {}, - .empty => {}, - } - } - - /// Zero bytes before deinit - /// Generally, for security reasons. - pub fn zdeinit(this: *@This()) void { - switch (this.*) { - .owned => { - - // Zero bytes before deinit - @memset(this.owned.slice(), 0); - - this.owned.deinitWithAllocator(bun.default_allocator); - }, - .temporary => {}, - .empty => {}, - } - } - - pub fn slice(this: @This()) []const u8 { - return switch (this) { - .owned => this.owned.slice(), - .temporary => this.temporary, - .empty => "", - }; - } - - pub fn substring(this: @This(), start_index: usize, end_index: usize) Data { - return switch (this) { - .owned => .{ .temporary = this.owned.slice()[start_index..end_index] }, - .temporary => .{ .temporary = this.temporary[start_index..end_index] }, - .empty => .{ .empty = {} }, - }; - } - - pub fn sliceZ(this: @This()) [:0]const u8 { - return switch (this) { - .owned => this.owned.slice()[0..this.owned.len :0], - .temporary => this.temporary[0..this.temporary.len :0], - .empty => "", - }; - } -}; -pub const protocol = @import("./postgres/postgres_protocol.zig"); -pub const types = @import("./postgres/postgres_types.zig"); - -const Socket = uws.AnySocket; -const PreparedStatementsMap = std.HashMapUnmanaged(u64, *PostgresSQLStatement, bun.IdentityContext(u64), 80); - -const SocketMonitor = struct { - const DebugSocketMonitorWriter = struct { - var file: std.fs.File = undefined; - var enabled = false; - var check = std.once(load); - pub fn write(data: []const u8) void { - file.writeAll(data) catch {}; - } - - fn load() void { - if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR")) |monitor| { - enabled = true; - file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { - enabled = false; - return; - }; - debug("writing to {s}", .{monitor}); - } - } - }; - - const DebugSocketMonitorReader = struct { - var file: std.fs.File = undefined; - var enabled = false; - var check = std.once(load); - - fn load() void { - if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR_READER")) |monitor| { - enabled = true; - file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { - enabled = false; - return; - }; - debug("duplicating reads to {s}", .{monitor}); - } - } - - pub fn write(data: []const u8) void { - file.writeAll(data) catch {}; - } - }; - - pub fn write(data: []const u8) void { - if (comptime bun.Environment.isDebug) { - DebugSocketMonitorWriter.check.call(); - if (DebugSocketMonitorWriter.enabled) { - DebugSocketMonitorWriter.write(data); - } - } - } - - pub fn read(data: []const u8) void { - if (comptime bun.Environment.isDebug) { - DebugSocketMonitorReader.check.call(); - if (DebugSocketMonitorReader.enabled) { - DebugSocketMonitorReader.write(data); - } - } - } -}; - -pub const PostgresSQLContext = struct { - tcp: ?*uws.SocketContext = null, - - onQueryResolveFn: JSC.Strong.Optional = .empty, - onQueryRejectFn: JSC.Strong.Optional = .empty, - - pub fn init(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var ctx = &globalObject.bunVM().rareData().postgresql_context; - ctx.onQueryResolveFn.set(globalObject, callframe.argument(0)); - ctx.onQueryRejectFn.set(globalObject, callframe.argument(1)); - - return .js_undefined; - } - - comptime { - const js_init = JSC.toJSHostFn(init); - @export(&js_init, .{ .name = "PostgresSQLContext__init" }); - } -}; -pub const PostgresSQLQueryResultMode = enum(u2) { - objects = 0, - values = 1, - raw = 2, -}; - -const JSRef = JSC.JSRef; - -pub const PostgresSQLQuery = struct { - statement: ?*PostgresSQLStatement = null, - query: bun.String = bun.String.empty, - cursor_name: bun.String = bun.String.empty, - - thisValue: JSRef = JSRef.empty(), - - status: Status = Status.pending, - - ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), - - flags: packed struct(u8) { - is_done: bool = false, - binary: bool = false, - bigint: bool = false, - simple: bool = false, - result_mode: PostgresSQLQueryResultMode = .objects, - _padding: u2 = 0, - } = .{}, - - pub const js = JSC.Codegen.JSPostgresSQLQuery; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - pub fn getTarget(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, clean_target: bool) JSC.JSValue { - const thisValue = this.thisValue.get(); - if (thisValue == .zero) { - return .zero; - } - const target = js.targetGetCached(thisValue) orelse return .zero; - if (clean_target) { - js.targetSetCached(thisValue, globalObject, .zero); - } - return target; - } - - pub const Status = enum(u8) { - /// The query was just enqueued, statement status can be checked for more details - pending, - /// The query is being bound to the statement - binding, - /// The query is running - running, - /// The query is waiting for a partial response - partial_response, - /// The query was successful - success, - /// The query failed - fail, - - pub fn isRunning(this: Status) bool { - return @intFromEnum(this) > @intFromEnum(Status.pending) and @intFromEnum(this) < @intFromEnum(Status.success); - } - }; - - pub fn hasPendingActivity(this: *@This()) bool { - return this.ref_count.load(.monotonic) > 1; - } - - pub fn deinit(this: *@This()) void { - this.thisValue.deinit(); - if (this.statement) |statement| { - statement.deref(); - } - this.query.deref(); - this.cursor_name.deref(); - bun.default_allocator.destroy(this); - } - - pub fn finalize(this: *@This()) void { - debug("PostgresSQLQuery finalize", .{}); - if (this.thisValue == .weak) { - // clean up if is a weak reference, if is a strong reference we need to wait until the query is done - // if we are a strong reference, here is probably a bug because GC'd should not happen - this.thisValue.weak = .zero; - } - this.deref(); - } - - pub fn deref(this: *@This()) void { - const ref_count = this.ref_count.fetchSub(1, .monotonic); - - if (ref_count == 1) { - this.deinit(); - } - } - - pub fn ref(this: *@This()) void { - bun.assert(this.ref_count.fetchAdd(1, .monotonic) > 0); - } - - pub fn onWriteFail( - this: *@This(), - err: AnyPostgresError, - globalObject: *JSC.JSGlobalObject, - queries_array: JSValue, - ) void { - this.status = .fail; - const thisValue = this.thisValue.get(); - defer this.thisValue.deinit(); - const targetValue = this.getTarget(globalObject, true); - if (thisValue == .zero or targetValue == .zero) { - return; - } - - const vm = JSC.VirtualMachine.get(); - const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?; - const event_loop = vm.eventLoop(); - event_loop.runCallback(function, globalObject, thisValue, &.{ - targetValue, - postgresErrorToJS(globalObject, null, err), - queries_array, - }); - } - pub fn onJSError(this: *@This(), err: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { - this.status = .fail; - this.ref(); - defer this.deref(); - - const thisValue = this.thisValue.get(); - defer this.thisValue.deinit(); - const targetValue = this.getTarget(globalObject, true); - if (thisValue == .zero or targetValue == .zero) { - return; - } - - var vm = JSC.VirtualMachine.get(); - const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?; - const event_loop = vm.eventLoop(); - event_loop.runCallback(function, globalObject, thisValue, &.{ - targetValue, - err, - }); - } - pub fn onError(this: *@This(), err: PostgresSQLStatement.Error, globalObject: *JSC.JSGlobalObject) void { - this.onJSError(err.toJS(globalObject), globalObject); - } - - const CommandTag = union(enum) { - // For an INSERT command, the tag is INSERT oid rows, where rows is the - // number of rows inserted. oid used to be the object ID of the inserted - // row if rows was 1 and the target table had OIDs, but OIDs system - // columns are not supported anymore; therefore oid is always 0. - INSERT: u64, - // For a DELETE command, the tag is DELETE rows where rows is the number - // of rows deleted. - DELETE: u64, - // For an UPDATE command, the tag is UPDATE rows where rows is the - // number of rows updated. - UPDATE: u64, - // For a MERGE command, the tag is MERGE rows where rows is the number - // of rows inserted, updated, or deleted. - MERGE: u64, - // For a SELECT or CREATE TABLE AS command, the tag is SELECT rows where - // rows is the number of rows retrieved. - SELECT: u64, - // For a MOVE command, the tag is MOVE rows where rows is the number of - // rows the cursor's position has been changed by. - MOVE: u64, - // For a FETCH command, the tag is FETCH rows where rows is the number - // of rows that have been retrieved from the cursor. - FETCH: u64, - // For a COPY command, the tag is COPY rows where rows is the number of - // rows copied. (Note: the row count appears only in PostgreSQL 8.2 and - // later.) - COPY: u64, - - other: []const u8, - - pub fn toJSTag(this: CommandTag, globalObject: *JSC.JSGlobalObject) JSValue { - return switch (this) { - .INSERT => JSValue.jsNumber(1), - .DELETE => JSValue.jsNumber(2), - .UPDATE => JSValue.jsNumber(3), - .MERGE => JSValue.jsNumber(4), - .SELECT => JSValue.jsNumber(5), - .MOVE => JSValue.jsNumber(6), - .FETCH => JSValue.jsNumber(7), - .COPY => JSValue.jsNumber(8), - .other => |tag| JSC.ZigString.init(tag).toJS(globalObject), - }; - } - - pub fn toJSNumber(this: CommandTag) JSValue { - return switch (this) { - .other => JSValue.jsNumber(0), - inline else => |val| JSValue.jsNumber(val), - }; - } - - const KnownCommand = enum { - INSERT, - DELETE, - UPDATE, - MERGE, - SELECT, - MOVE, - FETCH, - COPY, - - pub const Map = bun.ComptimeEnumMap(KnownCommand); - }; - - pub fn init(tag: []const u8) CommandTag { - const first_space_index = bun.strings.indexOfChar(tag, ' ') orelse return .{ .other = tag }; - const cmd = KnownCommand.Map.get(tag[0..first_space_index]) orelse return .{ - .other = tag, - }; - - const number = brk: { - switch (cmd) { - .INSERT => { - var remaining = tag[@min(first_space_index + 1, tag.len)..]; - const second_space = bun.strings.indexOfChar(remaining, ' ') orelse return .{ .other = tag }; - remaining = remaining[@min(second_space + 1, remaining.len)..]; - break :brk std.fmt.parseInt(u64, remaining, 0) catch |err| { - debug("CommandTag failed to parse number: {s}", .{@errorName(err)}); - return .{ .other = tag }; - }; - }, - else => { - const after_tag = tag[@min(first_space_index + 1, tag.len)..]; - break :brk std.fmt.parseInt(u64, after_tag, 0) catch |err| { - debug("CommandTag failed to parse number: {s}", .{@errorName(err)}); - return .{ .other = tag }; - }; - }, - } - }; - - switch (cmd) { - inline else => |t| return @unionInit(CommandTag, @tagName(t), number), - } - } - }; - - pub fn allowGC(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { - if (thisValue == .zero) { - return; - } - - defer thisValue.ensureStillAlive(); - js.bindingSetCached(thisValue, globalObject, .zero); - js.pendingValueSetCached(thisValue, globalObject, .zero); - js.targetSetCached(thisValue, globalObject, .zero); - } - - fn consumePendingValue(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) ?JSValue { - const pending_value = js.pendingValueGetCached(thisValue) orelse return null; - js.pendingValueSetCached(thisValue, globalObject, .zero); - return pending_value; - } - - pub fn onResult(this: *@This(), command_tag_str: []const u8, globalObject: *JSC.JSGlobalObject, connection: JSC.JSValue, is_last: bool) void { - this.ref(); - defer this.deref(); - - const thisValue = this.thisValue.get(); - const targetValue = this.getTarget(globalObject, is_last); - if (is_last) { - this.status = .success; - } else { - this.status = .partial_response; - } - defer if (is_last) { - allowGC(thisValue, globalObject); - this.thisValue.deinit(); - }; - if (thisValue == .zero or targetValue == .zero) { - return; - } - - const vm = JSC.VirtualMachine.get(); - const function = vm.rareData().postgresql_context.onQueryResolveFn.get().?; - const event_loop = vm.eventLoop(); - const tag = CommandTag.init(command_tag_str); - - event_loop.runCallback(function, globalObject, thisValue, &.{ - targetValue, - consumePendingValue(thisValue, globalObject) orelse .js_undefined, - tag.toJSTag(globalObject), - tag.toJSNumber(), - if (connection == .zero) .js_undefined else PostgresSQLConnection.js.queriesGetCached(connection) orelse .js_undefined, - JSValue.jsBoolean(is_last), - }); - } - - pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*PostgresSQLQuery { - _ = callframe; - return globalThis.throw("PostgresSQLQuery cannot be constructed directly", .{}); - } - - pub fn estimatedSize(this: *PostgresSQLQuery) usize { - _ = this; - return @sizeOf(PostgresSQLQuery); - } - - pub fn call(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(6).slice(); - var args = JSC.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const query = args.nextEat() orelse { - return globalThis.throw("query must be a string", .{}); - }; - const values = args.nextEat() orelse { - return globalThis.throw("values must be an array", .{}); - }; - - if (!query.isString()) { - return globalThis.throw("query must be a string", .{}); - } - - if (values.jsType() != .Array) { - return globalThis.throw("values must be an array", .{}); - } - - const pending_value: JSValue = args.nextEat() orelse .js_undefined; - const columns: JSValue = args.nextEat() orelse .js_undefined; - const js_bigint: JSValue = args.nextEat() orelse .false; - const js_simple: JSValue = args.nextEat() orelse .false; - - const bigint = js_bigint.isBoolean() and js_bigint.asBoolean(); - const simple = js_simple.isBoolean() and js_simple.asBoolean(); - if (simple) { - if (try values.getLength(globalThis) > 0) { - return globalThis.throwInvalidArguments("simple query cannot have parameters", .{}); - } - if (try query.getLength(globalThis) >= std.math.maxInt(i32)) { - return globalThis.throwInvalidArguments("query is too long", .{}); - } - } - if (!pending_value.jsType().isArrayLike()) { - return globalThis.throwInvalidArgumentType("query", "pendingValue", "Array"); - } - - var ptr = try bun.default_allocator.create(PostgresSQLQuery); - - const this_value = ptr.toJS(globalThis); - this_value.ensureStillAlive(); - - ptr.* = .{ - .query = try query.toBunString(globalThis), - .thisValue = JSRef.initWeak(this_value), - .flags = .{ - .bigint = bigint, - .simple = simple, - }, - }; - - js.bindingSetCached(this_value, globalThis, values); - js.pendingValueSetCached(this_value, globalThis, pending_value); - if (!columns.isUndefined()) { - js.columnsSetCached(this_value, globalThis, columns); - } - - return this_value; - } - - pub fn push(this: *PostgresSQLQuery, globalThis: *JSC.JSGlobalObject, value: JSValue) void { - var pending_value = this.pending_value.get() orelse return; - pending_value.push(globalThis, value); - } - - pub fn doDone(this: *@This(), globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - _ = globalObject; - this.flags.is_done = true; - return .js_undefined; - } - pub fn setPendingValue(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const result = callframe.argument(0); - js.pendingValueSetCached(this.thisValue.get(), globalObject, result); - return .js_undefined; - } - pub fn setMode(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const js_mode = callframe.argument(0); - if (js_mode.isEmptyOrUndefinedOrNull() or !js_mode.isNumber()) { - return globalObject.throwInvalidArgumentType("setMode", "mode", "Number"); - } - - const mode = try js_mode.coerce(i32, globalObject); - this.flags.result_mode = std.meta.intToEnum(PostgresSQLQueryResultMode, mode) catch { - return globalObject.throwInvalidArgumentTypeValue("mode", "Number", js_mode); - }; - return .js_undefined; - } - - pub fn doRun(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - var arguments_ = callframe.arguments_old(2); - const arguments = arguments_.slice(); - const connection: *PostgresSQLConnection = arguments[0].as(PostgresSQLConnection) orelse { - return globalObject.throw("connection must be a PostgresSQLConnection", .{}); - }; - - connection.poll_ref.ref(globalObject.bunVM()); - var query = arguments[1]; - - if (!query.isObject()) { - return globalObject.throwInvalidArgumentType("run", "query", "Query"); - } - - const this_value = callframe.this(); - const binding_value = js.bindingGetCached(this_value) orelse .zero; - var query_str = this.query.toUTF8(bun.default_allocator); - defer query_str.deinit(); - var writer = connection.writer(); - - if (this.flags.simple) { - debug("executeQuery", .{}); - - const can_execute = !connection.hasQueryRunning(); - if (can_execute) { - PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, writer) catch |err| { - if (!globalObject.hasException()) - return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to execute query", err)); - return error.JSError; - }; - connection.flags.is_ready_for_query = false; - this.status = .running; - } else { - this.status = .pending; - } - const stmt = bun.default_allocator.create(PostgresSQLStatement) catch { - return globalObject.throwOutOfMemory(); - }; - // Query is simple and it's the only owner of the statement - stmt.* = .{ - .signature = Signature.empty(), - .ref_count = 1, - .status = .parsing, - }; - this.statement = stmt; - // We need a strong reference to the query so that it doesn't get GC'd - connection.requests.writeItem(this) catch return globalObject.throwOutOfMemory(); - this.ref(); - this.thisValue.upgrade(globalObject); - - js.targetSetCached(this_value, globalObject, query); - if (this.status == .running) { - connection.flushDataAndResetTimeout(); - } else { - connection.resetConnectionTimeout(); - } - return .js_undefined; - } - - const columns_value: JSValue = js.columnsGetCached(this_value) orelse .js_undefined; - - var signature = Signature.generate(globalObject, query_str.slice(), binding_value, columns_value, connection.prepared_statement_id, connection.flags.use_unnamed_prepared_statements) catch |err| { - if (!globalObject.hasException()) - return globalObject.throwError(err, "failed to generate signature"); - return error.JSError; - }; - - const has_params = signature.fields.len > 0; - var did_write = false; - enqueue: { - var connection_entry_value: ?**PostgresSQLStatement = null; - if (!connection.flags.use_unnamed_prepared_statements) { - const entry = connection.statements.getOrPut(bun.default_allocator, bun.hash(signature.name)) catch |err| { - signature.deinit(); - return globalObject.throwError(err, "failed to allocate statement"); - }; - connection_entry_value = entry.value_ptr; - if (entry.found_existing) { - this.statement = connection_entry_value.?.*; - this.statement.?.ref(); - signature.deinit(); - - switch (this.statement.?.status) { - .failed => { - // If the statement failed, we need to throw the error - return globalObject.throwValue(this.statement.?.error_response.?.toJS(globalObject)); - }, - .prepared => { - if (!connection.hasQueryRunning()) { - this.flags.binary = this.statement.?.fields.len > 0; - debug("bindAndExecute", .{}); - - // bindAndExecute will bind + execute, it will change to running after binding is complete - PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, columns_value, PostgresSQLConnection.Writer, writer) catch |err| { - if (!globalObject.hasException()) - return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to bind and execute query", err)); - return error.JSError; - }; - connection.flags.is_ready_for_query = false; - this.status = .binding; - - did_write = true; - } - }, - .parsing, .pending => {}, - } - - break :enqueue; - } - } - const can_execute = !connection.hasQueryRunning(); - - if (can_execute) { - // If it does not have params, we can write and execute immediately in one go - if (!has_params) { - debug("prepareAndQueryWithSignature", .{}); - // prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete - PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| { - signature.deinit(); - if (!globalObject.hasException()) - return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to prepare and query", err)); - return error.JSError; - }; - connection.flags.is_ready_for_query = false; - this.status = .binding; - did_write = true; - } else { - debug("writeQuery", .{}); - - PostgresRequest.writeQuery(query_str.slice(), signature.prepared_statement_name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| { - signature.deinit(); - if (!globalObject.hasException()) - return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to write query", err)); - return error.JSError; - }; - writer.write(&protocol.Sync) catch |err| { - signature.deinit(); - if (!globalObject.hasException()) - return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to flush", err)); - return error.JSError; - }; - connection.flags.is_ready_for_query = false; - did_write = true; - } - } - { - const stmt = bun.default_allocator.create(PostgresSQLStatement) catch { - return globalObject.throwOutOfMemory(); - }; - // we only have connection_entry_value if we are using named prepared statements - if (connection_entry_value) |entry_value| { - connection.prepared_statement_id += 1; - stmt.* = .{ .signature = signature, .ref_count = 2, .status = if (can_execute) .parsing else .pending }; - this.statement = stmt; - - entry_value.* = stmt; - } else { - stmt.* = .{ .signature = signature, .ref_count = 1, .status = if (can_execute) .parsing else .pending }; - this.statement = stmt; - } - } - } - // We need a strong reference to the query so that it doesn't get GC'd - connection.requests.writeItem(this) catch return globalObject.throwOutOfMemory(); - this.ref(); - this.thisValue.upgrade(globalObject); - - js.targetSetCached(this_value, globalObject, query); - if (did_write) { - connection.flushDataAndResetTimeout(); - } else { - connection.resetConnectionTimeout(); - } - return .js_undefined; - } - - pub fn doCancel(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - _ = callframe; - _ = globalObject; - _ = this; - - return .js_undefined; - } - - comptime { - const jscall = JSC.toJSHostFn(call); - @export(&jscall, .{ .name = "PostgresSQLQuery__createInstance" }); - } -}; - -pub const PostgresRequest = struct { - pub fn writeBind( - name: []const u8, - cursor_name: bun.String, - globalObject: *JSC.JSGlobalObject, - values_array: JSValue, - columns_value: JSValue, - parameter_fields: []const int4, - result_fields: []const protocol.FieldDescription, - comptime Context: type, - writer: protocol.NewWriter(Context), - ) !void { - try writer.write("B"); - const length = try writer.length(); - - try writer.String(cursor_name); - try writer.string(name); - - const len: u32 = @truncate(parameter_fields.len); - - // The number of parameter format codes that follow (denoted C - // below). This can be zero to indicate that there are no - // parameters or that the parameters all use the default format - // (text); or one, in which case the specified format code is - // applied to all parameters; or it can equal the actual number - // of parameters. - try writer.short(len); - - var iter = try QueryBindingIterator.init(values_array, columns_value, globalObject); - for (0..len) |i| { - const parameter_field = parameter_fields[i]; - const is_custom_type = std.math.maxInt(short) < parameter_field; - const tag: types.Tag = if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); - - const force_text = is_custom_type or (tag.isBinaryFormatSupported() and brk: { - iter.to(@truncate(i)); - if (try iter.next()) |value| { - break :brk value.isString(); - } - if (iter.anyFailed()) { - return error.InvalidQueryBinding; - } - break :brk false; - }); - - if (force_text) { - // If they pass a value as a string, let's avoid attempting to - // convert it to the binary representation. This minimizes the room - // for mistakes on our end, such as stripping the timezone - // differently than what Postgres does when given a timestamp with - // timezone. - try writer.short(0); - continue; - } - - try writer.short( - tag.formatCode(), - ); - } - - // The number of parameter values that follow (possibly zero). This - // must match the number of parameters needed by the query. - try writer.short(len); - - debug("Bind: {} ({d} args)", .{ bun.fmt.quote(name), len }); - iter.to(0); - var i: usize = 0; - while (try iter.next()) |value| : (i += 1) { - const tag: types.Tag = brk: { - if (i >= len) { - // parameter in array but not in parameter_fields - // this is probably a bug a bug in bun lets return .text here so the server will send a error 08P01 - // with will describe better the error saying exactly how many parameters are missing and are expected - // Example: - // SQL error: PostgresError: bind message supplies 0 parameters, but prepared statement "PSELECT * FROM test_table WHERE id=$1 .in$0" requires 1 - // errno: "08P01", - // code: "ERR_POSTGRES_SERVER_ERROR" - break :brk .text; - } - const parameter_field = parameter_fields[i]; - const is_custom_type = std.math.maxInt(short) < parameter_field; - break :brk if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); - }; - if (value.isEmptyOrUndefinedOrNull()) { - debug(" -> NULL", .{}); - // As a special case, -1 indicates a - // NULL parameter value. No value bytes follow in the NULL case. - try writer.int4(@bitCast(@as(i32, -1))); - continue; - } - if (comptime bun.Environment.enable_logs) { - debug(" -> {s}", .{tag.tagName() orelse "(unknown)"}); - } - - switch ( - // If they pass a value as a string, let's avoid attempting to - // convert it to the binary representation. This minimizes the room - // for mistakes on our end, such as stripping the timezone - // differently than what Postgres does when given a timestamp with - // timezone. - if (tag.isBinaryFormatSupported() and value.isString()) .text else tag) { - .jsonb, .json => { - var str = bun.String.empty; - defer str.deref(); - try value.jsonStringify(globalObject, 0, &str); - const slice = str.toUTF8WithoutRef(bun.default_allocator); - defer slice.deinit(); - const l = try writer.length(); - try writer.write(slice.slice()); - try l.writeExcludingSelf(); - }, - .bool => { - const l = try writer.length(); - try writer.write(&[1]u8{@intFromBool(value.toBoolean())}); - try l.writeExcludingSelf(); - }, - .timestamp, .timestamptz => { - const l = try writer.length(); - try writer.int8(types.date.fromJS(globalObject, value)); - try l.writeExcludingSelf(); - }, - .bytea => { - var bytes: []const u8 = ""; - if (value.asArrayBuffer(globalObject)) |buf| { - bytes = buf.byteSlice(); - } - const l = try writer.length(); - debug(" {d} bytes", .{bytes.len}); - - try writer.write(bytes); - try l.writeExcludingSelf(); - }, - .int4 => { - const l = try writer.length(); - try writer.int4(@bitCast(try value.coerceToInt32(globalObject))); - try l.writeExcludingSelf(); - }, - .int4_array => { - const l = try writer.length(); - try writer.int4(@bitCast(try value.coerceToInt32(globalObject))); - try l.writeExcludingSelf(); - }, - .float8 => { - const l = try writer.length(); - try writer.f64(@bitCast(try value.toNumber(globalObject))); - try l.writeExcludingSelf(); - }, - - else => { - const str = try String.fromJS(value, globalObject); - if (str.tag == .Dead) return error.OutOfMemory; - defer str.deref(); - const slice = str.toUTF8WithoutRef(bun.default_allocator); - defer slice.deinit(); - const l = try writer.length(); - try writer.write(slice.slice()); - try l.writeExcludingSelf(); - }, - } - } - - var any_non_text_fields: bool = false; - for (result_fields) |field| { - if (field.typeTag().isBinaryFormatSupported()) { - any_non_text_fields = true; - break; - } - } - - if (any_non_text_fields) { - try writer.short(result_fields.len); - for (result_fields) |field| { - try writer.short( - field.typeTag().formatCode(), - ); - } - } else { - try writer.short(0); - } - - try length.write(); - } - - pub fn writeQuery( - query: []const u8, - name: []const u8, - params: []const int4, - comptime Context: type, - writer: protocol.NewWriter(Context), - ) AnyPostgresError!void { - { - var q = protocol.Parse{ - .name = name, - .params = params, - .query = query, - }; - try q.writeInternal(Context, writer); - debug("Parse: {}", .{bun.fmt.quote(query)}); - } - - { - var d = protocol.Describe{ - .p = .{ - .prepared_statement = name, - }, - }; - try d.writeInternal(Context, writer); - debug("Describe: {}", .{bun.fmt.quote(name)}); - } - } - - pub fn prepareAndQueryWithSignature( - globalObject: *JSC.JSGlobalObject, - query: []const u8, - array_value: JSValue, - comptime Context: type, - writer: protocol.NewWriter(Context), - signature: *Signature, - ) AnyPostgresError!void { - try writeQuery(query, signature.prepared_statement_name, signature.fields, Context, writer); - try writeBind(signature.prepared_statement_name, bun.String.empty, globalObject, array_value, .zero, &.{}, &.{}, Context, writer); - var exec = protocol.Execute{ - .p = .{ - .prepared_statement = signature.prepared_statement_name, - }, - }; - try exec.writeInternal(Context, writer); - - try writer.write(&protocol.Flush); - try writer.write(&protocol.Sync); - } - - pub fn bindAndExecute( - globalObject: *JSC.JSGlobalObject, - statement: *PostgresSQLStatement, - array_value: JSValue, - columns_value: JSValue, - comptime Context: type, - writer: protocol.NewWriter(Context), - ) !void { - try writeBind(statement.signature.prepared_statement_name, bun.String.empty, globalObject, array_value, columns_value, statement.parameters, statement.fields, Context, writer); - var exec = protocol.Execute{ - .p = .{ - .prepared_statement = statement.signature.prepared_statement_name, - }, - }; - try exec.writeInternal(Context, writer); - - try writer.write(&protocol.Flush); - try writer.write(&protocol.Sync); - } - - pub fn executeQuery( - query: []const u8, - comptime Context: type, - writer: protocol.NewWriter(Context), - ) !void { - try protocol.writeQuery(query, Context, writer); - try writer.write(&protocol.Flush); - try writer.write(&protocol.Sync); - } - - pub fn onData( - connection: *PostgresSQLConnection, - comptime Context: type, - reader: protocol.NewReader(Context), - ) !void { - while (true) { - reader.markMessageStart(); - const c = try reader.int(u8); - debug("read: {c}", .{c}); - switch (c) { - 'D' => try connection.on(.DataRow, Context, reader), - 'd' => try connection.on(.CopyData, Context, reader), - 'S' => { - if (connection.tls_status == .message_sent) { - bun.debugAssert(connection.tls_status.message_sent == 8); - connection.tls_status = .ssl_ok; - connection.setupTLS(); - return; - } - - try connection.on(.ParameterStatus, Context, reader); - }, - 'Z' => try connection.on(.ReadyForQuery, Context, reader), - 'C' => try connection.on(.CommandComplete, Context, reader), - '2' => try connection.on(.BindComplete, Context, reader), - '1' => try connection.on(.ParseComplete, Context, reader), - 't' => try connection.on(.ParameterDescription, Context, reader), - 'T' => try connection.on(.RowDescription, Context, reader), - 'R' => try connection.on(.Authentication, Context, reader), - 'n' => try connection.on(.NoData, Context, reader), - 'K' => try connection.on(.BackendKeyData, Context, reader), - 'E' => try connection.on(.ErrorResponse, Context, reader), - 's' => try connection.on(.PortalSuspended, Context, reader), - '3' => try connection.on(.CloseComplete, Context, reader), - 'G' => try connection.on(.CopyInResponse, Context, reader), - 'N' => { - if (connection.tls_status == .message_sent) { - connection.tls_status = .ssl_not_available; - debug("Server does not support SSL", .{}); - if (connection.ssl_mode == .require) { - connection.fail("Server does not support SSL", error.TLSNotAvailable); - return; - } - continue; - } - - try connection.on(.NoticeResponse, Context, reader); - }, - 'I' => try connection.on(.EmptyQueryResponse, Context, reader), - 'H' => try connection.on(.CopyOutResponse, Context, reader), - 'c' => try connection.on(.CopyDone, Context, reader), - 'W' => try connection.on(.CopyBothResponse, Context, reader), - - else => { - debug("Unknown message: {c}", .{c}); - const to_skip = try reader.length() -| 1; - debug("to_skip: {d}", .{to_skip}); - try reader.skip(@intCast(@max(to_skip, 0))); - }, - } - } - } - - pub const Queue = std.fifo.LinearFifo(*PostgresSQLQuery, .Dynamic); -}; - -pub const PostgresSQLConnection = struct { - socket: Socket, - status: Status = Status.connecting, - ref_count: u32 = 1, - - write_buffer: bun.OffsetByteList = .{}, - read_buffer: bun.OffsetByteList = .{}, - last_message_start: u32 = 0, - requests: PostgresRequest.Queue, - - poll_ref: bun.Async.KeepAlive = .{}, - globalObject: *JSC.JSGlobalObject, - - statements: PreparedStatementsMap, - prepared_statement_id: u64 = 0, - pending_activity_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - js_value: JSValue = .js_undefined, - - backend_parameters: bun.StringMap = bun.StringMap.init(bun.default_allocator, true), - backend_key_data: protocol.BackendKeyData = .{}, - - database: []const u8 = "", - user: []const u8 = "", - password: []const u8 = "", - path: []const u8 = "", - options: []const u8 = "", - options_buf: []const u8 = "", - - authentication_state: AuthenticationState = .{ .pending = {} }, - - tls_ctx: ?*uws.SocketContext = null, - tls_config: JSC.API.ServerConfig.SSLConfig = .{}, - tls_status: TLSStatus = .none, - ssl_mode: SSLMode = .disable, - - idle_timeout_interval_ms: u32 = 0, - connection_timeout_ms: u32 = 0, - - flags: ConnectionFlags = .{}, - - /// Before being connected, this is a connection timeout timer. - /// After being connected, this is an idle timeout timer. - timer: bun.api.Timer.EventLoopTimer = .{ - .tag = .PostgresSQLConnectionTimeout, - .next = .{ - .sec = 0, - .nsec = 0, - }, - }, - - /// This timer controls the maximum lifetime of a connection. - /// It starts when the connection successfully starts (i.e. after handshake is complete). - /// It stops when the connection is closed. - max_lifetime_interval_ms: u32 = 0, - max_lifetime_timer: bun.api.Timer.EventLoopTimer = .{ - .tag = .PostgresSQLConnectionMaxLifetime, - .next = .{ - .sec = 0, - .nsec = 0, - }, - }, - - pub const ConnectionFlags = packed struct { - is_ready_for_query: bool = false, - is_processing_data: bool = false, - use_unnamed_prepared_statements: bool = false, - }; - - pub const TLSStatus = union(enum) { - none, - pending, - - /// Number of bytes sent of the 8-byte SSL request message. - /// Since we may send a partial message, we need to know how many bytes were sent. - message_sent: u8, - - ssl_not_available, - ssl_ok, - }; - - pub const AuthenticationState = union(enum) { - pending: void, - none: void, - ok: void, - SASL: SASL, - md5: void, - - pub fn zero(this: *AuthenticationState) void { - switch (this.*) { - .SASL => |*sasl| { - sasl.deinit(); - }, - else => {}, - } - this.* = .{ .none = {} }; - } - }; - - pub const SASL = struct { - const nonce_byte_len = 18; - const nonce_base64_len = bun.base64.encodeLenFromSize(nonce_byte_len); - - const server_signature_byte_len = 32; - const server_signature_base64_len = bun.base64.encodeLenFromSize(server_signature_byte_len); - - const salted_password_byte_len = 32; - - nonce_base64_bytes: [nonce_base64_len]u8 = .{0} ** nonce_base64_len, - nonce_len: u8 = 0, - - server_signature_base64_bytes: [server_signature_base64_len]u8 = .{0} ** server_signature_base64_len, - server_signature_len: u8 = 0, - - salted_password_bytes: [salted_password_byte_len]u8 = .{0} ** salted_password_byte_len, - salted_password_created: bool = false, - - status: SASLStatus = .init, - - pub const SASLStatus = enum { - init, - @"continue", - }; - - fn hmac(password: []const u8, data: []const u8) ?[32]u8 { - var buf = std.mem.zeroes([bun.BoringSSL.c.EVP_MAX_MD_SIZE]u8); - - // TODO: I don't think this is failable. - const result = bun.hmac.generate(password, data, .sha256, &buf) orelse return null; - - assert(result.len == 32); - return buf[0..32].*; - } - - pub fn computeSaltedPassword(this: *SASL, salt_bytes: []const u8, iteration_count: u32, connection: *PostgresSQLConnection) !void { - this.salted_password_created = true; - if (Crypto.EVP.pbkdf2(&this.salted_password_bytes, connection.password, salt_bytes, iteration_count, .sha256) == null) { - return error.PBKDFD2; - } - } - - pub fn saltedPassword(this: *const SASL) []const u8 { - assert(this.salted_password_created); - return this.salted_password_bytes[0..salted_password_byte_len]; - } - - pub fn serverSignature(this: *const SASL) []const u8 { - assert(this.server_signature_len > 0); - return this.server_signature_base64_bytes[0..this.server_signature_len]; - } - - pub fn computeServerSignature(this: *SASL, auth_string: []const u8) !void { - assert(this.server_signature_len == 0); - - const server_key = hmac(this.saltedPassword(), "Server Key") orelse return error.InvalidServerKey; - const server_signature_bytes = hmac(&server_key, auth_string) orelse return error.InvalidServerSignature; - this.server_signature_len = @intCast(bun.base64.encode(&this.server_signature_base64_bytes, &server_signature_bytes)); - } - - pub fn clientKey(this: *const SASL) [32]u8 { - return hmac(this.saltedPassword(), "Client Key").?; - } - - pub fn clientKeySignature(_: *const SASL, client_key: []const u8, auth_string: []const u8) [32]u8 { - var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); - bun.sha.SHA256.hash(client_key, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); - return hmac(&sha_digest, auth_string).?; - } - - pub fn nonce(this: *SASL) []const u8 { - if (this.nonce_len == 0) { - var bytes: [nonce_byte_len]u8 = .{0} ** nonce_byte_len; - bun.csprng(&bytes); - this.nonce_len = @intCast(bun.base64.encode(&this.nonce_base64_bytes, &bytes)); - } - return this.nonce_base64_bytes[0..this.nonce_len]; - } - - pub fn deinit(this: *SASL) void { - this.nonce_len = 0; - this.salted_password_created = false; - this.server_signature_len = 0; - this.status = .init; - } - }; - - pub const Status = enum { - disconnected, - connecting, - // Prevent sending the startup message multiple times. - // Particularly relevant for TLS connections. - sent_startup_message, - connected, - failed, - }; - - pub const js = JSC.Codegen.JSPostgresSQLConnection; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - fn getTimeoutInterval(this: *const PostgresSQLConnection) u32 { - return switch (this.status) { - .connected => this.idle_timeout_interval_ms, - .failed => 0, - else => this.connection_timeout_ms, - }; - } - pub fn disableConnectionTimeout(this: *PostgresSQLConnection) void { - if (this.timer.state == .ACTIVE) { - this.globalObject.bunVM().timer.remove(&this.timer); - } - this.timer.state = .CANCELLED; - } - pub fn resetConnectionTimeout(this: *PostgresSQLConnection) void { - // if we are processing data, don't reset the timeout, wait for the data to be processed - if (this.flags.is_processing_data) return; - const interval = this.getTimeoutInterval(); - if (this.timer.state == .ACTIVE) { - this.globalObject.bunVM().timer.remove(&this.timer); - } - if (interval == 0) { - return; - } - - this.timer.next = bun.timespec.msFromNow(@intCast(interval)); - this.globalObject.bunVM().timer.insert(&this.timer); - } - - pub fn getQueries(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { - if (js.queriesGetCached(thisValue)) |value| { - return value; - } - - const array = try JSC.JSValue.createEmptyArray(globalObject, 0); - js.queriesSetCached(thisValue, globalObject, array); - - return array; - } - - pub fn getOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { - if (js.onconnectGetCached(thisValue)) |value| { - return value; - } - - return .js_undefined; - } - - pub fn setOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { - js.onconnectSetCached(thisValue, globalObject, value); - } - - pub fn getOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { - if (js.oncloseGetCached(thisValue)) |value| { - return value; - } - - return .js_undefined; - } - - pub fn setOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { - js.oncloseSetCached(thisValue, globalObject, value); - } - - pub fn setupTLS(this: *PostgresSQLConnection) void { - debug("setupTLS", .{}); - const new_socket = this.socket.SocketTCP.socket.connected.upgrade(this.tls_ctx.?, this.tls_config.server_name) orelse { - this.fail("Failed to upgrade to TLS", error.TLSUpgradeFailed); - return; - }; - this.socket = .{ - .SocketTLS = .{ - .socket = .{ - .connected = new_socket, - }, - }, - }; - - this.start(); - } - fn setupMaxLifetimeTimerIfNecessary(this: *PostgresSQLConnection) void { - if (this.max_lifetime_interval_ms == 0) return; - if (this.max_lifetime_timer.state == .ACTIVE) return; - - this.max_lifetime_timer.next = bun.timespec.msFromNow(@intCast(this.max_lifetime_interval_ms)); - this.globalObject.bunVM().timer.insert(&this.max_lifetime_timer); - } - - pub fn onConnectionTimeout(this: *PostgresSQLConnection) bun.api.Timer.EventLoopTimer.Arm { - debug("onConnectionTimeout", .{}); - - this.timer.state = .FIRED; - if (this.flags.is_processing_data) { - return .disarm; - } - - if (this.getTimeoutInterval() == 0) { - this.resetConnectionTimeout(); - return .disarm; - } - - switch (this.status) { - .connected => { - this.failFmt(.POSTGRES_IDLE_TIMEOUT, "Idle timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.idle_timeout_interval_ms) *| std.time.ns_per_ms)}); - }, - else => { - this.failFmt(.POSTGRES_CONNECTION_TIMEOUT, "Connection timeout after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); - }, - .sent_startup_message => { - this.failFmt(.POSTGRES_CONNECTION_TIMEOUT, "Connection timed out after {} (sent startup message, but never received response)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); - }, - } - return .disarm; - } - - pub fn onMaxLifetimeTimeout(this: *PostgresSQLConnection) bun.api.Timer.EventLoopTimer.Arm { - debug("onMaxLifetimeTimeout", .{}); - this.max_lifetime_timer.state = .FIRED; - if (this.status == .failed) return .disarm; - this.failFmt(.POSTGRES_LIFETIME_TIMEOUT, "Max lifetime timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.max_lifetime_interval_ms) *| std.time.ns_per_ms)}); - return .disarm; - } - - fn start(this: *PostgresSQLConnection) void { - this.setupMaxLifetimeTimerIfNecessary(); - this.resetConnectionTimeout(); - this.sendStartupMessage(); - - const event_loop = this.globalObject.bunVM().eventLoop(); - event_loop.enter(); - defer event_loop.exit(); - this.flushData(); - } - - pub fn hasPendingActivity(this: *PostgresSQLConnection) bool { - return this.pending_activity_count.load(.acquire) > 0; - } - - fn updateHasPendingActivity(this: *PostgresSQLConnection) void { - const a: u32 = if (this.requests.readableLength() > 0) 1 else 0; - const b: u32 = if (this.status != .disconnected) 1 else 0; - this.pending_activity_count.store(a + b, .release); - } - - pub fn setStatus(this: *PostgresSQLConnection, status: Status) void { - if (this.status == status) return; - defer this.updateHasPendingActivity(); - - this.status = status; - this.resetConnectionTimeout(); - - switch (status) { - .connected => { - const on_connect = this.consumeOnConnectCallback(this.globalObject) orelse return; - const js_value = this.js_value; - js_value.ensureStillAlive(); - this.globalObject.queueMicrotask(on_connect, &[_]JSValue{ JSValue.jsNull(), js_value }); - this.poll_ref.unref(this.globalObject.bunVM()); - }, - else => {}, - } - } - - pub fn finalize(this: *PostgresSQLConnection) void { - debug("PostgresSQLConnection finalize", .{}); - this.stopTimers(); - this.js_value = .zero; - this.deref(); - } - - pub fn flushDataAndResetTimeout(this: *PostgresSQLConnection) void { - this.resetConnectionTimeout(); - this.flushData(); - } - - pub fn flushData(this: *PostgresSQLConnection) void { - const chunk = this.write_buffer.remaining(); - if (chunk.len == 0) return; - const wrote = this.socket.write(chunk); - if (wrote > 0) { - SocketMonitor.write(chunk[0..@intCast(wrote)]); - this.write_buffer.consume(@intCast(wrote)); - } - } - - pub fn failWithJSValue(this: *PostgresSQLConnection, value: JSValue) void { - defer this.updateHasPendingActivity(); - this.stopTimers(); - if (this.status == .failed) return; - - this.status = .failed; - - this.ref(); - defer this.deref(); - // we defer the refAndClose so the on_close will be called first before we reject the pending requests - defer this.refAndClose(value); - const on_close = this.consumeOnCloseCallback(this.globalObject) orelse return; - - const loop = this.globalObject.bunVM().eventLoop(); - loop.enter(); - defer loop.exit(); - _ = on_close.call( - this.globalObject, - this.js_value, - &[_]JSValue{ - value, - this.getQueriesArray(), - }, - ) catch |e| this.globalObject.reportActiveExceptionAsUnhandled(e); - } - - pub fn failFmt(this: *PostgresSQLConnection, comptime error_code: JSC.Error, comptime fmt: [:0]const u8, args: anytype) void { - this.failWithJSValue(error_code.fmt(this.globalObject, fmt, args)); - } - - pub fn fail(this: *PostgresSQLConnection, message: []const u8, err: AnyPostgresError) void { - debug("failed: {s}: {s}", .{ message, @errorName(err) }); - - const globalObject = this.globalObject; - - this.failWithJSValue(postgresErrorToJS(globalObject, message, err)); - } - - pub fn onClose(this: *PostgresSQLConnection) void { - var vm = this.globalObject.bunVM(); - const loop = vm.eventLoop(); - loop.enter(); - defer loop.exit(); - this.poll_ref.unref(this.globalObject.bunVM()); - - this.fail("Connection closed", error.ConnectionClosed); - } - - fn sendStartupMessage(this: *PostgresSQLConnection) void { - if (this.status != .connecting) return; - debug("sendStartupMessage", .{}); - this.status = .sent_startup_message; - var msg = protocol.StartupMessage{ - .user = Data{ .temporary = this.user }, - .database = Data{ .temporary = this.database }, - .options = Data{ .temporary = this.options }, - }; - msg.writeInternal(Writer, this.writer()) catch |err| { - this.fail("Failed to write startup message", err); - }; - } - - fn startTLS(this: *PostgresSQLConnection, socket: uws.AnySocket) void { - debug("startTLS", .{}); - const offset = switch (this.tls_status) { - .message_sent => |count| count, - else => 0, - }; - const ssl_request = [_]u8{ - 0x00, 0x00, 0x00, 0x08, // Length - 0x04, 0xD2, 0x16, 0x2F, // SSL request code - }; - - const written = socket.write(ssl_request[offset..]); - if (written > 0) { - this.tls_status = .{ - .message_sent = offset + @as(u8, @intCast(written)), - }; - } else { - this.tls_status = .{ - .message_sent = offset, - }; - } - } - - pub fn onOpen(this: *PostgresSQLConnection, socket: uws.AnySocket) void { - this.socket = socket; - - this.poll_ref.ref(this.globalObject.bunVM()); - this.updateHasPendingActivity(); - - if (this.tls_status == .message_sent or this.tls_status == .pending) { - this.startTLS(socket); - return; - } - - this.start(); - } - - pub fn onHandshake(this: *PostgresSQLConnection, success: i32, ssl_error: uws.us_bun_verify_error_t) void { - debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no }); - const handshake_success = if (success == 1) true else false; - if (handshake_success) { - if (this.tls_config.reject_unauthorized != 0) { - // only reject the connection if reject_unauthorized == true - switch (this.ssl_mode) { - // https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279 - - .verify_ca, .verify_full => { - if (ssl_error.error_no != 0) { - this.failWithJSValue(ssl_error.toJS(this.globalObject)); - return; - } - - const ssl_ptr: *BoringSSL.c.SSL = @ptrCast(this.socket.getNativeHandle()); - if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| { - const hostname = servername[0..bun.len(servername)]; - if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { - this.failWithJSValue(ssl_error.toJS(this.globalObject)); - } - } - }, - else => { - return; - }, - } - } - } else { - // if we are here is because server rejected us, and the error_no is the cause of this - // no matter if reject_unauthorized is false because we are disconnected by the server - this.failWithJSValue(ssl_error.toJS(this.globalObject)); - } - } - - pub fn onTimeout(this: *PostgresSQLConnection) void { - _ = this; - debug("onTimeout", .{}); - } - - pub fn onDrain(this: *PostgresSQLConnection) void { - - // Don't send any other messages while we're waiting for TLS. - if (this.tls_status == .message_sent) { - if (this.tls_status.message_sent < 8) { - this.startTLS(this.socket); - } - - return; - } - - const event_loop = this.globalObject.bunVM().eventLoop(); - event_loop.enter(); - defer event_loop.exit(); - this.flushData(); - } - - pub fn onData(this: *PostgresSQLConnection, data: []const u8) void { - this.ref(); - this.flags.is_processing_data = true; - const vm = this.globalObject.bunVM(); - - this.disableConnectionTimeout(); - defer { - if (this.status == .connected and !this.hasQueryRunning() and this.write_buffer.remaining().len == 0) { - // Don't keep the process alive when there's nothing to do. - this.poll_ref.unref(vm); - } else if (this.status == .connected) { - // Keep the process alive if there's something to do. - this.poll_ref.ref(vm); - } - this.flags.is_processing_data = false; - - // reset the connection timeout after we're done processing the data - this.resetConnectionTimeout(); - this.deref(); - } - - const event_loop = vm.eventLoop(); - event_loop.enter(); - defer event_loop.exit(); - SocketMonitor.read(data); - // reset the head to the last message so remaining reflects the right amount of bytes - this.read_buffer.head = this.last_message_start; - - if (this.read_buffer.remaining().len == 0) { - var consumed: usize = 0; - var offset: usize = 0; - const reader = protocol.StackReader.init(data, &consumed, &offset); - PostgresRequest.onData(this, protocol.StackReader, reader) catch |err| { - if (err == error.ShortRead) { - if (comptime bun.Environment.allow_assert) { - debug("read_buffer: empty and received short read: last_message_start: {d}, head: {d}, len: {d}", .{ - offset, - consumed, - data.len, - }); - } - - this.read_buffer.head = 0; - this.last_message_start = 0; - this.read_buffer.byte_list.len = 0; - this.read_buffer.write(bun.default_allocator, data[offset..]) catch @panic("failed to write to read buffer"); - } else { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - this.fail("Failed to read data", err); - } - }; - // no need to reset anything, its already empty - return; - } - // read buffer is not empty, so we need to write the data to the buffer and then read it - this.read_buffer.write(bun.default_allocator, data) catch @panic("failed to write to read buffer"); - PostgresRequest.onData(this, Reader, this.bufferedReader()) catch |err| { - if (err != error.ShortRead) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - this.fail("Failed to read data", err); - return; - } - - if (comptime bun.Environment.allow_assert) { - debug("read_buffer: not empty and received short read: last_message_start: {d}, head: {d}, len: {d}", .{ - this.last_message_start, - this.read_buffer.head, - this.read_buffer.byte_list.len, - }); - } - return; - }; - - debug("clean read_buffer", .{}); - // success, we read everything! let's reset the last message start and the head - this.last_message_start = 0; - this.read_buffer.head = 0; - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*PostgresSQLConnection { - _ = callframe; - return globalObject.throw("PostgresSQLConnection cannot be constructed directly", .{}); - } - - comptime { - const jscall = JSC.toJSHostFn(call); - @export(&jscall, .{ .name = "PostgresSQLConnection__createInstance" }); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var vm = globalObject.bunVM(); - const arguments = callframe.arguments_old(15).slice(); - const hostname_str = try arguments[0].toBunString(globalObject); - defer hostname_str.deref(); - const port = try arguments[1].coerce(i32, globalObject); - - const username_str = try arguments[2].toBunString(globalObject); - defer username_str.deref(); - const password_str = try arguments[3].toBunString(globalObject); - defer password_str.deref(); - const database_str = try arguments[4].toBunString(globalObject); - defer database_str.deref(); - const ssl_mode: SSLMode = switch (arguments[5].toInt32()) { - 0 => .disable, - 1 => .prefer, - 2 => .require, - 3 => .verify_ca, - 4 => .verify_full, - else => .disable, - }; - - const tls_object = arguments[6]; - - var tls_config: JSC.API.ServerConfig.SSLConfig = .{}; - var tls_ctx: ?*uws.SocketContext = null; - if (ssl_mode != .disable) { - tls_config = if (tls_object.isBoolean() and tls_object.toBoolean()) - .{} - else if (tls_object.isObject()) - (JSC.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls_object) catch return .zero) orelse .{} - else { - return globalObject.throwInvalidArguments("tls must be a boolean or an object", .{}); - }; - - if (globalObject.hasException()) { - tls_config.deinit(); - return .zero; - } - - // we always request the cert so we can verify it and also we manually abort the connection if the hostname doesn't match - const original_reject_unauthorized = tls_config.reject_unauthorized; - tls_config.reject_unauthorized = 0; - tls_config.request_cert = 1; - // We create it right here so we can throw errors early. - const context_options = tls_config.asUSockets(); - var err: uws.create_bun_socket_error_t = .none; - tls_ctx = uws.SocketContext.createSSLContext(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), context_options, &err) orelse { - if (err != .none) { - return globalObject.throw("failed to create TLS context", .{}); - } else { - return globalObject.throwValue(err.toJS(globalObject)); - } - }; - // restore the original reject_unauthorized - tls_config.reject_unauthorized = original_reject_unauthorized; - if (err != .none) { - tls_config.deinit(); - if (tls_ctx) |ctx| { - ctx.deinit(true); - } - return globalObject.throwValue(err.toJS(globalObject)); - } - - uws.NewSocketHandler(true).configure(tls_ctx.?, true, *PostgresSQLConnection, SocketHandler(true)); - } - - var username: []const u8 = ""; - var password: []const u8 = ""; - var database: []const u8 = ""; - var options: []const u8 = ""; - var path: []const u8 = ""; - - const options_str = try arguments[7].toBunString(globalObject); - defer options_str.deref(); - - const path_str = try arguments[8].toBunString(globalObject); - defer path_str.deref(); - - const options_buf: []u8 = brk: { - var b = bun.StringBuilder{}; - b.cap += username_str.utf8ByteLength() + 1 + password_str.utf8ByteLength() + 1 + database_str.utf8ByteLength() + 1 + options_str.utf8ByteLength() + 1 + path_str.utf8ByteLength() + 1; - - b.allocate(bun.default_allocator) catch {}; - var u = username_str.toUTF8WithoutRef(bun.default_allocator); - defer u.deinit(); - username = b.append(u.slice()); - - var p = password_str.toUTF8WithoutRef(bun.default_allocator); - defer p.deinit(); - password = b.append(p.slice()); - - var d = database_str.toUTF8WithoutRef(bun.default_allocator); - defer d.deinit(); - database = b.append(d.slice()); - - var o = options_str.toUTF8WithoutRef(bun.default_allocator); - defer o.deinit(); - options = b.append(o.slice()); - - var _path = path_str.toUTF8WithoutRef(bun.default_allocator); - defer _path.deinit(); - path = b.append(_path.slice()); - - break :brk b.allocatedSlice(); - }; - - const on_connect = arguments[9]; - const on_close = arguments[10]; - const idle_timeout = arguments[11].toInt32(); - const connection_timeout = arguments[12].toInt32(); - const max_lifetime = arguments[13].toInt32(); - const use_unnamed_prepared_statements = arguments[14].asBoolean(); - - const ptr: *PostgresSQLConnection = try bun.default_allocator.create(PostgresSQLConnection); - - ptr.* = PostgresSQLConnection{ - .globalObject = globalObject, - - .database = database, - .user = username, - .password = password, - .path = path, - .options = options, - .options_buf = options_buf, - .socket = .{ .SocketTCP = .{ .socket = .{ .detached = {} } } }, - .requests = PostgresRequest.Queue.init(bun.default_allocator), - .statements = PreparedStatementsMap{}, - .tls_config = tls_config, - .tls_ctx = tls_ctx, - .ssl_mode = ssl_mode, - .tls_status = if (ssl_mode != .disable) .pending else .none, - .idle_timeout_interval_ms = @intCast(idle_timeout), - .connection_timeout_ms = @intCast(connection_timeout), - .max_lifetime_interval_ms = @intCast(max_lifetime), - .flags = .{ - .use_unnamed_prepared_statements = use_unnamed_prepared_statements, - }, - }; - - ptr.updateHasPendingActivity(); - ptr.poll_ref.ref(vm); - const js_value = ptr.toJS(globalObject); - js_value.ensureStillAlive(); - ptr.js_value = js_value; - - js.onconnectSetCached(js_value, globalObject, on_connect); - js.oncloseSetCached(js_value, globalObject, on_close); - bun.analytics.Features.postgres_connections += 1; - - { - const hostname = hostname_str.toUTF8(bun.default_allocator); - defer hostname.deinit(); - - const ctx = vm.rareData().postgresql_context.tcp orelse brk: { - const ctx_ = uws.SocketContext.createNoSSLContext(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection)).?; - uws.NewSocketHandler(false).configure(ctx_, true, *PostgresSQLConnection, SocketHandler(false)); - vm.rareData().postgresql_context.tcp = ctx_; - break :brk ctx_; - }; - - if (path.len > 0) { - ptr.socket = .{ - .SocketTCP = uws.SocketTCP.connectUnixAnon(path, ctx, ptr, false) catch |err| { - tls_config.deinit(); - if (tls_ctx) |tls| { - tls.deinit(true); - } - ptr.deinit(); - return globalObject.throwError(err, "failed to connect to postgresql"); - }, - }; - } else { - ptr.socket = .{ - .SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| { - tls_config.deinit(); - if (tls_ctx) |tls| { - tls.deinit(true); - } - ptr.deinit(); - return globalObject.throwError(err, "failed to connect to postgresql"); - }, - }; - } - ptr.resetConnectionTimeout(); - } - - return js_value; - } - - fn SocketHandler(comptime ssl: bool) type { - return struct { - const SocketType = uws.NewSocketHandler(ssl); - fn _socket(s: SocketType) Socket { - if (comptime ssl) { - return Socket{ .SocketTLS = s }; - } - - return Socket{ .SocketTCP = s }; - } - pub fn onOpen(this: *PostgresSQLConnection, socket: SocketType) void { - this.onOpen(_socket(socket)); - } - - fn onHandshake_(this: *PostgresSQLConnection, _: anytype, success: i32, ssl_error: uws.us_bun_verify_error_t) void { - this.onHandshake(success, ssl_error); - } - - pub const onHandshake = if (ssl) onHandshake_ else null; - - pub fn onClose(this: *PostgresSQLConnection, socket: SocketType, _: i32, _: ?*anyopaque) void { - _ = socket; - this.onClose(); - } - - pub fn onEnd(this: *PostgresSQLConnection, socket: SocketType) void { - _ = socket; - this.onClose(); - } - - pub fn onConnectError(this: *PostgresSQLConnection, socket: SocketType, _: i32) void { - _ = socket; - this.onClose(); - } - - pub fn onTimeout(this: *PostgresSQLConnection, socket: SocketType) void { - _ = socket; - this.onTimeout(); - } - - pub fn onData(this: *PostgresSQLConnection, socket: SocketType, data: []const u8) void { - _ = socket; - this.onData(data); - } - - pub fn onWritable(this: *PostgresSQLConnection, socket: SocketType) void { - _ = socket; - this.onDrain(); - } - }; - } - - pub fn ref(this: *@This()) void { - bun.assert(this.ref_count > 0); - this.ref_count += 1; - } - - pub fn doRef(this: *@This(), _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.poll_ref.ref(this.globalObject.bunVM()); - this.updateHasPendingActivity(); - return .js_undefined; - } - - pub fn doUnref(this: *@This(), _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.poll_ref.unref(this.globalObject.bunVM()); - this.updateHasPendingActivity(); - return .js_undefined; - } - pub fn doFlush(this: *PostgresSQLConnection, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - this.flushData(); - return .js_undefined; - } - - pub fn deref(this: *@This()) void { - const ref_count = this.ref_count; - this.ref_count -= 1; - - if (ref_count == 1) { - this.disconnect(); - this.deinit(); - } - } - - pub fn doClose(this: *@This(), globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - _ = globalObject; - this.disconnect(); - this.write_buffer.deinit(bun.default_allocator); - - return .js_undefined; - } - - pub fn stopTimers(this: *PostgresSQLConnection) void { - if (this.timer.state == .ACTIVE) { - this.globalObject.bunVM().timer.remove(&this.timer); - } - if (this.max_lifetime_timer.state == .ACTIVE) { - this.globalObject.bunVM().timer.remove(&this.max_lifetime_timer); - } - } - - pub fn deinit(this: *@This()) void { - this.stopTimers(); - var iter = this.statements.valueIterator(); - while (iter.next()) |stmt_ptr| { - var stmt = stmt_ptr.*; - stmt.deref(); - } - this.statements.deinit(bun.default_allocator); - this.write_buffer.deinit(bun.default_allocator); - this.read_buffer.deinit(bun.default_allocator); - this.backend_parameters.deinit(); - - bun.freeSensitive(bun.default_allocator, this.options_buf); - - this.tls_config.deinit(); - bun.default_allocator.destroy(this); - } - - fn refAndClose(this: *@This(), js_reason: ?JSC.JSValue) void { - // refAndClose is always called when we wanna to disconnect or when we are closed - - if (!this.socket.isClosed()) { - // event loop need to be alive to close the socket - this.poll_ref.ref(this.globalObject.bunVM()); - // will unref on socket close - this.socket.close(); - } - - // cleanup requests - while (this.current()) |request| { - switch (request.status) { - // pending we will fail the request and the stmt will be marked as error ConnectionClosed too - .pending => { - const stmt = request.statement orelse continue; - stmt.error_response = .{ .postgres_error = AnyPostgresError.ConnectionClosed }; - stmt.status = .failed; - if (js_reason) |reason| { - request.onJSError(reason, this.globalObject); - } else { - request.onError(.{ .postgres_error = AnyPostgresError.ConnectionClosed }, this.globalObject); - } - }, - // in the middle of running - .binding, - .running, - .partial_response, - => { - if (js_reason) |reason| { - request.onJSError(reason, this.globalObject); - } else { - request.onError(.{ .postgres_error = AnyPostgresError.ConnectionClosed }, this.globalObject); - } - }, - // just ignore success and fail cases - .success, .fail => {}, - } - request.deref(); - this.requests.discard(1); - } - } - - pub fn disconnect(this: *@This()) void { - this.stopTimers(); - - if (this.status == .connected) { - this.status = .disconnected; - this.refAndClose(null); - } - } - - fn current(this: *PostgresSQLConnection) ?*PostgresSQLQuery { - if (this.requests.readableLength() == 0) { - return null; - } - - return this.requests.peekItem(0); - } - - fn hasQueryRunning(this: *PostgresSQLConnection) bool { - return !this.flags.is_ready_for_query or this.current() != null; - } - - pub const Writer = struct { - connection: *PostgresSQLConnection, - - pub fn write(this: Writer, data: []const u8) AnyPostgresError!void { - var buffer = &this.connection.write_buffer; - try buffer.write(bun.default_allocator, data); - } - - pub fn pwrite(this: Writer, data: []const u8, index: usize) AnyPostgresError!void { - @memcpy(this.connection.write_buffer.byte_list.slice()[index..][0..data.len], data); - } - - pub fn offset(this: Writer) usize { - return this.connection.write_buffer.len(); - } - }; - - pub fn writer(this: *PostgresSQLConnection) protocol.NewWriter(Writer) { - return .{ - .wrapped = .{ - .connection = this, - }, - }; - } - - pub const Reader = struct { - connection: *PostgresSQLConnection, - - pub fn markMessageStart(this: Reader) void { - this.connection.last_message_start = this.connection.read_buffer.head; - } - - pub const ensureLength = ensureCapacity; - - pub fn peek(this: Reader) []const u8 { - return this.connection.read_buffer.remaining(); - } - pub fn skip(this: Reader, count: usize) void { - this.connection.read_buffer.head = @min(this.connection.read_buffer.head + @as(u32, @truncate(count)), this.connection.read_buffer.byte_list.len); - } - pub fn ensureCapacity(this: Reader, count: usize) bool { - return @as(usize, this.connection.read_buffer.head) + count <= @as(usize, this.connection.read_buffer.byte_list.len); - } - pub fn read(this: Reader, count: usize) AnyPostgresError!Data { - var remaining = this.connection.read_buffer.remaining(); - if (@as(usize, remaining.len) < count) { - return error.ShortRead; - } - - this.skip(count); - return Data{ - .temporary = remaining[0..count], - }; - } - pub fn readZ(this: Reader) AnyPostgresError!Data { - const remain = this.connection.read_buffer.remaining(); - - if (bun.strings.indexOfChar(remain, 0)) |zero| { - this.skip(zero + 1); - return Data{ - .temporary = remain[0..zero], - }; - } - - return error.ShortRead; - } - }; - - pub fn bufferedReader(this: *PostgresSQLConnection) protocol.NewReader(Reader) { - return .{ - .wrapped = .{ .connection = this }, - }; - } - - fn advance(this: *PostgresSQLConnection) !void { - while (this.requests.readableLength() > 0) { - var req: *PostgresSQLQuery = this.requests.peekItem(0); - switch (req.status) { - .pending => { - if (req.flags.simple) { - debug("executeQuery", .{}); - var query_str = req.query.toUTF8(bun.default_allocator); - defer query_str.deinit(); - PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, this.writer()) catch |err| { - req.onWriteFail(err, this.globalObject, this.getQueriesArray()); - req.deref(); - this.requests.discard(1); - - continue; - }; - this.flags.is_ready_for_query = false; - req.status = .running; - return; - } else { - const stmt = req.statement orelse return error.ExpectedStatement; - - switch (stmt.status) { - .failed => { - bun.assert(stmt.error_response != null); - req.onError(stmt.error_response.?, this.globalObject); - req.deref(); - this.requests.discard(1); - - continue; - }, - .prepared => { - const thisValue = req.thisValue.get(); - bun.assert(thisValue != .zero); - const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero; - const columns_value = PostgresSQLQuery.js.columnsGetCached(thisValue) orelse .zero; - req.flags.binary = stmt.fields.len > 0; - - PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| { - req.onWriteFail(err, this.globalObject, this.getQueriesArray()); - req.deref(); - this.requests.discard(1); - - continue; - }; - this.flags.is_ready_for_query = false; - req.status = .binding; - return; - }, - .pending => { - // statement is pending, lets write/parse it - var query_str = req.query.toUTF8(bun.default_allocator); - defer query_str.deinit(); - const has_params = stmt.signature.fields.len > 0; - // If it does not have params, we can write and execute immediately in one go - if (!has_params) { - const thisValue = req.thisValue.get(); - bun.assert(thisValue != .zero); - // prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete - const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero; - PostgresRequest.prepareAndQueryWithSignature(this.globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, this.writer(), &stmt.signature) catch |err| { - stmt.status = .failed; - stmt.error_response = .{ .postgres_error = err }; - req.onWriteFail(err, this.globalObject, this.getQueriesArray()); - req.deref(); - this.requests.discard(1); - - continue; - }; - this.flags.is_ready_for_query = false; - req.status = .binding; - stmt.status = .parsing; - - return; - } - const connection_writer = this.writer(); - // write query and wait for it to be prepared - PostgresRequest.writeQuery(query_str.slice(), stmt.signature.prepared_statement_name, stmt.signature.fields, PostgresSQLConnection.Writer, connection_writer) catch |err| { - stmt.error_response = .{ .postgres_error = err }; - stmt.status = .failed; - - req.onWriteFail(err, this.globalObject, this.getQueriesArray()); - req.deref(); - this.requests.discard(1); - - continue; - }; - connection_writer.write(&protocol.Sync) catch |err| { - stmt.error_response = .{ .postgres_error = err }; - stmt.status = .failed; - - req.onWriteFail(err, this.globalObject, this.getQueriesArray()); - req.deref(); - this.requests.discard(1); - - continue; - }; - this.flags.is_ready_for_query = false; - stmt.status = .parsing; - return; - }, - .parsing => { - // we are still parsing, lets wait for it to be prepared or failed - return; - }, - } - } - }, - - .running, .binding, .partial_response => { - // if we are binding it will switch to running immediately - // if we are running, we need to wait for it to be success or fail - return; - }, - .success, .fail => { - req.deref(); - this.requests.discard(1); - continue; - }, - } - } - } - - pub fn getQueriesArray(this: *const PostgresSQLConnection) JSValue { - return js.queriesGetCached(this.js_value) orelse .zero; - } - - pub const DataCell = @import("./DataCell.zig").DataCell; - - pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.enum_literal), comptime Context: type, reader: protocol.NewReader(Context)) AnyPostgresError!void { - debug("on({s})", .{@tagName(MessageType)}); - - switch (comptime MessageType) { - .DataRow => { - const request = this.current() orelse return error.ExpectedRequest; - var statement = request.statement orelse return error.ExpectedStatement; - var structure: JSValue = .js_undefined; - var cached_structure: ?PostgresCachedStructure = null; - // explicit use switch without else so if new modes are added, we don't forget to check for duplicate fields - switch (request.flags.result_mode) { - .objects => { - cached_structure = statement.structure(this.js_value, this.globalObject); - structure = cached_structure.?.jsValue() orelse .js_undefined; - }, - .raw, .values => { - // no need to check for duplicate fields or structure - }, - } - - var putter = DataCell.Putter{ - .list = &.{}, - .fields = statement.fields, - .binary = request.flags.binary, - .bigint = request.flags.bigint, - .globalObject = this.globalObject, - }; - - var stack_buf: [70]DataCell = undefined; - var cells: []DataCell = stack_buf[0..@min(statement.fields.len, JSC.JSObject.maxInlineCapacity())]; - var free_cells = false; - defer { - for (cells[0..putter.count]) |*cell| { - cell.deinit(); - } - if (free_cells) bun.default_allocator.free(cells); - } - - if (statement.fields.len >= JSC.JSObject.maxInlineCapacity()) { - cells = try bun.default_allocator.alloc(DataCell, statement.fields.len); - free_cells = true; - } - // make sure all cells are reset if reader short breaks the fields will just be null with is better than undefined behavior - @memset(cells, DataCell{ .tag = .null, .value = .{ .null = 0 } }); - putter.list = cells; - - if (request.flags.result_mode == .raw) { - try protocol.DataRow.decode( - &putter, - Context, - reader, - DataCell.Putter.putRaw, - ); - } else { - try protocol.DataRow.decode( - &putter, - Context, - reader, - DataCell.Putter.put, - ); - } - const thisValue = request.thisValue.get(); - bun.assert(thisValue != .zero); - const pending_value = PostgresSQLQuery.js.pendingValueGetCached(thisValue) orelse .zero; - pending_value.ensureStillAlive(); - const result = putter.toJS(this.globalObject, pending_value, structure, statement.fields_flags, request.flags.result_mode, cached_structure); - - if (pending_value == .zero) { - PostgresSQLQuery.js.pendingValueSetCached(thisValue, this.globalObject, result); - } - }, - .CopyData => { - var copy_data: protocol.CopyData = undefined; - try copy_data.decodeInternal(Context, reader); - copy_data.data.deinit(); - }, - .ParameterStatus => { - var parameter_status: protocol.ParameterStatus = undefined; - try parameter_status.decodeInternal(Context, reader); - defer { - parameter_status.deinit(); - } - try this.backend_parameters.insert(parameter_status.name.slice(), parameter_status.value.slice()); - }, - .ReadyForQuery => { - var ready_for_query: protocol.ReadyForQuery = undefined; - try ready_for_query.decodeInternal(Context, reader); - - this.setStatus(.connected); - this.flags.is_ready_for_query = true; - this.socket.setTimeout(300); - defer this.updateRef(); - - if (this.current()) |request| { - if (request.status == .partial_response) { - // if is a partial response, just signal that the query is now complete - request.onResult("", this.globalObject, this.js_value, true); - } - } - try this.advance(); - - this.flushData(); - }, - .CommandComplete => { - var request = this.current() orelse return error.ExpectedRequest; - - var cmd: protocol.CommandComplete = undefined; - try cmd.decodeInternal(Context, reader); - defer { - cmd.deinit(); - } - debug("-> {s}", .{cmd.command_tag.slice()}); - defer this.updateRef(); - - if (request.flags.simple) { - // simple queries can have multiple commands - request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, false); - } else { - request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, true); - } - }, - .BindComplete => { - try reader.eatMessage(protocol.BindComplete); - var request = this.current() orelse return error.ExpectedRequest; - if (request.status == .binding) { - request.status = .running; - } - }, - .ParseComplete => { - try reader.eatMessage(protocol.ParseComplete); - const request = this.current() orelse return error.ExpectedRequest; - if (request.statement) |statement| { - // if we have params wait for parameter description - if (statement.status == .parsing and statement.signature.fields.len == 0) { - statement.status = .prepared; - } - } - }, - .ParameterDescription => { - var description: protocol.ParameterDescription = undefined; - try description.decodeInternal(Context, reader); - const request = this.current() orelse return error.ExpectedRequest; - var statement = request.statement orelse return error.ExpectedStatement; - statement.parameters = description.parameters; - if (statement.status == .parsing) { - statement.status = .prepared; - } - }, - .RowDescription => { - var description: protocol.RowDescription = undefined; - try description.decodeInternal(Context, reader); - errdefer description.deinit(); - const request = this.current() orelse return error.ExpectedRequest; - var statement = request.statement orelse return error.ExpectedStatement; - statement.fields = description.fields; - }, - .Authentication => { - var auth: protocol.Authentication = undefined; - try auth.decodeInternal(Context, reader); - defer auth.deinit(); - - switch (auth) { - .SASL => { - if (this.authentication_state != .SASL) { - this.authentication_state = .{ .SASL = .{} }; - } - - var mechanism_buf: [128]u8 = undefined; - const mechanism = std.fmt.bufPrintZ(&mechanism_buf, "n,,n=*,r={s}", .{this.authentication_state.SASL.nonce()}) catch unreachable; - var response = protocol.SASLInitialResponse{ - .mechanism = .{ - .temporary = "SCRAM-SHA-256", - }, - .data = .{ - .temporary = mechanism, - }, - }; - - try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); - debug("SASL", .{}); - this.flushData(); - }, - .SASLContinue => |*cont| { - if (this.authentication_state != .SASL) { - debug("Unexpected SASLContinue for authentiation state: {s}", .{@tagName(std.meta.activeTag(this.authentication_state))}); - return error.UnexpectedMessage; - } - var sasl = &this.authentication_state.SASL; - - if (sasl.status != .init) { - debug("Unexpected SASLContinue for SASL state: {s}", .{@tagName(sasl.status)}); - return error.UnexpectedMessage; - } - debug("SASLContinue", .{}); - - const iteration_count = try cont.iterationCount(); - - const server_salt_decoded_base64 = bun.base64.decodeAlloc(bun.z_allocator, cont.s) catch |err| { - return switch (err) { - error.DecodingFailed => error.SASL_SIGNATURE_INVALID_BASE64, - else => |e| e, - }; - }; - defer bun.z_allocator.free(server_salt_decoded_base64); - try sasl.computeSaltedPassword(server_salt_decoded_base64, iteration_count, this); - - const auth_string = try std.fmt.allocPrint( - bun.z_allocator, - "n=*,r={s},r={s},s={s},i={s},c=biws,r={s}", - .{ - sasl.nonce(), - cont.r, - cont.s, - cont.i, - cont.r, - }, - ); - defer bun.z_allocator.free(auth_string); - try sasl.computeServerSignature(auth_string); - - const client_key = sasl.clientKey(); - const client_key_signature = sasl.clientKeySignature(&client_key, auth_string); - var client_key_xor_buffer: [32]u8 = undefined; - for (&client_key_xor_buffer, client_key, client_key_signature) |*out, a, b| { - out.* = a ^ b; - } - - var client_key_xor_base64_buf = std.mem.zeroes([bun.base64.encodeLenFromSize(32)]u8); - const xor_base64_len = bun.base64.encode(&client_key_xor_base64_buf, &client_key_xor_buffer); - - const payload = try std.fmt.allocPrint( - bun.z_allocator, - "c=biws,r={s},p={s}", - .{ cont.r, client_key_xor_base64_buf[0..xor_base64_len] }, - ); - defer bun.z_allocator.free(payload); - - var response = protocol.SASLResponse{ - .data = .{ - .temporary = payload, - }, - }; - - try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); - sasl.status = .@"continue"; - this.flushData(); - }, - .SASLFinal => |final| { - if (this.authentication_state != .SASL) { - debug("SASLFinal - Unexpected SASLContinue for authentiation state: {s}", .{@tagName(std.meta.activeTag(this.authentication_state))}); - return error.UnexpectedMessage; - } - var sasl = &this.authentication_state.SASL; - - if (sasl.status != .@"continue") { - debug("SASLFinal - Unexpected SASLContinue for SASL state: {s}", .{@tagName(sasl.status)}); - return error.UnexpectedMessage; - } - - if (sasl.server_signature_len == 0) { - debug("SASLFinal - Server signature is empty", .{}); - return error.UnexpectedMessage; - } - - const server_signature = sasl.serverSignature(); - - // This will usually start with "v=" - const comparison_signature = final.data.slice(); - - if (comparison_signature.len < 2 or !bun.strings.eqlLong(server_signature, comparison_signature[2..], true)) { - debug("SASLFinal - SASL Server signature mismatch\nExpected: {s}\nActual: {s}", .{ server_signature, comparison_signature[2..] }); - this.fail("The server did not return the correct signature", error.SASL_SIGNATURE_MISMATCH); - } else { - debug("SASLFinal - SASL Server signature match", .{}); - this.authentication_state.zero(); - } - }, - .Ok => { - debug("Authentication OK", .{}); - this.authentication_state.zero(); - this.authentication_state = .{ .ok = {} }; - }, - - .Unknown => { - this.fail("Unknown authentication method", error.UNKNOWN_AUTHENTICATION_METHOD); - }, - - .ClearTextPassword => { - debug("ClearTextPassword", .{}); - var response = protocol.PasswordMessage{ - .password = .{ - .temporary = this.password, - }, - }; - - try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); - this.flushData(); - }, - - .MD5Password => |md5| { - debug("MD5Password", .{}); - // Format is: md5 + md5(md5(password + username) + salt) - var first_hash_buf: bun.sha.MD5.Digest = undefined; - var first_hash_str: [32]u8 = undefined; - var final_hash_buf: bun.sha.MD5.Digest = undefined; - var final_hash_str: [32]u8 = undefined; - var final_password_buf: [36]u8 = undefined; - - // First hash: md5(password + username) - var first_hasher = bun.sha.MD5.init(); - first_hasher.update(this.password); - first_hasher.update(this.user); - first_hasher.final(&first_hash_buf); - const first_hash_str_output = std.fmt.bufPrint(&first_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&first_hash_buf)}) catch unreachable; - - // Second hash: md5(first_hash + salt) - var final_hasher = bun.sha.MD5.init(); - final_hasher.update(first_hash_str_output); - final_hasher.update(&md5.salt); - final_hasher.final(&final_hash_buf); - const final_hash_str_output = std.fmt.bufPrint(&final_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&final_hash_buf)}) catch unreachable; - - // Format final password as "md5" + final_hash - const final_password = std.fmt.bufPrintZ(&final_password_buf, "md5{s}", .{final_hash_str_output}) catch unreachable; - - var response = protocol.PasswordMessage{ - .password = .{ - .temporary = final_password, - }, - }; - - this.authentication_state = .{ .md5 = {} }; - try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); - this.flushData(); - }, - - else => { - debug("TODO auth: {s}", .{@tagName(std.meta.activeTag(auth))}); - this.fail("TODO: support authentication method: {s}", error.UNSUPPORTED_AUTHENTICATION_METHOD); - }, - } - }, - .NoData => { - try reader.eatMessage(protocol.NoData); - var request = this.current() orelse return error.ExpectedRequest; - if (request.status == .binding) { - request.status = .running; - } - }, - .BackendKeyData => { - try this.backend_key_data.decodeInternal(Context, reader); - }, - .ErrorResponse => { - var err: protocol.ErrorResponse = undefined; - try err.decodeInternal(Context, reader); - - if (this.status == .connecting or this.status == .sent_startup_message) { - defer { - err.deinit(); - } - - this.failWithJSValue(err.toJS(this.globalObject)); - - // it shouldn't enqueue any requests while connecting - bun.assert(this.requests.count == 0); - return; - } - - var request = this.current() orelse { - debug("ErrorResponse: {}", .{err}); - return error.ExpectedRequest; - }; - var is_error_owned = true; - defer { - if (is_error_owned) { - err.deinit(); - } - } - if (request.statement) |stmt| { - if (stmt.status == PostgresSQLStatement.Status.parsing) { - stmt.status = PostgresSQLStatement.Status.failed; - stmt.error_response = .{ .protocol = err }; - is_error_owned = false; - if (this.statements.remove(bun.hash(stmt.signature.name))) { - stmt.deref(); - } - } - } - this.updateRef(); - - request.onError(.{ .protocol = err }, this.globalObject); - }, - .PortalSuspended => { - // try reader.eatMessage(&protocol.PortalSuspended); - // var request = this.current() orelse return error.ExpectedRequest; - // _ = request; - debug("TODO PortalSuspended", .{}); - }, - .CloseComplete => { - try reader.eatMessage(protocol.CloseComplete); - var request = this.current() orelse return error.ExpectedRequest; - defer this.updateRef(); - if (request.flags.simple) { - request.onResult("CLOSECOMPLETE", this.globalObject, this.js_value, false); - } else { - request.onResult("CLOSECOMPLETE", this.globalObject, this.js_value, true); - } - }, - .CopyInResponse => { - debug("TODO CopyInResponse", .{}); - }, - .NoticeResponse => { - debug("UNSUPPORTED NoticeResponse", .{}); - var resp: protocol.NoticeResponse = undefined; - - try resp.decodeInternal(Context, reader); - resp.deinit(); - }, - .EmptyQueryResponse => { - try reader.eatMessage(protocol.EmptyQueryResponse); - var request = this.current() orelse return error.ExpectedRequest; - defer this.updateRef(); - if (request.flags.simple) { - request.onResult("", this.globalObject, this.js_value, false); - } else { - request.onResult("", this.globalObject, this.js_value, true); - } - }, - .CopyOutResponse => { - debug("TODO CopyOutResponse", .{}); - }, - .CopyDone => { - debug("TODO CopyDone", .{}); - }, - .CopyBothResponse => { - debug("TODO CopyBothResponse", .{}); - }, - else => @compileError("Unknown message type: " ++ @tagName(MessageType)), - } - } - - pub fn updateRef(this: *PostgresSQLConnection) void { - this.updateHasPendingActivity(); - if (this.pending_activity_count.raw > 0) { - this.poll_ref.ref(this.globalObject.bunVM()); - } else { - this.poll_ref.unref(this.globalObject.bunVM()); - } - } - - pub fn getConnected(this: *PostgresSQLConnection, _: *JSC.JSGlobalObject) JSValue { - return JSValue.jsBoolean(this.status == Status.connected); - } - - pub fn consumeOnConnectCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { - debug("consumeOnConnectCallback", .{}); - const on_connect = js.onconnectGetCached(this.js_value) orelse return null; - debug("consumeOnConnectCallback exists", .{}); - - js.onconnectSetCached(this.js_value, globalObject, .zero); - return on_connect; - } - - pub fn consumeOnCloseCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { - debug("consumeOnCloseCallback", .{}); - const on_close = js.oncloseGetCached(this.js_value) orelse return null; - debug("consumeOnCloseCallback exists", .{}); - js.oncloseSetCached(this.js_value, globalObject, .zero); - return on_close; - } -}; - -pub const PostgresCachedStructure = struct { - structure: JSC.Strong.Optional = .empty, - // only populated if more than JSC.JSC__JSObject__maxInlineCapacity fields otherwise the structure will contain all fields inlined - fields: ?[]JSC.JSObject.ExternColumnIdentifier = null, - - pub fn has(this: *@This()) bool { - return this.structure.has() or this.fields != null; - } - - pub fn jsValue(this: *const @This()) ?JSC.JSValue { - return this.structure.get(); - } - - pub fn set(this: *@This(), globalObject: *JSC.JSGlobalObject, value: ?JSC.JSValue, fields: ?[]JSC.JSObject.ExternColumnIdentifier) void { - if (value) |v| { - this.structure.set(globalObject, v); - } - this.fields = fields; - } - - pub fn deinit(this: *@This()) void { - this.structure.deinit(); - if (this.fields) |fields| { - this.fields = null; - for (fields) |*name| { - name.deinit(); - } - bun.default_allocator.free(fields); - } - } -}; -pub const PostgresSQLStatement = struct { - cached_structure: PostgresCachedStructure = .{}, - ref_count: u32 = 1, - fields: []protocol.FieldDescription = &[_]protocol.FieldDescription{}, - parameters: []const int4 = &[_]int4{}, - signature: Signature, - status: Status = Status.pending, - error_response: ?Error = null, - needs_duplicate_check: bool = true, - fields_flags: PostgresSQLConnection.DataCell.Flags = .{}, - - pub const Error = union(enum) { - protocol: protocol.ErrorResponse, - postgres_error: AnyPostgresError, - - pub fn deinit(this: *@This()) void { - switch (this.*) { - .protocol => |*err| err.deinit(), - .postgres_error => {}, - } - } - - pub fn toJS(this: *const @This(), globalObject: *JSC.JSGlobalObject) JSValue { - return switch (this.*) { - .protocol => |err| err.toJS(globalObject), - .postgres_error => |err| postgresErrorToJS(globalObject, null, err), - }; - } - }; - pub const Status = enum { - pending, - parsing, - prepared, - failed, - - pub fn isRunning(this: @This()) bool { - return this == .parsing; - } - }; - pub fn ref(this: *@This()) void { - bun.assert(this.ref_count > 0); - this.ref_count += 1; - } - - pub fn deref(this: *@This()) void { - const ref_count = this.ref_count; - this.ref_count -= 1; - - if (ref_count == 1) { - this.deinit(); - } - } - - pub fn checkForDuplicateFields(this: *PostgresSQLStatement) void { - if (!this.needs_duplicate_check) return; - this.needs_duplicate_check = false; - - var seen_numbers = std.ArrayList(u32).init(bun.default_allocator); - defer seen_numbers.deinit(); - var seen_fields = bun.StringHashMap(void).init(bun.default_allocator); - seen_fields.ensureUnusedCapacity(@intCast(this.fields.len)) catch bun.outOfMemory(); - defer seen_fields.deinit(); - - // iterate backwards - var remaining = this.fields.len; - var flags: PostgresSQLConnection.DataCell.Flags = .{}; - while (remaining > 0) { - remaining -= 1; - const field: *protocol.FieldDescription = &this.fields[remaining]; - switch (field.name_or_index) { - .name => |*name| { - const seen = seen_fields.getOrPut(name.slice()) catch unreachable; - if (seen.found_existing) { - field.name_or_index = .duplicate; - flags.has_duplicate_columns = true; - } - - flags.has_named_columns = true; - }, - .index => |index| { - if (std.mem.indexOfScalar(u32, seen_numbers.items, index) != null) { - field.name_or_index = .duplicate; - flags.has_duplicate_columns = true; - } else { - seen_numbers.append(index) catch bun.outOfMemory(); - } - - flags.has_indexed_columns = true; - }, - .duplicate => { - flags.has_duplicate_columns = true; - }, - } - } - - this.fields_flags = flags; - } - - pub fn deinit(this: *PostgresSQLStatement) void { - debug("PostgresSQLStatement deinit", .{}); - - bun.assert(this.ref_count == 0); - - for (this.fields) |*field| { - field.deinit(); - } - bun.default_allocator.free(this.fields); - bun.default_allocator.free(this.parameters); - this.cached_structure.deinit(); - if (this.error_response) |err| { - this.error_response = null; - var _error = err; - _error.deinit(); - } - this.signature.deinit(); - bun.default_allocator.destroy(this); - } - - pub fn structure(this: *PostgresSQLStatement, owner: JSValue, globalObject: *JSC.JSGlobalObject) PostgresCachedStructure { - if (this.cached_structure.has()) { - return this.cached_structure; - } - this.checkForDuplicateFields(); - - // lets avoid most allocations - var stack_ids: [70]JSC.JSObject.ExternColumnIdentifier = undefined; - // lets de duplicate the fields early - var nonDuplicatedCount = this.fields.len; - for (this.fields) |*field| { - if (field.name_or_index == .duplicate) { - nonDuplicatedCount -= 1; - } - } - const ids = if (nonDuplicatedCount <= JSC.JSObject.maxInlineCapacity()) stack_ids[0..nonDuplicatedCount] else bun.default_allocator.alloc(JSC.JSObject.ExternColumnIdentifier, nonDuplicatedCount) catch bun.outOfMemory(); - - var i: usize = 0; - for (this.fields) |*field| { - if (field.name_or_index == .duplicate) continue; - - var id: *JSC.JSObject.ExternColumnIdentifier = &ids[i]; - switch (field.name_or_index) { - .name => |name| { - id.value.name = String.createAtomIfPossible(name.slice()); - }, - .index => |index| { - id.value.index = index; - }, - .duplicate => unreachable, - } - id.tag = switch (field.name_or_index) { - .name => 2, - .index => 1, - .duplicate => 0, - }; - i += 1; - } - - if (nonDuplicatedCount > JSC.JSObject.maxInlineCapacity()) { - this.cached_structure.set(globalObject, null, ids); - } else { - this.cached_structure.set(globalObject, JSC.JSObject.createStructure( - globalObject, - owner, - @truncate(ids.len), - ids.ptr, - ), null); - } - - return this.cached_structure; - } -}; - -const QueryBindingIterator = union(enum) { - array: JSC.JSArrayIterator, - objects: ObjectIterator, - - pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!QueryBindingIterator { - if (columns.isEmptyOrUndefinedOrNull()) { - return .{ .array = try JSC.JSArrayIterator.init(array, globalObject) }; - } - - return .{ - .objects = .{ - .array = array, - .columns = columns, - .globalObject = globalObject, - .columns_count = try columns.getLength(globalObject), - .array_length = try array.getLength(globalObject), - }, - }; - } - - pub const ObjectIterator = struct { - array: JSValue, - columns: JSValue = .zero, - globalObject: *JSC.JSGlobalObject, - cell_i: usize = 0, - row_i: usize = 0, - current_row: JSC.JSValue = .zero, - columns_count: usize = 0, - array_length: usize = 0, - any_failed: bool = false, - - pub fn next(this: *ObjectIterator) ?JSC.JSValue { - if (this.row_i >= this.array_length) { - return null; - } - - const cell_i = this.cell_i; - this.cell_i += 1; - const row_i = this.row_i; - - const globalObject = this.globalObject; - - if (this.current_row == .zero) { - this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i)) catch { - this.any_failed = true; - return null; - }; - if (this.current_row.isEmptyOrUndefinedOrNull()) { - return globalObject.throw("Expected a row to be returned at index {d}", .{row_i}) catch null; - } - } - - defer { - if (this.cell_i >= this.columns_count) { - this.cell_i = 0; - this.current_row = .zero; - this.row_i += 1; - } - } - - const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i)) catch { - this.any_failed = true; - return null; - }; - if (property.isUndefined()) { - return globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i }) catch null; - } - - const value = this.current_row.getOwnByValue(globalObject, property); - if (value == .zero or (value != null and value.?.isUndefined())) { - if (!globalObject.hasException()) - return globalObject.throw("Expected a value at index {d} in row {d}", .{ cell_i, row_i }) catch null; - this.any_failed = true; - return null; - } - return value; - } - }; - - pub fn next(this: *QueryBindingIterator) bun.JSError!?JSC.JSValue { - return switch (this.*) { - .array => |*iter| iter.next(), - .objects => |*iter| iter.next(), - }; - } - - pub fn anyFailed(this: *const QueryBindingIterator) bool { - return switch (this.*) { - .array => false, - .objects => |*iter| iter.any_failed, - }; - } - - pub fn to(this: *QueryBindingIterator, index: u32) void { - switch (this.*) { - .array => |*iter| iter.i = index, - .objects => |*iter| { - iter.cell_i = index % iter.columns_count; - iter.row_i = index / iter.columns_count; - iter.current_row = .zero; - }, - } - } - - pub fn reset(this: *QueryBindingIterator) void { - switch (this.*) { - .array => |*iter| { - iter.i = 0; - }, - .objects => |*iter| { - iter.cell_i = 0; - iter.row_i = 0; - iter.current_row = .zero; - }, - } - } -}; - -const Signature = struct { - fields: []const int4, - name: []const u8, - query: []const u8, - prepared_statement_name: []const u8, - - pub fn empty() Signature { - return Signature{ - .fields = &[_]int4{}, - .name = &[_]u8{}, - .query = &[_]u8{}, - .prepared_statement_name = &[_]u8{}, - }; - } - - const log = bun.Output.scoped(.PostgresSignature, false); - pub fn deinit(this: *Signature) void { - if (this.prepared_statement_name.len > 0) { - bun.default_allocator.free(this.prepared_statement_name); - } - if (this.name.len > 0) { - bun.default_allocator.free(this.name); - } - if (this.fields.len > 0) { - bun.default_allocator.free(this.fields); - } - if (this.query.len > 0) { - bun.default_allocator.free(this.query); - } - } - - pub fn hash(this: *const Signature) u64 { - var hasher = std.hash.Wyhash.init(0); - hasher.update(this.name); - hasher.update(std.mem.sliceAsBytes(this.fields)); - return hasher.final(); - } - - pub fn generate(globalObject: *JSC.JSGlobalObject, query: []const u8, array_value: JSValue, columns: JSValue, prepared_statement_id: u64, unnamed: bool) !Signature { - var fields = std.ArrayList(int4).init(bun.default_allocator); - var name = try std.ArrayList(u8).initCapacity(bun.default_allocator, query.len); - - name.appendSliceAssumeCapacity(query); - - errdefer { - fields.deinit(); - name.deinit(); - } - - var iter = try QueryBindingIterator.init(array_value, columns, globalObject); - - while (try iter.next()) |value| { - if (value.isEmptyOrUndefinedOrNull()) { - // Allow postgres to decide the type - try fields.append(0); - try name.appendSlice(".null"); - continue; - } - - const tag = try types.Tag.fromJS(globalObject, value); - - switch (tag) { - .int8 => try name.appendSlice(".int8"), - .int4 => try name.appendSlice(".int4"), - // .int4_array => try name.appendSlice(".int4_array"), - .int2 => try name.appendSlice(".int2"), - .float8 => try name.appendSlice(".float8"), - .float4 => try name.appendSlice(".float4"), - .numeric => try name.appendSlice(".numeric"), - .json, .jsonb => try name.appendSlice(".json"), - .bool => try name.appendSlice(".bool"), - .timestamp => try name.appendSlice(".timestamp"), - .timestamptz => try name.appendSlice(".timestamptz"), - .bytea => try name.appendSlice(".bytea"), - else => try name.appendSlice(".string"), - } - - switch (tag) { - .bool, .int4, .int8, .float8, .int2, .numeric, .float4, .bytea => { - // We decide the type - try fields.append(@intFromEnum(tag)); - }, - else => { - // Allow postgres to decide the type - try fields.append(0); - }, - } - } - - if (iter.anyFailed()) { - return error.InvalidQueryBinding; - } - // max u64 length is 20, max prepared_statement_name length is 63 - const prepared_statement_name = if (unnamed) "" else try std.fmt.allocPrint(bun.default_allocator, "P{s}${d}", .{ name.items[0..@min(40, name.items.len)], prepared_statement_id }); - - return Signature{ - .prepared_statement_name = prepared_statement_name, - .name = name.items, - .fields = fields.items, - .query = try bun.default_allocator.dupe(u8, query), - }; - } -}; - pub fn createBinding(globalObject: *JSC.JSGlobalObject) JSValue { const binding = JSValue.createEmptyObjectWithNullPrototype(globalObject); binding.put(globalObject, ZigString.static("PostgresSQLConnection"), PostgresSQLConnection.js.getConstructor(globalObject)); @@ -3287,6 +17,15 @@ pub fn createBinding(globalObject: *JSC.JSGlobalObject) JSValue { return binding; } -const ZigString = JSC.ZigString; +// @sortImports -const assert = bun.assert; +pub const PostgresSQLConnection = @import("./postgres/PostgresSQLConnection.zig"); +pub const PostgresSQLContext = @import("./postgres/PostgresSQLContext.zig"); +pub const PostgresSQLQuery = @import("./postgres/PostgresSQLQuery.zig"); +const bun = @import("bun"); +pub const protocol = @import("./postgres/PostgresProtocol.zig"); +pub const types = @import("./postgres/PostgresTypes.zig"); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const ZigString = JSC.ZigString; diff --git a/src/sql/postgres/AnyPostgresError.zig b/src/sql/postgres/AnyPostgresError.zig new file mode 100644 index 0000000000..04167ec521 --- /dev/null +++ b/src/sql/postgres/AnyPostgresError.zig @@ -0,0 +1,89 @@ +pub const AnyPostgresError = error{ + ConnectionClosed, + ExpectedRequest, + ExpectedStatement, + InvalidBackendKeyData, + InvalidBinaryData, + InvalidByteSequence, + InvalidByteSequenceForEncoding, + InvalidCharacter, + InvalidMessage, + InvalidMessageLength, + InvalidQueryBinding, + InvalidServerKey, + InvalidServerSignature, + JSError, + MultidimensionalArrayNotSupportedYet, + NullsInArrayNotSupportedYet, + OutOfMemory, + Overflow, + PBKDFD2, + SASL_SIGNATURE_MISMATCH, + SASL_SIGNATURE_INVALID_BASE64, + ShortRead, + TLSNotAvailable, + TLSUpgradeFailed, + UnexpectedMessage, + UNKNOWN_AUTHENTICATION_METHOD, + UNSUPPORTED_AUTHENTICATION_METHOD, + UnsupportedByteaFormat, + UnsupportedIntegerSize, + UnsupportedArrayFormat, + UnsupportedNumericFormat, + UnknownFormatCode, +}; + +pub fn postgresErrorToJS(globalObject: *JSC.JSGlobalObject, message: ?[]const u8, err: AnyPostgresError) JSValue { + const error_code: JSC.Error = switch (err) { + error.ConnectionClosed => .POSTGRES_CONNECTION_CLOSED, + error.ExpectedRequest => .POSTGRES_EXPECTED_REQUEST, + error.ExpectedStatement => .POSTGRES_EXPECTED_STATEMENT, + error.InvalidBackendKeyData => .POSTGRES_INVALID_BACKEND_KEY_DATA, + error.InvalidBinaryData => .POSTGRES_INVALID_BINARY_DATA, + error.InvalidByteSequence => .POSTGRES_INVALID_BYTE_SEQUENCE, + error.InvalidByteSequenceForEncoding => .POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING, + error.InvalidCharacter => .POSTGRES_INVALID_CHARACTER, + error.InvalidMessage => .POSTGRES_INVALID_MESSAGE, + error.InvalidMessageLength => .POSTGRES_INVALID_MESSAGE_LENGTH, + error.InvalidQueryBinding => .POSTGRES_INVALID_QUERY_BINDING, + error.InvalidServerKey => .POSTGRES_INVALID_SERVER_KEY, + error.InvalidServerSignature => .POSTGRES_INVALID_SERVER_SIGNATURE, + error.MultidimensionalArrayNotSupportedYet => .POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET, + error.NullsInArrayNotSupportedYet => .POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET, + error.Overflow => .POSTGRES_OVERFLOW, + error.PBKDFD2 => .POSTGRES_AUTHENTICATION_FAILED_PBKDF2, + error.SASL_SIGNATURE_MISMATCH => .POSTGRES_SASL_SIGNATURE_MISMATCH, + error.SASL_SIGNATURE_INVALID_BASE64 => .POSTGRES_SASL_SIGNATURE_INVALID_BASE64, + error.TLSNotAvailable => .POSTGRES_TLS_NOT_AVAILABLE, + error.TLSUpgradeFailed => .POSTGRES_TLS_UPGRADE_FAILED, + error.UnexpectedMessage => .POSTGRES_UNEXPECTED_MESSAGE, + error.UNKNOWN_AUTHENTICATION_METHOD => .POSTGRES_UNKNOWN_AUTHENTICATION_METHOD, + error.UNSUPPORTED_AUTHENTICATION_METHOD => .POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD, + error.UnsupportedByteaFormat => .POSTGRES_UNSUPPORTED_BYTEA_FORMAT, + error.UnsupportedArrayFormat => .POSTGRES_UNSUPPORTED_ARRAY_FORMAT, + error.UnsupportedIntegerSize => .POSTGRES_UNSUPPORTED_INTEGER_SIZE, + error.UnsupportedNumericFormat => .POSTGRES_UNSUPPORTED_NUMERIC_FORMAT, + error.UnknownFormatCode => .POSTGRES_UNKNOWN_FORMAT_CODE, + error.JSError => { + return globalObject.takeException(error.JSError); + }, + error.OutOfMemory => { + // TODO: add binding for creating an out of memory error? + return globalObject.takeException(globalObject.throwOutOfMemory()); + }, + error.ShortRead => { + bun.unreachablePanic("Assertion failed: ShortRead should be handled by the caller in postgres", .{}); + }, + }; + if (message) |msg| { + return error_code.fmt(globalObject, "{s}", .{msg}); + } + return error_code.fmt(globalObject, "Failed to bind query: {s}", .{@errorName(err)}); +} + +// @sortImports + +const bun = @import("bun"); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/AuthenticationState.zig b/src/sql/postgres/AuthenticationState.zig new file mode 100644 index 0000000000..97d19c0893 --- /dev/null +++ b/src/sql/postgres/AuthenticationState.zig @@ -0,0 +1,21 @@ +pub const AuthenticationState = union(enum) { + pending: void, + none: void, + ok: void, + SASL: SASL, + md5: void, + + pub fn zero(this: *AuthenticationState) void { + switch (this.*) { + .SASL => |*sasl| { + sasl.deinit(); + }, + else => {}, + } + this.* = .{ .none = {} }; + } +}; + +// @sortImports + +const SASL = @import("./SASL.zig"); diff --git a/src/sql/postgres/CommandTag.zig b/src/sql/postgres/CommandTag.zig new file mode 100644 index 0000000000..5c89426eb3 --- /dev/null +++ b/src/sql/postgres/CommandTag.zig @@ -0,0 +1,107 @@ +pub const CommandTag = union(enum) { + // For an INSERT command, the tag is INSERT oid rows, where rows is the + // number of rows inserted. oid used to be the object ID of the inserted + // row if rows was 1 and the target table had OIDs, but OIDs system + // columns are not supported anymore; therefore oid is always 0. + INSERT: u64, + // For a DELETE command, the tag is DELETE rows where rows is the number + // of rows deleted. + DELETE: u64, + // For an UPDATE command, the tag is UPDATE rows where rows is the + // number of rows updated. + UPDATE: u64, + // For a MERGE command, the tag is MERGE rows where rows is the number + // of rows inserted, updated, or deleted. + MERGE: u64, + // For a SELECT or CREATE TABLE AS command, the tag is SELECT rows where + // rows is the number of rows retrieved. + SELECT: u64, + // For a MOVE command, the tag is MOVE rows where rows is the number of + // rows the cursor's position has been changed by. + MOVE: u64, + // For a FETCH command, the tag is FETCH rows where rows is the number + // of rows that have been retrieved from the cursor. + FETCH: u64, + // For a COPY command, the tag is COPY rows where rows is the number of + // rows copied. (Note: the row count appears only in PostgreSQL 8.2 and + // later.) + COPY: u64, + + other: []const u8, + + pub fn toJSTag(this: CommandTag, globalObject: *JSC.JSGlobalObject) JSValue { + return switch (this) { + .INSERT => JSValue.jsNumber(1), + .DELETE => JSValue.jsNumber(2), + .UPDATE => JSValue.jsNumber(3), + .MERGE => JSValue.jsNumber(4), + .SELECT => JSValue.jsNumber(5), + .MOVE => JSValue.jsNumber(6), + .FETCH => JSValue.jsNumber(7), + .COPY => JSValue.jsNumber(8), + .other => |tag| JSC.ZigString.init(tag).toJS(globalObject), + }; + } + + pub fn toJSNumber(this: CommandTag) JSValue { + return switch (this) { + .other => JSValue.jsNumber(0), + inline else => |val| JSValue.jsNumber(val), + }; + } + + const KnownCommand = enum { + INSERT, + DELETE, + UPDATE, + MERGE, + SELECT, + MOVE, + FETCH, + COPY, + + pub const Map = bun.ComptimeEnumMap(KnownCommand); + }; + + pub fn init(tag: []const u8) CommandTag { + const first_space_index = bun.strings.indexOfChar(tag, ' ') orelse return .{ .other = tag }; + const cmd = KnownCommand.Map.get(tag[0..first_space_index]) orelse return .{ + .other = tag, + }; + + const number = brk: { + switch (cmd) { + .INSERT => { + var remaining = tag[@min(first_space_index + 1, tag.len)..]; + const second_space = bun.strings.indexOfChar(remaining, ' ') orelse return .{ .other = tag }; + remaining = remaining[@min(second_space + 1, remaining.len)..]; + break :brk std.fmt.parseInt(u64, remaining, 0) catch |err| { + debug("CommandTag failed to parse number: {s}", .{@errorName(err)}); + return .{ .other = tag }; + }; + }, + else => { + const after_tag = tag[@min(first_space_index + 1, tag.len)..]; + break :brk std.fmt.parseInt(u64, after_tag, 0) catch |err| { + debug("CommandTag failed to parse number: {s}", .{@errorName(err)}); + return .{ .other = tag }; + }; + }, + } + }; + + switch (cmd) { + inline else => |t| return @unionInit(CommandTag, @tagName(t), number), + } + } +}; + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/ConnectionFlags.zig b/src/sql/postgres/ConnectionFlags.zig new file mode 100644 index 0000000000..49ad9d6f90 --- /dev/null +++ b/src/sql/postgres/ConnectionFlags.zig @@ -0,0 +1,7 @@ +pub const ConnectionFlags = packed struct { + is_ready_for_query: bool = false, + is_processing_data: bool = false, + use_unnamed_prepared_statements: bool = false, +}; + +// @sortImports diff --git a/src/sql/postgres/Data.zig b/src/sql/postgres/Data.zig new file mode 100644 index 0000000000..557d00fe49 --- /dev/null +++ b/src/sql/postgres/Data.zig @@ -0,0 +1,67 @@ +pub const Data = union(enum) { + owned: bun.ByteList, + temporary: []const u8, + empty: void, + + pub const Empty: Data = .{ .empty = {} }; + + pub fn toOwned(this: @This()) !bun.ByteList { + return switch (this) { + .owned => this.owned, + .temporary => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.temporary)), + .empty => bun.ByteList.init(&.{}), + }; + } + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .owned => this.owned.deinitWithAllocator(bun.default_allocator), + .temporary => {}, + .empty => {}, + } + } + + /// Zero bytes before deinit + /// Generally, for security reasons. + pub fn zdeinit(this: *@This()) void { + switch (this.*) { + .owned => { + + // Zero bytes before deinit + @memset(this.owned.slice(), 0); + + this.owned.deinitWithAllocator(bun.default_allocator); + }, + .temporary => {}, + .empty => {}, + } + } + + pub fn slice(this: @This()) []const u8 { + return switch (this) { + .owned => this.owned.slice(), + .temporary => this.temporary, + .empty => "", + }; + } + + pub fn substring(this: @This(), start_index: usize, end_index: usize) Data { + return switch (this) { + .owned => .{ .temporary = this.owned.slice()[start_index..end_index] }, + .temporary => .{ .temporary = this.temporary[start_index..end_index] }, + .empty => .{ .empty = {} }, + }; + } + + pub fn sliceZ(this: @This()) [:0]const u8 { + return switch (this) { + .owned => this.owned.slice()[0..this.owned.len :0], + .temporary => this.temporary[0..this.temporary.len :0], + .empty => "", + }; + } +}; + +// @sortImports + +const bun = @import("bun"); diff --git a/src/sql/DataCell.zig b/src/sql/postgres/DataCell.zig similarity index 99% rename from src/sql/DataCell.zig rename to src/sql/postgres/DataCell.zig index 3d841657f4..f2f8dd5f00 100644 --- a/src/sql/DataCell.zig +++ b/src/sql/postgres/DataCell.zig @@ -1085,19 +1085,23 @@ pub const DataCell = extern struct { }; }; +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const PostgresCachedStructure = @import("./PostgresCachedStructure.zig"); +const protocol = @import("./PostgresProtocol.zig"); +const std = @import("std"); +const Data = @import("./Data.zig").Data; +const PostgresSQLQueryResultMode = @import("./PostgresSQLQueryResultMode.zig").PostgresSQLQueryResultMode; + +const types = @import("./PostgresTypes.zig"); +const AnyPostgresError = types.AnyPostgresError; +const int4 = types.int4; +const short = types.short; + const bun = @import("bun"); +const String = bun.String; const JSC = bun.JSC; -const std = @import("std"); const JSValue = JSC.JSValue; -const postgres = @import("./postgres.zig"); -const Data = postgres.Data; -const types = postgres.types; -const String = bun.String; -const int4 = postgres.int4; -const AnyPostgresError = postgres.AnyPostgresError; -const protocol = postgres.protocol; -const PostgresSQLQueryResultMode = postgres.PostgresSQLQueryResultMode; -const PostgresCachedStructure = postgres.PostgresCachedStructure; -const debug = postgres.debug; -const short = postgres.short; diff --git a/src/sql/postgres/DebugSocketMonitorReader.zig b/src/sql/postgres/DebugSocketMonitorReader.zig new file mode 100644 index 0000000000..19a95c58cd --- /dev/null +++ b/src/sql/postgres/DebugSocketMonitorReader.zig @@ -0,0 +1,25 @@ +var file: std.fs.File = undefined; +pub var enabled = false; +pub var check = std.once(load); + +pub fn load() void { + if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR_READER")) |monitor| { + enabled = true; + file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { + enabled = false; + return; + }; + debug("duplicating reads to {s}", .{monitor}); + } +} + +pub fn write(data: []const u8) void { + file.writeAll(data) catch {}; +} + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/sql/postgres/DebugSocketMonitorWriter.zig b/src/sql/postgres/DebugSocketMonitorWriter.zig new file mode 100644 index 0000000000..5dd43cdf79 --- /dev/null +++ b/src/sql/postgres/DebugSocketMonitorWriter.zig @@ -0,0 +1,25 @@ +var file: std.fs.File = undefined; +pub var enabled = false; +pub var check = std.once(load); + +pub fn write(data: []const u8) void { + file.writeAll(data) catch {}; +} + +pub fn load() void { + if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR")) |monitor| { + enabled = true; + file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { + enabled = false; + return; + }; + debug("writing to {s}", .{monitor}); + } +} + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/sql/postgres/ObjectIterator.zig b/src/sql/postgres/ObjectIterator.zig new file mode 100644 index 0000000000..4c8c6be7e9 --- /dev/null +++ b/src/sql/postgres/ObjectIterator.zig @@ -0,0 +1,64 @@ +array: JSValue, +columns: JSValue = .zero, +globalObject: *JSC.JSGlobalObject, +cell_i: usize = 0, +row_i: usize = 0, +current_row: JSC.JSValue = .zero, +columns_count: usize = 0, +array_length: usize = 0, +any_failed: bool = false, + +pub fn next(this: *ObjectIterator) ?JSC.JSValue { + if (this.row_i >= this.array_length) { + return null; + } + + const cell_i = this.cell_i; + this.cell_i += 1; + const row_i = this.row_i; + + const globalObject = this.globalObject; + + if (this.current_row == .zero) { + this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i)) catch { + this.any_failed = true; + return null; + }; + if (this.current_row.isEmptyOrUndefinedOrNull()) { + return globalObject.throw("Expected a row to be returned at index {d}", .{row_i}) catch null; + } + } + + defer { + if (this.cell_i >= this.columns_count) { + this.cell_i = 0; + this.current_row = .zero; + this.row_i += 1; + } + } + + const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i)) catch { + this.any_failed = true; + return null; + }; + if (property.isUndefined()) { + return globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i }) catch null; + } + + const value = this.current_row.getOwnByValue(globalObject, property); + if (value == .zero or (value != null and value.?.isUndefined())) { + if (!globalObject.hasException()) + return globalObject.throw("Expected a value at index {d} in row {d}", .{ cell_i, row_i }) catch null; + this.any_failed = true; + return null; + } + return value; +} + +// @sortImports + +const ObjectIterator = @This(); +const bun = @import("bun"); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/PostgresCachedStructure.zig b/src/sql/postgres/PostgresCachedStructure.zig new file mode 100644 index 0000000000..367e4c19c8 --- /dev/null +++ b/src/sql/postgres/PostgresCachedStructure.zig @@ -0,0 +1,34 @@ +structure: JSC.Strong.Optional = .empty, +// only populated if more than JSC.JSC__JSObject__maxInlineCapacity fields otherwise the structure will contain all fields inlined +fields: ?[]JSC.JSObject.ExternColumnIdentifier = null, + +pub fn has(this: *@This()) bool { + return this.structure.has() or this.fields != null; +} + +pub fn jsValue(this: *const @This()) ?JSC.JSValue { + return this.structure.get(); +} + +pub fn set(this: *@This(), globalObject: *JSC.JSGlobalObject, value: ?JSC.JSValue, fields: ?[]JSC.JSObject.ExternColumnIdentifier) void { + if (value) |v| { + this.structure.set(globalObject, v); + } + this.fields = fields; +} + +pub fn deinit(this: *@This()) void { + this.structure.deinit(); + if (this.fields) |fields| { + this.fields = null; + for (fields) |*name| { + name.deinit(); + } + bun.default_allocator.free(fields); + } +} + +// @sortImports + +const bun = @import("bun"); +const JSC = bun.JSC; diff --git a/src/sql/postgres/PostgresProtocol.zig b/src/sql/postgres/PostgresProtocol.zig new file mode 100644 index 0000000000..8f4ac063aa --- /dev/null +++ b/src/sql/postgres/PostgresProtocol.zig @@ -0,0 +1,63 @@ +pub const CloseComplete = [_]u8{'3'} ++ toBytes(Int32(4)); +pub const EmptyQueryResponse = [_]u8{'I'} ++ toBytes(Int32(4)); +pub const Terminate = [_]u8{'X'} ++ toBytes(Int32(4)); + +pub const BindComplete = [_]u8{'2'} ++ toBytes(Int32(4)); + +pub const ParseComplete = [_]u8{'1'} ++ toBytes(Int32(4)); + +pub const CopyDone = [_]u8{'c'} ++ toBytes(Int32(4)); +pub const Sync = [_]u8{'S'} ++ toBytes(Int32(4)); +pub const Flush = [_]u8{'H'} ++ toBytes(Int32(4)); +pub const SSLRequest = toBytes(Int32(8)) ++ toBytes(Int32(80877103)); +pub const NoData = [_]u8{'n'} ++ toBytes(Int32(4)); + +pub fn writeQuery(query: []const u8, comptime Context: type, writer: NewWriter(Context)) !void { + const count: u32 = @sizeOf((u32)) + @as(u32, @intCast(query.len)) + 1; + const header = [_]u8{ + 'Q', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(query); +} + +// @sortImports + +pub const ArrayList = @import("./protocol/ArrayList.zig"); +pub const BackendKeyData = @import("./protocol/BackendKeyData.zig"); +pub const CommandComplete = @import("./protocol/CommandComplete.zig"); +pub const CopyData = @import("./protocol/CopyData.zig"); +pub const CopyFail = @import("./protocol/CopyFail.zig"); +pub const DataRow = @import("./protocol/DataRow.zig"); +pub const Describe = @import("./protocol/Describe.zig"); +pub const ErrorResponse = @import("./protocol/ErrorResponse.zig"); +pub const Execute = @import("./protocol/Execute.zig"); +pub const FieldDescription = @import("./protocol/FieldDescription.zig"); +pub const NegotiateProtocolVersion = @import("./protocol/NegotiateProtocolVersion.zig"); +pub const NoticeResponse = @import("./protocol/NoticeResponse.zig"); +pub const NotificationResponse = @import("./protocol/NotificationResponse.zig"); +pub const ParameterDescription = @import("./protocol/ParameterDescription.zig"); +pub const ParameterStatus = @import("./protocol/ParameterStatus.zig"); +pub const Parse = @import("./protocol/Parse.zig"); +pub const PasswordMessage = @import("./protocol/PasswordMessage.zig"); +pub const ReadyForQuery = @import("./protocol/ReadyForQuery.zig"); +pub const RowDescription = @import("./protocol/RowDescription.zig"); +pub const SASLInitialResponse = @import("./protocol/SASLInitialResponse.zig"); +pub const SASLResponse = @import("./protocol/SASLResponse.zig"); +pub const StackReader = @import("./protocol/StackReader.zig"); +pub const StartupMessage = @import("./protocol/StartupMessage.zig"); +const std = @import("std"); +const types = @import("./PostgresTypes.zig"); +pub const Authentication = @import("./protocol/Authentication.zig").Authentication; +pub const ColumnIdentifier = @import("./protocol/ColumnIdentifier.zig").ColumnIdentifier; +pub const DecoderWrap = @import("./protocol/DecoderWrap.zig").DecoderWrap; +pub const FieldMessage = @import("./protocol/FieldMessage.zig").FieldMessage; +pub const FieldType = @import("./protocol/FieldType.zig").FieldType; +pub const NewReader = @import("./protocol/NewReader.zig").NewReader; +pub const NewWriter = @import("./protocol/NewWriter.zig").NewWriter; +pub const PortalOrPreparedStatement = @import("./protocol/PortalOrPreparedStatement.zig").PortalOrPreparedStatement; +pub const WriteWrap = @import("./protocol/WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; + +const int_types = @import("./types/int_types.zig"); +const Int32 = int_types.Int32; diff --git a/src/sql/postgres/PostgresRequest.zig b/src/sql/postgres/PostgresRequest.zig new file mode 100644 index 0000000000..c8769f4047 --- /dev/null +++ b/src/sql/postgres/PostgresRequest.zig @@ -0,0 +1,348 @@ +pub fn writeBind( + name: []const u8, + cursor_name: bun.String, + globalObject: *JSC.JSGlobalObject, + values_array: JSValue, + columns_value: JSValue, + parameter_fields: []const int4, + result_fields: []const protocol.FieldDescription, + comptime Context: type, + writer: protocol.NewWriter(Context), +) !void { + try writer.write("B"); + const length = try writer.length(); + + try writer.String(cursor_name); + try writer.string(name); + + const len: u32 = @truncate(parameter_fields.len); + + // The number of parameter format codes that follow (denoted C + // below). This can be zero to indicate that there are no + // parameters or that the parameters all use the default format + // (text); or one, in which case the specified format code is + // applied to all parameters; or it can equal the actual number + // of parameters. + try writer.short(len); + + var iter = try QueryBindingIterator.init(values_array, columns_value, globalObject); + for (0..len) |i| { + const parameter_field = parameter_fields[i]; + const is_custom_type = std.math.maxInt(short) < parameter_field; + const tag: types.Tag = if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); + + const force_text = is_custom_type or (tag.isBinaryFormatSupported() and brk: { + iter.to(@truncate(i)); + if (try iter.next()) |value| { + break :brk value.isString(); + } + if (iter.anyFailed()) { + return error.InvalidQueryBinding; + } + break :brk false; + }); + + if (force_text) { + // If they pass a value as a string, let's avoid attempting to + // convert it to the binary representation. This minimizes the room + // for mistakes on our end, such as stripping the timezone + // differently than what Postgres does when given a timestamp with + // timezone. + try writer.short(0); + continue; + } + + try writer.short( + tag.formatCode(), + ); + } + + // The number of parameter values that follow (possibly zero). This + // must match the number of parameters needed by the query. + try writer.short(len); + + debug("Bind: {} ({d} args)", .{ bun.fmt.quote(name), len }); + iter.to(0); + var i: usize = 0; + while (try iter.next()) |value| : (i += 1) { + const tag: types.Tag = brk: { + if (i >= len) { + // parameter in array but not in parameter_fields + // this is probably a bug a bug in bun lets return .text here so the server will send a error 08P01 + // with will describe better the error saying exactly how many parameters are missing and are expected + // Example: + // SQL error: PostgresError: bind message supplies 0 parameters, but prepared statement "PSELECT * FROM test_table WHERE id=$1 .in$0" requires 1 + // errno: "08P01", + // code: "ERR_POSTGRES_SERVER_ERROR" + break :brk .text; + } + const parameter_field = parameter_fields[i]; + const is_custom_type = std.math.maxInt(short) < parameter_field; + break :brk if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); + }; + if (value.isEmptyOrUndefinedOrNull()) { + debug(" -> NULL", .{}); + // As a special case, -1 indicates a + // NULL parameter value. No value bytes follow in the NULL case. + try writer.int4(@bitCast(@as(i32, -1))); + continue; + } + if (comptime bun.Environment.enable_logs) { + debug(" -> {s}", .{tag.tagName() orelse "(unknown)"}); + } + + switch ( + // If they pass a value as a string, let's avoid attempting to + // convert it to the binary representation. This minimizes the room + // for mistakes on our end, such as stripping the timezone + // differently than what Postgres does when given a timestamp with + // timezone. + if (tag.isBinaryFormatSupported() and value.isString()) .text else tag) { + .jsonb, .json => { + var str = bun.String.empty; + defer str.deref(); + try value.jsonStringify(globalObject, 0, &str); + const slice = str.toUTF8WithoutRef(bun.default_allocator); + defer slice.deinit(); + const l = try writer.length(); + try writer.write(slice.slice()); + try l.writeExcludingSelf(); + }, + .bool => { + const l = try writer.length(); + try writer.write(&[1]u8{@intFromBool(value.toBoolean())}); + try l.writeExcludingSelf(); + }, + .timestamp, .timestamptz => { + const l = try writer.length(); + try writer.int8(types.date.fromJS(globalObject, value)); + try l.writeExcludingSelf(); + }, + .bytea => { + var bytes: []const u8 = ""; + if (value.asArrayBuffer(globalObject)) |buf| { + bytes = buf.byteSlice(); + } + const l = try writer.length(); + debug(" {d} bytes", .{bytes.len}); + + try writer.write(bytes); + try l.writeExcludingSelf(); + }, + .int4 => { + const l = try writer.length(); + try writer.int4(@bitCast(try value.coerceToInt32(globalObject))); + try l.writeExcludingSelf(); + }, + .int4_array => { + const l = try writer.length(); + try writer.int4(@bitCast(try value.coerceToInt32(globalObject))); + try l.writeExcludingSelf(); + }, + .float8 => { + const l = try writer.length(); + try writer.f64(@bitCast(try value.toNumber(globalObject))); + try l.writeExcludingSelf(); + }, + + else => { + const str = try String.fromJS(value, globalObject); + if (str.tag == .Dead) return error.OutOfMemory; + defer str.deref(); + const slice = str.toUTF8WithoutRef(bun.default_allocator); + defer slice.deinit(); + const l = try writer.length(); + try writer.write(slice.slice()); + try l.writeExcludingSelf(); + }, + } + } + + var any_non_text_fields: bool = false; + for (result_fields) |field| { + if (field.typeTag().isBinaryFormatSupported()) { + any_non_text_fields = true; + break; + } + } + + if (any_non_text_fields) { + try writer.short(result_fields.len); + for (result_fields) |field| { + try writer.short( + field.typeTag().formatCode(), + ); + } + } else { + try writer.short(0); + } + + try length.write(); +} + +pub fn writeQuery( + query: []const u8, + name: []const u8, + params: []const int4, + comptime Context: type, + writer: protocol.NewWriter(Context), +) AnyPostgresError!void { + { + var q = protocol.Parse{ + .name = name, + .params = params, + .query = query, + }; + try q.writeInternal(Context, writer); + debug("Parse: {}", .{bun.fmt.quote(query)}); + } + + { + var d = protocol.Describe{ + .p = .{ + .prepared_statement = name, + }, + }; + try d.writeInternal(Context, writer); + debug("Describe: {}", .{bun.fmt.quote(name)}); + } +} + +pub fn prepareAndQueryWithSignature( + globalObject: *JSC.JSGlobalObject, + query: []const u8, + array_value: JSValue, + comptime Context: type, + writer: protocol.NewWriter(Context), + signature: *Signature, +) AnyPostgresError!void { + try writeQuery(query, signature.prepared_statement_name, signature.fields, Context, writer); + try writeBind(signature.prepared_statement_name, bun.String.empty, globalObject, array_value, .zero, &.{}, &.{}, Context, writer); + var exec = protocol.Execute{ + .p = .{ + .prepared_statement = signature.prepared_statement_name, + }, + }; + try exec.writeInternal(Context, writer); + + try writer.write(&protocol.Flush); + try writer.write(&protocol.Sync); +} + +pub fn bindAndExecute( + globalObject: *JSC.JSGlobalObject, + statement: *PostgresSQLStatement, + array_value: JSValue, + columns_value: JSValue, + comptime Context: type, + writer: protocol.NewWriter(Context), +) !void { + try writeBind(statement.signature.prepared_statement_name, bun.String.empty, globalObject, array_value, columns_value, statement.parameters, statement.fields, Context, writer); + var exec = protocol.Execute{ + .p = .{ + .prepared_statement = statement.signature.prepared_statement_name, + }, + }; + try exec.writeInternal(Context, writer); + + try writer.write(&protocol.Flush); + try writer.write(&protocol.Sync); +} + +pub fn executeQuery( + query: []const u8, + comptime Context: type, + writer: protocol.NewWriter(Context), +) !void { + try protocol.writeQuery(query, Context, writer); + try writer.write(&protocol.Flush); + try writer.write(&protocol.Sync); +} + +pub fn onData( + connection: *PostgresSQLConnection, + comptime Context: type, + reader: protocol.NewReader(Context), +) !void { + while (true) { + reader.markMessageStart(); + const c = try reader.int(u8); + debug("read: {c}", .{c}); + switch (c) { + 'D' => try connection.on(.DataRow, Context, reader), + 'd' => try connection.on(.CopyData, Context, reader), + 'S' => { + if (connection.tls_status == .message_sent) { + bun.debugAssert(connection.tls_status.message_sent == 8); + connection.tls_status = .ssl_ok; + connection.setupTLS(); + return; + } + + try connection.on(.ParameterStatus, Context, reader); + }, + 'Z' => try connection.on(.ReadyForQuery, Context, reader), + 'C' => try connection.on(.CommandComplete, Context, reader), + '2' => try connection.on(.BindComplete, Context, reader), + '1' => try connection.on(.ParseComplete, Context, reader), + 't' => try connection.on(.ParameterDescription, Context, reader), + 'T' => try connection.on(.RowDescription, Context, reader), + 'R' => try connection.on(.Authentication, Context, reader), + 'n' => try connection.on(.NoData, Context, reader), + 'K' => try connection.on(.BackendKeyData, Context, reader), + 'E' => try connection.on(.ErrorResponse, Context, reader), + 's' => try connection.on(.PortalSuspended, Context, reader), + '3' => try connection.on(.CloseComplete, Context, reader), + 'G' => try connection.on(.CopyInResponse, Context, reader), + 'N' => { + if (connection.tls_status == .message_sent) { + connection.tls_status = .ssl_not_available; + debug("Server does not support SSL", .{}); + if (connection.ssl_mode == .require) { + connection.fail("Server does not support SSL", error.TLSNotAvailable); + return; + } + continue; + } + + try connection.on(.NoticeResponse, Context, reader); + }, + 'I' => try connection.on(.EmptyQueryResponse, Context, reader), + 'H' => try connection.on(.CopyOutResponse, Context, reader), + 'c' => try connection.on(.CopyDone, Context, reader), + 'W' => try connection.on(.CopyBothResponse, Context, reader), + + else => { + debug("Unknown message: {c}", .{c}); + const to_skip = try reader.length() -| 1; + debug("to_skip: {d}", .{to_skip}); + try reader.skip(@intCast(@max(to_skip, 0))); + }, + } + } +} + +pub const Queue = std.fifo.LinearFifo(*PostgresSQLQuery, .Dynamic); + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const PostgresSQLConnection = @import("./PostgresSQLConnection.zig"); +const PostgresSQLQuery = @import("./PostgresSQLQuery.zig"); +const PostgresSQLStatement = @import("./PostgresSQLStatement.zig"); +const Signature = @import("./Signature.zig"); +const protocol = @import("./PostgresProtocol.zig"); +const std = @import("std"); +const QueryBindingIterator = @import("./QueryBindingIterator.zig").QueryBindingIterator; + +const types = @import("./PostgresTypes.zig"); +const AnyPostgresError = @import("./PostgresTypes.zig").AnyPostgresError; +const int4 = types.int4; +const short = types.short; + +const bun = @import("bun"); +const String = bun.String; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/PostgresSQLConnection.zig b/src/sql/postgres/PostgresSQLConnection.zig new file mode 100644 index 0000000000..4ee0e3c8f2 --- /dev/null +++ b/src/sql/postgres/PostgresSQLConnection.zig @@ -0,0 +1,1574 @@ +socket: Socket, +status: Status = Status.connecting, +ref_count: u32 = 1, + +write_buffer: bun.OffsetByteList = .{}, +read_buffer: bun.OffsetByteList = .{}, +last_message_start: u32 = 0, +requests: PostgresRequest.Queue, + +poll_ref: bun.Async.KeepAlive = .{}, +globalObject: *JSC.JSGlobalObject, + +statements: PreparedStatementsMap, +prepared_statement_id: u64 = 0, +pending_activity_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), +js_value: JSValue = .js_undefined, + +backend_parameters: bun.StringMap = bun.StringMap.init(bun.default_allocator, true), +backend_key_data: protocol.BackendKeyData = .{}, + +database: []const u8 = "", +user: []const u8 = "", +password: []const u8 = "", +path: []const u8 = "", +options: []const u8 = "", +options_buf: []const u8 = "", + +authentication_state: AuthenticationState = .{ .pending = {} }, + +tls_ctx: ?*uws.SocketContext = null, +tls_config: JSC.API.ServerConfig.SSLConfig = .{}, +tls_status: TLSStatus = .none, +ssl_mode: SSLMode = .disable, + +idle_timeout_interval_ms: u32 = 0, +connection_timeout_ms: u32 = 0, + +flags: ConnectionFlags = .{}, + +/// Before being connected, this is a connection timeout timer. +/// After being connected, this is an idle timeout timer. +timer: bun.api.Timer.EventLoopTimer = .{ + .tag = .PostgresSQLConnectionTimeout, + .next = .{ + .sec = 0, + .nsec = 0, + }, +}, + +/// This timer controls the maximum lifetime of a connection. +/// It starts when the connection successfully starts (i.e. after handshake is complete). +/// It stops when the connection is closed. +max_lifetime_interval_ms: u32 = 0, +max_lifetime_timer: bun.api.Timer.EventLoopTimer = .{ + .tag = .PostgresSQLConnectionMaxLifetime, + .next = .{ + .sec = 0, + .nsec = 0, + }, +}, + +fn getTimeoutInterval(this: *const PostgresSQLConnection) u32 { + return switch (this.status) { + .connected => this.idle_timeout_interval_ms, + .failed => 0, + else => this.connection_timeout_ms, + }; +} +pub fn disableConnectionTimeout(this: *PostgresSQLConnection) void { + if (this.timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.timer); + } + this.timer.state = .CANCELLED; +} +pub fn resetConnectionTimeout(this: *PostgresSQLConnection) void { + // if we are processing data, don't reset the timeout, wait for the data to be processed + if (this.flags.is_processing_data) return; + const interval = this.getTimeoutInterval(); + if (this.timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.timer); + } + if (interval == 0) { + return; + } + + this.timer.next = bun.timespec.msFromNow(@intCast(interval)); + this.globalObject.bunVM().timer.insert(&this.timer); +} + +pub fn getQueries(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + if (js.queriesGetCached(thisValue)) |value| { + return value; + } + + const array = try JSC.JSValue.createEmptyArray(globalObject, 0); + js.queriesSetCached(thisValue, globalObject, array); + + return array; +} + +pub fn getOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + if (js.onconnectGetCached(thisValue)) |value| { + return value; + } + + return .js_undefined; +} + +pub fn setOnConnect(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + js.onconnectSetCached(thisValue, globalObject, value); +} + +pub fn getOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + if (js.oncloseGetCached(thisValue)) |value| { + return value; + } + + return .js_undefined; +} + +pub fn setOnClose(_: *PostgresSQLConnection, thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + js.oncloseSetCached(thisValue, globalObject, value); +} + +pub fn setupTLS(this: *PostgresSQLConnection) void { + debug("setupTLS", .{}); + const new_socket = this.socket.SocketTCP.socket.connected.upgrade(this.tls_ctx.?, this.tls_config.server_name) orelse { + this.fail("Failed to upgrade to TLS", error.TLSUpgradeFailed); + return; + }; + this.socket = .{ + .SocketTLS = .{ + .socket = .{ + .connected = new_socket, + }, + }, + }; + + this.start(); +} +fn setupMaxLifetimeTimerIfNecessary(this: *PostgresSQLConnection) void { + if (this.max_lifetime_interval_ms == 0) return; + if (this.max_lifetime_timer.state == .ACTIVE) return; + + this.max_lifetime_timer.next = bun.timespec.msFromNow(@intCast(this.max_lifetime_interval_ms)); + this.globalObject.bunVM().timer.insert(&this.max_lifetime_timer); +} + +pub fn onConnectionTimeout(this: *PostgresSQLConnection) bun.api.Timer.EventLoopTimer.Arm { + debug("onConnectionTimeout", .{}); + + this.timer.state = .FIRED; + if (this.flags.is_processing_data) { + return .disarm; + } + + if (this.getTimeoutInterval() == 0) { + this.resetConnectionTimeout(); + return .disarm; + } + + switch (this.status) { + .connected => { + this.failFmt(.POSTGRES_IDLE_TIMEOUT, "Idle timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.idle_timeout_interval_ms) *| std.time.ns_per_ms)}); + }, + else => { + this.failFmt(.POSTGRES_CONNECTION_TIMEOUT, "Connection timeout after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); + }, + .sent_startup_message => { + this.failFmt(.POSTGRES_CONNECTION_TIMEOUT, "Connection timed out after {} (sent startup message, but never received response)", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.connection_timeout_ms) *| std.time.ns_per_ms)}); + }, + } + return .disarm; +} + +pub fn onMaxLifetimeTimeout(this: *PostgresSQLConnection) bun.api.Timer.EventLoopTimer.Arm { + debug("onMaxLifetimeTimeout", .{}); + this.max_lifetime_timer.state = .FIRED; + if (this.status == .failed) return .disarm; + this.failFmt(.POSTGRES_LIFETIME_TIMEOUT, "Max lifetime timeout reached after {}", .{bun.fmt.fmtDurationOneDecimal(@as(u64, this.max_lifetime_interval_ms) *| std.time.ns_per_ms)}); + return .disarm; +} + +fn start(this: *PostgresSQLConnection) void { + this.setupMaxLifetimeTimerIfNecessary(); + this.resetConnectionTimeout(); + this.sendStartupMessage(); + + const event_loop = this.globalObject.bunVM().eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + this.flushData(); +} + +pub fn hasPendingActivity(this: *PostgresSQLConnection) bool { + return this.pending_activity_count.load(.acquire) > 0; +} + +fn updateHasPendingActivity(this: *PostgresSQLConnection) void { + const a: u32 = if (this.requests.readableLength() > 0) 1 else 0; + const b: u32 = if (this.status != .disconnected) 1 else 0; + this.pending_activity_count.store(a + b, .release); +} + +pub fn setStatus(this: *PostgresSQLConnection, status: Status) void { + if (this.status == status) return; + defer this.updateHasPendingActivity(); + + this.status = status; + this.resetConnectionTimeout(); + + switch (status) { + .connected => { + const on_connect = this.consumeOnConnectCallback(this.globalObject) orelse return; + const js_value = this.js_value; + js_value.ensureStillAlive(); + this.globalObject.queueMicrotask(on_connect, &[_]JSValue{ JSValue.jsNull(), js_value }); + this.poll_ref.unref(this.globalObject.bunVM()); + }, + else => {}, + } +} + +pub fn finalize(this: *PostgresSQLConnection) void { + debug("PostgresSQLConnection finalize", .{}); + this.stopTimers(); + this.js_value = .zero; + this.deref(); +} + +pub fn flushDataAndResetTimeout(this: *PostgresSQLConnection) void { + this.resetConnectionTimeout(); + this.flushData(); +} + +pub fn flushData(this: *PostgresSQLConnection) void { + const chunk = this.write_buffer.remaining(); + if (chunk.len == 0) return; + const wrote = this.socket.write(chunk); + if (wrote > 0) { + SocketMonitor.write(chunk[0..@intCast(wrote)]); + this.write_buffer.consume(@intCast(wrote)); + } +} + +pub fn failWithJSValue(this: *PostgresSQLConnection, value: JSValue) void { + defer this.updateHasPendingActivity(); + this.stopTimers(); + if (this.status == .failed) return; + + this.status = .failed; + + this.ref(); + defer this.deref(); + // we defer the refAndClose so the on_close will be called first before we reject the pending requests + defer this.refAndClose(value); + const on_close = this.consumeOnCloseCallback(this.globalObject) orelse return; + + const loop = this.globalObject.bunVM().eventLoop(); + loop.enter(); + defer loop.exit(); + _ = on_close.call( + this.globalObject, + this.js_value, + &[_]JSValue{ + value, + this.getQueriesArray(), + }, + ) catch |e| this.globalObject.reportActiveExceptionAsUnhandled(e); +} + +pub fn failFmt(this: *PostgresSQLConnection, comptime error_code: JSC.Error, comptime fmt: [:0]const u8, args: anytype) void { + this.failWithJSValue(error_code.fmt(this.globalObject, fmt, args)); +} + +pub fn fail(this: *PostgresSQLConnection, message: []const u8, err: AnyPostgresError) void { + debug("failed: {s}: {s}", .{ message, @errorName(err) }); + + const globalObject = this.globalObject; + + this.failWithJSValue(postgresErrorToJS(globalObject, message, err)); +} + +pub fn onClose(this: *PostgresSQLConnection) void { + var vm = this.globalObject.bunVM(); + const loop = vm.eventLoop(); + loop.enter(); + defer loop.exit(); + this.poll_ref.unref(this.globalObject.bunVM()); + + this.fail("Connection closed", error.ConnectionClosed); +} + +fn sendStartupMessage(this: *PostgresSQLConnection) void { + if (this.status != .connecting) return; + debug("sendStartupMessage", .{}); + this.status = .sent_startup_message; + var msg = protocol.StartupMessage{ + .user = Data{ .temporary = this.user }, + .database = Data{ .temporary = this.database }, + .options = Data{ .temporary = this.options }, + }; + msg.writeInternal(Writer, this.writer()) catch |err| { + this.fail("Failed to write startup message", err); + }; +} + +fn startTLS(this: *PostgresSQLConnection, socket: uws.AnySocket) void { + debug("startTLS", .{}); + const offset = switch (this.tls_status) { + .message_sent => |count| count, + else => 0, + }; + const ssl_request = [_]u8{ + 0x00, 0x00, 0x00, 0x08, // Length + 0x04, 0xD2, 0x16, 0x2F, // SSL request code + }; + + const written = socket.write(ssl_request[offset..]); + if (written > 0) { + this.tls_status = .{ + .message_sent = offset + @as(u8, @intCast(written)), + }; + } else { + this.tls_status = .{ + .message_sent = offset, + }; + } +} + +pub fn onOpen(this: *PostgresSQLConnection, socket: uws.AnySocket) void { + this.socket = socket; + + this.poll_ref.ref(this.globalObject.bunVM()); + this.updateHasPendingActivity(); + + if (this.tls_status == .message_sent or this.tls_status == .pending) { + this.startTLS(socket); + return; + } + + this.start(); +} + +pub fn onHandshake(this: *PostgresSQLConnection, success: i32, ssl_error: uws.us_bun_verify_error_t) void { + debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no }); + const handshake_success = if (success == 1) true else false; + if (handshake_success) { + if (this.tls_config.reject_unauthorized != 0) { + // only reject the connection if reject_unauthorized == true + switch (this.ssl_mode) { + // https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279 + + .verify_ca, .verify_full => { + if (ssl_error.error_no != 0) { + this.failWithJSValue(ssl_error.toJS(this.globalObject)); + return; + } + + const ssl_ptr: *BoringSSL.c.SSL = @ptrCast(this.socket.getNativeHandle()); + if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| { + const hostname = servername[0..bun.len(servername)]; + if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { + this.failWithJSValue(ssl_error.toJS(this.globalObject)); + } + } + }, + else => { + return; + }, + } + } + } else { + // if we are here is because server rejected us, and the error_no is the cause of this + // no matter if reject_unauthorized is false because we are disconnected by the server + this.failWithJSValue(ssl_error.toJS(this.globalObject)); + } +} + +pub fn onTimeout(this: *PostgresSQLConnection) void { + _ = this; + debug("onTimeout", .{}); +} + +pub fn onDrain(this: *PostgresSQLConnection) void { + + // Don't send any other messages while we're waiting for TLS. + if (this.tls_status == .message_sent) { + if (this.tls_status.message_sent < 8) { + this.startTLS(this.socket); + } + + return; + } + + const event_loop = this.globalObject.bunVM().eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + this.flushData(); +} + +pub fn onData(this: *PostgresSQLConnection, data: []const u8) void { + this.ref(); + this.flags.is_processing_data = true; + const vm = this.globalObject.bunVM(); + + this.disableConnectionTimeout(); + defer { + if (this.status == .connected and !this.hasQueryRunning() and this.write_buffer.remaining().len == 0) { + // Don't keep the process alive when there's nothing to do. + this.poll_ref.unref(vm); + } else if (this.status == .connected) { + // Keep the process alive if there's something to do. + this.poll_ref.ref(vm); + } + this.flags.is_processing_data = false; + + // reset the connection timeout after we're done processing the data + this.resetConnectionTimeout(); + this.deref(); + } + + const event_loop = vm.eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + SocketMonitor.read(data); + // reset the head to the last message so remaining reflects the right amount of bytes + this.read_buffer.head = this.last_message_start; + + if (this.read_buffer.remaining().len == 0) { + var consumed: usize = 0; + var offset: usize = 0; + const reader = protocol.StackReader.init(data, &consumed, &offset); + PostgresRequest.onData(this, protocol.StackReader, reader) catch |err| { + if (err == error.ShortRead) { + if (comptime bun.Environment.allow_assert) { + debug("read_buffer: empty and received short read: last_message_start: {d}, head: {d}, len: {d}", .{ + offset, + consumed, + data.len, + }); + } + + this.read_buffer.head = 0; + this.last_message_start = 0; + this.read_buffer.byte_list.len = 0; + this.read_buffer.write(bun.default_allocator, data[offset..]) catch @panic("failed to write to read buffer"); + } else { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + + this.fail("Failed to read data", err); + } + }; + // no need to reset anything, its already empty + return; + } + // read buffer is not empty, so we need to write the data to the buffer and then read it + this.read_buffer.write(bun.default_allocator, data) catch @panic("failed to write to read buffer"); + PostgresRequest.onData(this, Reader, this.bufferedReader()) catch |err| { + if (err != error.ShortRead) { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + this.fail("Failed to read data", err); + return; + } + + if (comptime bun.Environment.allow_assert) { + debug("read_buffer: not empty and received short read: last_message_start: {d}, head: {d}, len: {d}", .{ + this.last_message_start, + this.read_buffer.head, + this.read_buffer.byte_list.len, + }); + } + return; + }; + + debug("clean read_buffer", .{}); + // success, we read everything! let's reset the last message start and the head + this.last_message_start = 0; + this.read_buffer.head = 0; +} + +pub fn constructor(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*PostgresSQLConnection { + _ = callframe; + return globalObject.throw("PostgresSQLConnection cannot be constructed directly", .{}); +} + +comptime { + const jscall = JSC.toJSHostFn(call); + @export(&jscall, .{ .name = "PostgresSQLConnection__createInstance" }); +} + +pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var vm = globalObject.bunVM(); + const arguments = callframe.arguments_old(15).slice(); + const hostname_str = try arguments[0].toBunString(globalObject); + defer hostname_str.deref(); + const port = try arguments[1].coerce(i32, globalObject); + + const username_str = try arguments[2].toBunString(globalObject); + defer username_str.deref(); + const password_str = try arguments[3].toBunString(globalObject); + defer password_str.deref(); + const database_str = try arguments[4].toBunString(globalObject); + defer database_str.deref(); + const ssl_mode: SSLMode = switch (arguments[5].toInt32()) { + 0 => .disable, + 1 => .prefer, + 2 => .require, + 3 => .verify_ca, + 4 => .verify_full, + else => .disable, + }; + + const tls_object = arguments[6]; + + var tls_config: JSC.API.ServerConfig.SSLConfig = .{}; + var tls_ctx: ?*uws.SocketContext = null; + if (ssl_mode != .disable) { + tls_config = if (tls_object.isBoolean() and tls_object.toBoolean()) + .{} + else if (tls_object.isObject()) + (JSC.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls_object) catch return .zero) orelse .{} + else { + return globalObject.throwInvalidArguments("tls must be a boolean or an object", .{}); + }; + + if (globalObject.hasException()) { + tls_config.deinit(); + return .zero; + } + + // we always request the cert so we can verify it and also we manually abort the connection if the hostname doesn't match + const original_reject_unauthorized = tls_config.reject_unauthorized; + tls_config.reject_unauthorized = 0; + tls_config.request_cert = 1; + // We create it right here so we can throw errors early. + const context_options = tls_config.asUSockets(); + var err: uws.create_bun_socket_error_t = .none; + tls_ctx = uws.SocketContext.createSSLContext(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), context_options, &err) orelse { + if (err != .none) { + return globalObject.throw("failed to create TLS context", .{}); + } else { + return globalObject.throwValue(err.toJS(globalObject)); + } + }; + // restore the original reject_unauthorized + tls_config.reject_unauthorized = original_reject_unauthorized; + if (err != .none) { + tls_config.deinit(); + if (tls_ctx) |ctx| { + ctx.deinit(true); + } + return globalObject.throwValue(err.toJS(globalObject)); + } + + uws.NewSocketHandler(true).configure(tls_ctx.?, true, *PostgresSQLConnection, SocketHandler(true)); + } + + var username: []const u8 = ""; + var password: []const u8 = ""; + var database: []const u8 = ""; + var options: []const u8 = ""; + var path: []const u8 = ""; + + const options_str = try arguments[7].toBunString(globalObject); + defer options_str.deref(); + + const path_str = try arguments[8].toBunString(globalObject); + defer path_str.deref(); + + const options_buf: []u8 = brk: { + var b = bun.StringBuilder{}; + b.cap += username_str.utf8ByteLength() + 1 + password_str.utf8ByteLength() + 1 + database_str.utf8ByteLength() + 1 + options_str.utf8ByteLength() + 1 + path_str.utf8ByteLength() + 1; + + b.allocate(bun.default_allocator) catch {}; + var u = username_str.toUTF8WithoutRef(bun.default_allocator); + defer u.deinit(); + username = b.append(u.slice()); + + var p = password_str.toUTF8WithoutRef(bun.default_allocator); + defer p.deinit(); + password = b.append(p.slice()); + + var d = database_str.toUTF8WithoutRef(bun.default_allocator); + defer d.deinit(); + database = b.append(d.slice()); + + var o = options_str.toUTF8WithoutRef(bun.default_allocator); + defer o.deinit(); + options = b.append(o.slice()); + + var _path = path_str.toUTF8WithoutRef(bun.default_allocator); + defer _path.deinit(); + path = b.append(_path.slice()); + + break :brk b.allocatedSlice(); + }; + + const on_connect = arguments[9]; + const on_close = arguments[10]; + const idle_timeout = arguments[11].toInt32(); + const connection_timeout = arguments[12].toInt32(); + const max_lifetime = arguments[13].toInt32(); + const use_unnamed_prepared_statements = arguments[14].asBoolean(); + + const ptr: *PostgresSQLConnection = try bun.default_allocator.create(PostgresSQLConnection); + + ptr.* = PostgresSQLConnection{ + .globalObject = globalObject, + + .database = database, + .user = username, + .password = password, + .path = path, + .options = options, + .options_buf = options_buf, + .socket = .{ .SocketTCP = .{ .socket = .{ .detached = {} } } }, + .requests = PostgresRequest.Queue.init(bun.default_allocator), + .statements = PreparedStatementsMap{}, + .tls_config = tls_config, + .tls_ctx = tls_ctx, + .ssl_mode = ssl_mode, + .tls_status = if (ssl_mode != .disable) .pending else .none, + .idle_timeout_interval_ms = @intCast(idle_timeout), + .connection_timeout_ms = @intCast(connection_timeout), + .max_lifetime_interval_ms = @intCast(max_lifetime), + .flags = .{ + .use_unnamed_prepared_statements = use_unnamed_prepared_statements, + }, + }; + + ptr.updateHasPendingActivity(); + ptr.poll_ref.ref(vm); + const js_value = ptr.toJS(globalObject); + js_value.ensureStillAlive(); + ptr.js_value = js_value; + + js.onconnectSetCached(js_value, globalObject, on_connect); + js.oncloseSetCached(js_value, globalObject, on_close); + bun.analytics.Features.postgres_connections += 1; + + { + const hostname = hostname_str.toUTF8(bun.default_allocator); + defer hostname.deinit(); + + const ctx = vm.rareData().postgresql_context.tcp orelse brk: { + const ctx_ = uws.SocketContext.createNoSSLContext(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection)).?; + uws.NewSocketHandler(false).configure(ctx_, true, *PostgresSQLConnection, SocketHandler(false)); + vm.rareData().postgresql_context.tcp = ctx_; + break :brk ctx_; + }; + + if (path.len > 0) { + ptr.socket = .{ + .SocketTCP = uws.SocketTCP.connectUnixAnon(path, ctx, ptr, false) catch |err| { + tls_config.deinit(); + if (tls_ctx) |tls| { + tls.deinit(true); + } + ptr.deinit(); + return globalObject.throwError(err, "failed to connect to postgresql"); + }, + }; + } else { + ptr.socket = .{ + .SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| { + tls_config.deinit(); + if (tls_ctx) |tls| { + tls.deinit(true); + } + ptr.deinit(); + return globalObject.throwError(err, "failed to connect to postgresql"); + }, + }; + } + ptr.resetConnectionTimeout(); + } + + return js_value; +} + +fn SocketHandler(comptime ssl: bool) type { + return struct { + const SocketType = uws.NewSocketHandler(ssl); + fn _socket(s: SocketType) Socket { + if (comptime ssl) { + return Socket{ .SocketTLS = s }; + } + + return Socket{ .SocketTCP = s }; + } + pub fn onOpen(this: *PostgresSQLConnection, socket: SocketType) void { + this.onOpen(_socket(socket)); + } + + fn onHandshake_(this: *PostgresSQLConnection, _: anytype, success: i32, ssl_error: uws.us_bun_verify_error_t) void { + this.onHandshake(success, ssl_error); + } + + pub const onHandshake = if (ssl) onHandshake_ else null; + + pub fn onClose(this: *PostgresSQLConnection, socket: SocketType, _: i32, _: ?*anyopaque) void { + _ = socket; + this.onClose(); + } + + pub fn onEnd(this: *PostgresSQLConnection, socket: SocketType) void { + _ = socket; + this.onClose(); + } + + pub fn onConnectError(this: *PostgresSQLConnection, socket: SocketType, _: i32) void { + _ = socket; + this.onClose(); + } + + pub fn onTimeout(this: *PostgresSQLConnection, socket: SocketType) void { + _ = socket; + this.onTimeout(); + } + + pub fn onData(this: *PostgresSQLConnection, socket: SocketType, data: []const u8) void { + _ = socket; + this.onData(data); + } + + pub fn onWritable(this: *PostgresSQLConnection, socket: SocketType) void { + _ = socket; + this.onDrain(); + } + }; +} + +pub fn ref(this: *@This()) void { + bun.assert(this.ref_count > 0); + this.ref_count += 1; +} + +pub fn doRef(this: *@This(), _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.poll_ref.ref(this.globalObject.bunVM()); + this.updateHasPendingActivity(); + return .js_undefined; +} + +pub fn doUnref(this: *@This(), _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.poll_ref.unref(this.globalObject.bunVM()); + this.updateHasPendingActivity(); + return .js_undefined; +} +pub fn doFlush(this: *PostgresSQLConnection, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + this.flushData(); + return .js_undefined; +} + +pub fn deref(this: *@This()) void { + const ref_count = this.ref_count; + this.ref_count -= 1; + + if (ref_count == 1) { + this.disconnect(); + this.deinit(); + } +} + +pub fn doClose(this: *@This(), globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + _ = globalObject; + this.disconnect(); + this.write_buffer.deinit(bun.default_allocator); + + return .js_undefined; +} + +pub fn stopTimers(this: *PostgresSQLConnection) void { + if (this.timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.timer); + } + if (this.max_lifetime_timer.state == .ACTIVE) { + this.globalObject.bunVM().timer.remove(&this.max_lifetime_timer); + } +} + +pub fn deinit(this: *@This()) void { + this.stopTimers(); + var iter = this.statements.valueIterator(); + while (iter.next()) |stmt_ptr| { + var stmt = stmt_ptr.*; + stmt.deref(); + } + this.statements.deinit(bun.default_allocator); + this.write_buffer.deinit(bun.default_allocator); + this.read_buffer.deinit(bun.default_allocator); + this.backend_parameters.deinit(); + + bun.freeSensitive(bun.default_allocator, this.options_buf); + + this.tls_config.deinit(); + bun.default_allocator.destroy(this); +} + +fn refAndClose(this: *@This(), js_reason: ?JSC.JSValue) void { + // refAndClose is always called when we wanna to disconnect or when we are closed + + if (!this.socket.isClosed()) { + // event loop need to be alive to close the socket + this.poll_ref.ref(this.globalObject.bunVM()); + // will unref on socket close + this.socket.close(); + } + + // cleanup requests + while (this.current()) |request| { + switch (request.status) { + // pending we will fail the request and the stmt will be marked as error ConnectionClosed too + .pending => { + const stmt = request.statement orelse continue; + stmt.error_response = .{ .postgres_error = AnyPostgresError.ConnectionClosed }; + stmt.status = .failed; + if (js_reason) |reason| { + request.onJSError(reason, this.globalObject); + } else { + request.onError(.{ .postgres_error = AnyPostgresError.ConnectionClosed }, this.globalObject); + } + }, + // in the middle of running + .binding, + .running, + .partial_response, + => { + if (js_reason) |reason| { + request.onJSError(reason, this.globalObject); + } else { + request.onError(.{ .postgres_error = AnyPostgresError.ConnectionClosed }, this.globalObject); + } + }, + // just ignore success and fail cases + .success, .fail => {}, + } + request.deref(); + this.requests.discard(1); + } +} + +pub fn disconnect(this: *@This()) void { + this.stopTimers(); + + if (this.status == .connected) { + this.status = .disconnected; + this.refAndClose(null); + } +} + +fn current(this: *PostgresSQLConnection) ?*PostgresSQLQuery { + if (this.requests.readableLength() == 0) { + return null; + } + + return this.requests.peekItem(0); +} + +pub fn hasQueryRunning(this: *PostgresSQLConnection) bool { + return !this.flags.is_ready_for_query or this.current() != null; +} + +pub const Writer = struct { + connection: *PostgresSQLConnection, + + pub fn write(this: Writer, data: []const u8) AnyPostgresError!void { + var buffer = &this.connection.write_buffer; + try buffer.write(bun.default_allocator, data); + } + + pub fn pwrite(this: Writer, data: []const u8, index: usize) AnyPostgresError!void { + @memcpy(this.connection.write_buffer.byte_list.slice()[index..][0..data.len], data); + } + + pub fn offset(this: Writer) usize { + return this.connection.write_buffer.len(); + } +}; + +pub fn writer(this: *PostgresSQLConnection) protocol.NewWriter(Writer) { + return .{ + .wrapped = .{ + .connection = this, + }, + }; +} + +pub const Reader = struct { + connection: *PostgresSQLConnection, + + pub fn markMessageStart(this: Reader) void { + this.connection.last_message_start = this.connection.read_buffer.head; + } + + pub const ensureLength = ensureCapacity; + + pub fn peek(this: Reader) []const u8 { + return this.connection.read_buffer.remaining(); + } + pub fn skip(this: Reader, count: usize) void { + this.connection.read_buffer.head = @min(this.connection.read_buffer.head + @as(u32, @truncate(count)), this.connection.read_buffer.byte_list.len); + } + pub fn ensureCapacity(this: Reader, count: usize) bool { + return @as(usize, this.connection.read_buffer.head) + count <= @as(usize, this.connection.read_buffer.byte_list.len); + } + pub fn read(this: Reader, count: usize) AnyPostgresError!Data { + var remaining = this.connection.read_buffer.remaining(); + if (@as(usize, remaining.len) < count) { + return error.ShortRead; + } + + this.skip(count); + return Data{ + .temporary = remaining[0..count], + }; + } + pub fn readZ(this: Reader) AnyPostgresError!Data { + const remain = this.connection.read_buffer.remaining(); + + if (bun.strings.indexOfChar(remain, 0)) |zero| { + this.skip(zero + 1); + return Data{ + .temporary = remain[0..zero], + }; + } + + return error.ShortRead; + } +}; + +pub fn bufferedReader(this: *PostgresSQLConnection) protocol.NewReader(Reader) { + return .{ + .wrapped = .{ .connection = this }, + }; +} + +fn advance(this: *PostgresSQLConnection) !void { + while (this.requests.readableLength() > 0) { + var req: *PostgresSQLQuery = this.requests.peekItem(0); + switch (req.status) { + .pending => { + if (req.flags.simple) { + debug("executeQuery", .{}); + var query_str = req.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, this.writer()) catch |err| { + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); + req.deref(); + this.requests.discard(1); + + continue; + }; + this.flags.is_ready_for_query = false; + req.status = .running; + return; + } else { + const stmt = req.statement orelse return error.ExpectedStatement; + + switch (stmt.status) { + .failed => { + bun.assert(stmt.error_response != null); + req.onError(stmt.error_response.?, this.globalObject); + req.deref(); + this.requests.discard(1); + + continue; + }, + .prepared => { + const thisValue = req.thisValue.get(); + bun.assert(thisValue != .zero); + const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero; + const columns_value = PostgresSQLQuery.js.columnsGetCached(thisValue) orelse .zero; + req.flags.binary = stmt.fields.len > 0; + + PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| { + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); + req.deref(); + this.requests.discard(1); + + continue; + }; + this.flags.is_ready_for_query = false; + req.status = .binding; + return; + }, + .pending => { + // statement is pending, lets write/parse it + var query_str = req.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + const has_params = stmt.signature.fields.len > 0; + // If it does not have params, we can write and execute immediately in one go + if (!has_params) { + const thisValue = req.thisValue.get(); + bun.assert(thisValue != .zero); + // prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete + const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero; + PostgresRequest.prepareAndQueryWithSignature(this.globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, this.writer(), &stmt.signature) catch |err| { + stmt.status = .failed; + stmt.error_response = .{ .postgres_error = err }; + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); + req.deref(); + this.requests.discard(1); + + continue; + }; + this.flags.is_ready_for_query = false; + req.status = .binding; + stmt.status = .parsing; + + return; + } + const connection_writer = this.writer(); + // write query and wait for it to be prepared + PostgresRequest.writeQuery(query_str.slice(), stmt.signature.prepared_statement_name, stmt.signature.fields, PostgresSQLConnection.Writer, connection_writer) catch |err| { + stmt.error_response = .{ .postgres_error = err }; + stmt.status = .failed; + + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); + req.deref(); + this.requests.discard(1); + + continue; + }; + connection_writer.write(&protocol.Sync) catch |err| { + stmt.error_response = .{ .postgres_error = err }; + stmt.status = .failed; + + req.onWriteFail(err, this.globalObject, this.getQueriesArray()); + req.deref(); + this.requests.discard(1); + + continue; + }; + this.flags.is_ready_for_query = false; + stmt.status = .parsing; + return; + }, + .parsing => { + // we are still parsing, lets wait for it to be prepared or failed + return; + }, + } + } + }, + + .running, .binding, .partial_response => { + // if we are binding it will switch to running immediately + // if we are running, we need to wait for it to be success or fail + return; + }, + .success, .fail => { + req.deref(); + this.requests.discard(1); + continue; + }, + } + } +} + +pub fn getQueriesArray(this: *const PostgresSQLConnection) JSValue { + return js.queriesGetCached(this.js_value) orelse .zero; +} + +pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.enum_literal), comptime Context: type, reader: protocol.NewReader(Context)) AnyPostgresError!void { + debug("on({s})", .{@tagName(MessageType)}); + + switch (comptime MessageType) { + .DataRow => { + const request = this.current() orelse return error.ExpectedRequest; + var statement = request.statement orelse return error.ExpectedStatement; + var structure: JSValue = .js_undefined; + var cached_structure: ?PostgresCachedStructure = null; + // explicit use switch without else so if new modes are added, we don't forget to check for duplicate fields + switch (request.flags.result_mode) { + .objects => { + cached_structure = statement.structure(this.js_value, this.globalObject); + structure = cached_structure.?.jsValue() orelse .js_undefined; + }, + .raw, .values => { + // no need to check for duplicate fields or structure + }, + } + + var putter = DataCell.Putter{ + .list = &.{}, + .fields = statement.fields, + .binary = request.flags.binary, + .bigint = request.flags.bigint, + .globalObject = this.globalObject, + }; + + var stack_buf: [70]DataCell = undefined; + var cells: []DataCell = stack_buf[0..@min(statement.fields.len, JSC.JSObject.maxInlineCapacity())]; + var free_cells = false; + defer { + for (cells[0..putter.count]) |*cell| { + cell.deinit(); + } + if (free_cells) bun.default_allocator.free(cells); + } + + if (statement.fields.len >= JSC.JSObject.maxInlineCapacity()) { + cells = try bun.default_allocator.alloc(DataCell, statement.fields.len); + free_cells = true; + } + // make sure all cells are reset if reader short breaks the fields will just be null with is better than undefined behavior + @memset(cells, DataCell{ .tag = .null, .value = .{ .null = 0 } }); + putter.list = cells; + + if (request.flags.result_mode == .raw) { + try protocol.DataRow.decode( + &putter, + Context, + reader, + DataCell.Putter.putRaw, + ); + } else { + try protocol.DataRow.decode( + &putter, + Context, + reader, + DataCell.Putter.put, + ); + } + const thisValue = request.thisValue.get(); + bun.assert(thisValue != .zero); + const pending_value = PostgresSQLQuery.js.pendingValueGetCached(thisValue) orelse .zero; + pending_value.ensureStillAlive(); + const result = putter.toJS(this.globalObject, pending_value, structure, statement.fields_flags, request.flags.result_mode, cached_structure); + + if (pending_value == .zero) { + PostgresSQLQuery.js.pendingValueSetCached(thisValue, this.globalObject, result); + } + }, + .CopyData => { + var copy_data: protocol.CopyData = undefined; + try copy_data.decodeInternal(Context, reader); + copy_data.data.deinit(); + }, + .ParameterStatus => { + var parameter_status: protocol.ParameterStatus = undefined; + try parameter_status.decodeInternal(Context, reader); + defer { + parameter_status.deinit(); + } + try this.backend_parameters.insert(parameter_status.name.slice(), parameter_status.value.slice()); + }, + .ReadyForQuery => { + var ready_for_query: protocol.ReadyForQuery = undefined; + try ready_for_query.decodeInternal(Context, reader); + + this.setStatus(.connected); + this.flags.is_ready_for_query = true; + this.socket.setTimeout(300); + defer this.updateRef(); + + if (this.current()) |request| { + if (request.status == .partial_response) { + // if is a partial response, just signal that the query is now complete + request.onResult("", this.globalObject, this.js_value, true); + } + } + try this.advance(); + + this.flushData(); + }, + .CommandComplete => { + var request = this.current() orelse return error.ExpectedRequest; + + var cmd: protocol.CommandComplete = undefined; + try cmd.decodeInternal(Context, reader); + defer { + cmd.deinit(); + } + debug("-> {s}", .{cmd.command_tag.slice()}); + defer this.updateRef(); + + if (request.flags.simple) { + // simple queries can have multiple commands + request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, false); + } else { + request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, true); + } + }, + .BindComplete => { + try reader.eatMessage(protocol.BindComplete); + var request = this.current() orelse return error.ExpectedRequest; + if (request.status == .binding) { + request.status = .running; + } + }, + .ParseComplete => { + try reader.eatMessage(protocol.ParseComplete); + const request = this.current() orelse return error.ExpectedRequest; + if (request.statement) |statement| { + // if we have params wait for parameter description + if (statement.status == .parsing and statement.signature.fields.len == 0) { + statement.status = .prepared; + } + } + }, + .ParameterDescription => { + var description: protocol.ParameterDescription = undefined; + try description.decodeInternal(Context, reader); + const request = this.current() orelse return error.ExpectedRequest; + var statement = request.statement orelse return error.ExpectedStatement; + statement.parameters = description.parameters; + if (statement.status == .parsing) { + statement.status = .prepared; + } + }, + .RowDescription => { + var description: protocol.RowDescription = undefined; + try description.decodeInternal(Context, reader); + errdefer description.deinit(); + const request = this.current() orelse return error.ExpectedRequest; + var statement = request.statement orelse return error.ExpectedStatement; + statement.fields = description.fields; + }, + .Authentication => { + var auth: protocol.Authentication = undefined; + try auth.decodeInternal(Context, reader); + defer auth.deinit(); + + switch (auth) { + .SASL => { + if (this.authentication_state != .SASL) { + this.authentication_state = .{ .SASL = .{} }; + } + + var mechanism_buf: [128]u8 = undefined; + const mechanism = std.fmt.bufPrintZ(&mechanism_buf, "n,,n=*,r={s}", .{this.authentication_state.SASL.nonce()}) catch unreachable; + var response = protocol.SASLInitialResponse{ + .mechanism = .{ + .temporary = "SCRAM-SHA-256", + }, + .data = .{ + .temporary = mechanism, + }, + }; + + try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); + debug("SASL", .{}); + this.flushData(); + }, + .SASLContinue => |*cont| { + if (this.authentication_state != .SASL) { + debug("Unexpected SASLContinue for authentiation state: {s}", .{@tagName(std.meta.activeTag(this.authentication_state))}); + return error.UnexpectedMessage; + } + var sasl = &this.authentication_state.SASL; + + if (sasl.status != .init) { + debug("Unexpected SASLContinue for SASL state: {s}", .{@tagName(sasl.status)}); + return error.UnexpectedMessage; + } + debug("SASLContinue", .{}); + + const iteration_count = try cont.iterationCount(); + + const server_salt_decoded_base64 = bun.base64.decodeAlloc(bun.z_allocator, cont.s) catch |err| { + return switch (err) { + error.DecodingFailed => error.SASL_SIGNATURE_INVALID_BASE64, + else => |e| e, + }; + }; + defer bun.z_allocator.free(server_salt_decoded_base64); + try sasl.computeSaltedPassword(server_salt_decoded_base64, iteration_count, this); + + const auth_string = try std.fmt.allocPrint( + bun.z_allocator, + "n=*,r={s},r={s},s={s},i={s},c=biws,r={s}", + .{ + sasl.nonce(), + cont.r, + cont.s, + cont.i, + cont.r, + }, + ); + defer bun.z_allocator.free(auth_string); + try sasl.computeServerSignature(auth_string); + + const client_key = sasl.clientKey(); + const client_key_signature = sasl.clientKeySignature(&client_key, auth_string); + var client_key_xor_buffer: [32]u8 = undefined; + for (&client_key_xor_buffer, client_key, client_key_signature) |*out, a, b| { + out.* = a ^ b; + } + + var client_key_xor_base64_buf = std.mem.zeroes([bun.base64.encodeLenFromSize(32)]u8); + const xor_base64_len = bun.base64.encode(&client_key_xor_base64_buf, &client_key_xor_buffer); + + const payload = try std.fmt.allocPrint( + bun.z_allocator, + "c=biws,r={s},p={s}", + .{ cont.r, client_key_xor_base64_buf[0..xor_base64_len] }, + ); + defer bun.z_allocator.free(payload); + + var response = protocol.SASLResponse{ + .data = .{ + .temporary = payload, + }, + }; + + try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); + sasl.status = .@"continue"; + this.flushData(); + }, + .SASLFinal => |final| { + if (this.authentication_state != .SASL) { + debug("SASLFinal - Unexpected SASLContinue for authentiation state: {s}", .{@tagName(std.meta.activeTag(this.authentication_state))}); + return error.UnexpectedMessage; + } + var sasl = &this.authentication_state.SASL; + + if (sasl.status != .@"continue") { + debug("SASLFinal - Unexpected SASLContinue for SASL state: {s}", .{@tagName(sasl.status)}); + return error.UnexpectedMessage; + } + + if (sasl.server_signature_len == 0) { + debug("SASLFinal - Server signature is empty", .{}); + return error.UnexpectedMessage; + } + + const server_signature = sasl.serverSignature(); + + // This will usually start with "v=" + const comparison_signature = final.data.slice(); + + if (comparison_signature.len < 2 or !bun.strings.eqlLong(server_signature, comparison_signature[2..], true)) { + debug("SASLFinal - SASL Server signature mismatch\nExpected: {s}\nActual: {s}", .{ server_signature, comparison_signature[2..] }); + this.fail("The server did not return the correct signature", error.SASL_SIGNATURE_MISMATCH); + } else { + debug("SASLFinal - SASL Server signature match", .{}); + this.authentication_state.zero(); + } + }, + .Ok => { + debug("Authentication OK", .{}); + this.authentication_state.zero(); + this.authentication_state = .{ .ok = {} }; + }, + + .Unknown => { + this.fail("Unknown authentication method", error.UNKNOWN_AUTHENTICATION_METHOD); + }, + + .ClearTextPassword => { + debug("ClearTextPassword", .{}); + var response = protocol.PasswordMessage{ + .password = .{ + .temporary = this.password, + }, + }; + + try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); + this.flushData(); + }, + + .MD5Password => |md5| { + debug("MD5Password", .{}); + // Format is: md5 + md5(md5(password + username) + salt) + var first_hash_buf: bun.sha.MD5.Digest = undefined; + var first_hash_str: [32]u8 = undefined; + var final_hash_buf: bun.sha.MD5.Digest = undefined; + var final_hash_str: [32]u8 = undefined; + var final_password_buf: [36]u8 = undefined; + + // First hash: md5(password + username) + var first_hasher = bun.sha.MD5.init(); + first_hasher.update(this.password); + first_hasher.update(this.user); + first_hasher.final(&first_hash_buf); + const first_hash_str_output = std.fmt.bufPrint(&first_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&first_hash_buf)}) catch unreachable; + + // Second hash: md5(first_hash + salt) + var final_hasher = bun.sha.MD5.init(); + final_hasher.update(first_hash_str_output); + final_hasher.update(&md5.salt); + final_hasher.final(&final_hash_buf); + const final_hash_str_output = std.fmt.bufPrint(&final_hash_str, "{x}", .{std.fmt.fmtSliceHexLower(&final_hash_buf)}) catch unreachable; + + // Format final password as "md5" + final_hash + const final_password = std.fmt.bufPrintZ(&final_password_buf, "md5{s}", .{final_hash_str_output}) catch unreachable; + + var response = protocol.PasswordMessage{ + .password = .{ + .temporary = final_password, + }, + }; + + this.authentication_state = .{ .md5 = {} }; + try response.writeInternal(PostgresSQLConnection.Writer, this.writer()); + this.flushData(); + }, + + else => { + debug("TODO auth: {s}", .{@tagName(std.meta.activeTag(auth))}); + this.fail("TODO: support authentication method: {s}", error.UNSUPPORTED_AUTHENTICATION_METHOD); + }, + } + }, + .NoData => { + try reader.eatMessage(protocol.NoData); + var request = this.current() orelse return error.ExpectedRequest; + if (request.status == .binding) { + request.status = .running; + } + }, + .BackendKeyData => { + try this.backend_key_data.decodeInternal(Context, reader); + }, + .ErrorResponse => { + var err: protocol.ErrorResponse = undefined; + try err.decodeInternal(Context, reader); + + if (this.status == .connecting or this.status == .sent_startup_message) { + defer { + err.deinit(); + } + + this.failWithJSValue(err.toJS(this.globalObject)); + + // it shouldn't enqueue any requests while connecting + bun.assert(this.requests.count == 0); + return; + } + + var request = this.current() orelse { + debug("ErrorResponse: {}", .{err}); + return error.ExpectedRequest; + }; + var is_error_owned = true; + defer { + if (is_error_owned) { + err.deinit(); + } + } + if (request.statement) |stmt| { + if (stmt.status == PostgresSQLStatement.Status.parsing) { + stmt.status = PostgresSQLStatement.Status.failed; + stmt.error_response = .{ .protocol = err }; + is_error_owned = false; + if (this.statements.remove(bun.hash(stmt.signature.name))) { + stmt.deref(); + } + } + } + this.updateRef(); + + request.onError(.{ .protocol = err }, this.globalObject); + }, + .PortalSuspended => { + // try reader.eatMessage(&protocol.PortalSuspended); + // var request = this.current() orelse return error.ExpectedRequest; + // _ = request; + debug("TODO PortalSuspended", .{}); + }, + .CloseComplete => { + try reader.eatMessage(protocol.CloseComplete); + var request = this.current() orelse return error.ExpectedRequest; + defer this.updateRef(); + if (request.flags.simple) { + request.onResult("CLOSECOMPLETE", this.globalObject, this.js_value, false); + } else { + request.onResult("CLOSECOMPLETE", this.globalObject, this.js_value, true); + } + }, + .CopyInResponse => { + debug("TODO CopyInResponse", .{}); + }, + .NoticeResponse => { + debug("UNSUPPORTED NoticeResponse", .{}); + var resp: protocol.NoticeResponse = undefined; + + try resp.decodeInternal(Context, reader); + resp.deinit(); + }, + .EmptyQueryResponse => { + try reader.eatMessage(protocol.EmptyQueryResponse); + var request = this.current() orelse return error.ExpectedRequest; + defer this.updateRef(); + if (request.flags.simple) { + request.onResult("", this.globalObject, this.js_value, false); + } else { + request.onResult("", this.globalObject, this.js_value, true); + } + }, + .CopyOutResponse => { + debug("TODO CopyOutResponse", .{}); + }, + .CopyDone => { + debug("TODO CopyDone", .{}); + }, + .CopyBothResponse => { + debug("TODO CopyBothResponse", .{}); + }, + else => @compileError("Unknown message type: " ++ @tagName(MessageType)), + } +} + +pub fn updateRef(this: *PostgresSQLConnection) void { + this.updateHasPendingActivity(); + if (this.pending_activity_count.raw > 0) { + this.poll_ref.ref(this.globalObject.bunVM()); + } else { + this.poll_ref.unref(this.globalObject.bunVM()); + } +} + +pub fn getConnected(this: *PostgresSQLConnection, _: *JSC.JSGlobalObject) JSValue { + return JSValue.jsBoolean(this.status == Status.connected); +} + +pub fn consumeOnConnectCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { + debug("consumeOnConnectCallback", .{}); + const on_connect = js.onconnectGetCached(this.js_value) orelse return null; + debug("consumeOnConnectCallback exists", .{}); + + js.onconnectSetCached(this.js_value, globalObject, .zero); + return on_connect; +} + +pub fn consumeOnCloseCallback(this: *const PostgresSQLConnection, globalObject: *JSC.JSGlobalObject) ?JSC.JSValue { + debug("consumeOnCloseCallback", .{}); + const on_close = js.oncloseGetCached(this.js_value) orelse return null; + debug("consumeOnCloseCallback exists", .{}); + js.oncloseSetCached(this.js_value, globalObject, .zero); + return on_close; +} + +const PreparedStatementsMap = std.HashMapUnmanaged(u64, *PostgresSQLStatement, bun.IdentityContext(u64), 80); + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const PostgresCachedStructure = @import("./PostgresCachedStructure.zig"); +const PostgresRequest = @import("./PostgresRequest.zig"); +const PostgresSQLConnection = @This(); +const PostgresSQLQuery = @import("./PostgresSQLQuery.zig"); +const PostgresSQLStatement = @import("./PostgresSQLStatement.zig"); +const SocketMonitor = @import("./SocketMonitor.zig"); +const protocol = @import("./PostgresProtocol.zig"); +const std = @import("std"); +const AuthenticationState = @import("./AuthenticationState.zig").AuthenticationState; +const ConnectionFlags = @import("./ConnectionFlags.zig").ConnectionFlags; +const Data = @import("./Data.zig").Data; +const DataCell = @import("./DataCell.zig").DataCell; +const SSLMode = @import("./SSLMode.zig").SSLMode; +const Status = @import("./Status.zig").Status; +const TLSStatus = @import("./TLSStatus.zig").TLSStatus; + +const AnyPostgresError = @import("./AnyPostgresError.zig").AnyPostgresError; +const postgresErrorToJS = @import("./AnyPostgresError.zig").postgresErrorToJS; + +const bun = @import("bun"); +const BoringSSL = bun.BoringSSL; +const assert = bun.assert; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; + +pub const js = JSC.Codegen.JSPostgresSQLConnection; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; +pub const toJS = js.toJS; + +const uws = bun.uws; +const Socket = uws.AnySocket; diff --git a/src/sql/postgres/PostgresSQLContext.zig b/src/sql/postgres/PostgresSQLContext.zig new file mode 100644 index 0000000000..35ecc7f46e --- /dev/null +++ b/src/sql/postgres/PostgresSQLContext.zig @@ -0,0 +1,23 @@ +tcp: ?*uws.SocketContext = null, + +onQueryResolveFn: JSC.Strong.Optional = .empty, +onQueryRejectFn: JSC.Strong.Optional = .empty, + +pub fn init(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var ctx = &globalObject.bunVM().rareData().postgresql_context; + ctx.onQueryResolveFn.set(globalObject, callframe.argument(0)); + ctx.onQueryRejectFn.set(globalObject, callframe.argument(1)); + + return .js_undefined; +} + +comptime { + const js_init = JSC.toJSHostFn(init); + @export(&js_init, .{ .name = "PostgresSQLContext__init" }); +} + +// @sortImports + +const bun = @import("bun"); +const JSC = bun.JSC; +const uws = bun.uws; diff --git a/src/sql/postgres/PostgresSQLQuery.zig b/src/sql/postgres/PostgresSQLQuery.zig new file mode 100644 index 0000000000..3aaa4d3920 --- /dev/null +++ b/src/sql/postgres/PostgresSQLQuery.zig @@ -0,0 +1,499 @@ +statement: ?*PostgresSQLStatement = null, +query: bun.String = bun.String.empty, +cursor_name: bun.String = bun.String.empty, + +thisValue: JSRef = JSRef.empty(), + +status: Status = Status.pending, + +ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), + +flags: packed struct(u8) { + is_done: bool = false, + binary: bool = false, + bigint: bool = false, + simple: bool = false, + result_mode: PostgresSQLQueryResultMode = .objects, + _padding: u2 = 0, +} = .{}, + +pub fn getTarget(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, clean_target: bool) JSC.JSValue { + const thisValue = this.thisValue.get(); + if (thisValue == .zero) { + return .zero; + } + const target = js.targetGetCached(thisValue) orelse return .zero; + if (clean_target) { + js.targetSetCached(thisValue, globalObject, .zero); + } + return target; +} + +pub const Status = enum(u8) { + /// The query was just enqueued, statement status can be checked for more details + pending, + /// The query is being bound to the statement + binding, + /// The query is running + running, + /// The query is waiting for a partial response + partial_response, + /// The query was successful + success, + /// The query failed + fail, + + pub fn isRunning(this: Status) bool { + return @intFromEnum(this) > @intFromEnum(Status.pending) and @intFromEnum(this) < @intFromEnum(Status.success); + } +}; + +pub fn hasPendingActivity(this: *@This()) bool { + return this.ref_count.load(.monotonic) > 1; +} + +pub fn deinit(this: *@This()) void { + this.thisValue.deinit(); + if (this.statement) |statement| { + statement.deref(); + } + this.query.deref(); + this.cursor_name.deref(); + bun.default_allocator.destroy(this); +} + +pub fn finalize(this: *@This()) void { + debug("PostgresSQLQuery finalize", .{}); + if (this.thisValue == .weak) { + // clean up if is a weak reference, if is a strong reference we need to wait until the query is done + // if we are a strong reference, here is probably a bug because GC'd should not happen + this.thisValue.weak = .zero; + } + this.deref(); +} + +pub fn deref(this: *@This()) void { + const ref_count = this.ref_count.fetchSub(1, .monotonic); + + if (ref_count == 1) { + this.deinit(); + } +} + +pub fn ref(this: *@This()) void { + bun.assert(this.ref_count.fetchAdd(1, .monotonic) > 0); +} + +pub fn onWriteFail( + this: *@This(), + err: AnyPostgresError, + globalObject: *JSC.JSGlobalObject, + queries_array: JSValue, +) void { + this.status = .fail; + const thisValue = this.thisValue.get(); + defer this.thisValue.deinit(); + const targetValue = this.getTarget(globalObject, true); + if (thisValue == .zero or targetValue == .zero) { + return; + } + + const vm = JSC.VirtualMachine.get(); + const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?; + const event_loop = vm.eventLoop(); + event_loop.runCallback(function, globalObject, thisValue, &.{ + targetValue, + postgresErrorToJS(globalObject, null, err), + queries_array, + }); +} +pub fn onJSError(this: *@This(), err: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { + this.status = .fail; + this.ref(); + defer this.deref(); + + const thisValue = this.thisValue.get(); + defer this.thisValue.deinit(); + const targetValue = this.getTarget(globalObject, true); + if (thisValue == .zero or targetValue == .zero) { + return; + } + + var vm = JSC.VirtualMachine.get(); + const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?; + const event_loop = vm.eventLoop(); + event_loop.runCallback(function, globalObject, thisValue, &.{ + targetValue, + err, + }); +} +pub fn onError(this: *@This(), err: PostgresSQLStatement.Error, globalObject: *JSC.JSGlobalObject) void { + this.onJSError(err.toJS(globalObject), globalObject); +} + +pub fn allowGC(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) void { + if (thisValue == .zero) { + return; + } + + defer thisValue.ensureStillAlive(); + js.bindingSetCached(thisValue, globalObject, .zero); + js.pendingValueSetCached(thisValue, globalObject, .zero); + js.targetSetCached(thisValue, globalObject, .zero); +} + +fn consumePendingValue(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject) ?JSValue { + const pending_value = js.pendingValueGetCached(thisValue) orelse return null; + js.pendingValueSetCached(thisValue, globalObject, .zero); + return pending_value; +} + +pub fn onResult(this: *@This(), command_tag_str: []const u8, globalObject: *JSC.JSGlobalObject, connection: JSC.JSValue, is_last: bool) void { + this.ref(); + defer this.deref(); + + const thisValue = this.thisValue.get(); + const targetValue = this.getTarget(globalObject, is_last); + if (is_last) { + this.status = .success; + } else { + this.status = .partial_response; + } + defer if (is_last) { + allowGC(thisValue, globalObject); + this.thisValue.deinit(); + }; + if (thisValue == .zero or targetValue == .zero) { + return; + } + + const vm = JSC.VirtualMachine.get(); + const function = vm.rareData().postgresql_context.onQueryResolveFn.get().?; + const event_loop = vm.eventLoop(); + const tag = CommandTag.init(command_tag_str); + + event_loop.runCallback(function, globalObject, thisValue, &.{ + targetValue, + consumePendingValue(thisValue, globalObject) orelse .js_undefined, + tag.toJSTag(globalObject), + tag.toJSNumber(), + if (connection == .zero) .js_undefined else PostgresSQLConnection.js.queriesGetCached(connection) orelse .js_undefined, + JSValue.jsBoolean(is_last), + }); +} + +pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*PostgresSQLQuery { + _ = callframe; + return globalThis.throw("PostgresSQLQuery cannot be constructed directly", .{}); +} + +pub fn estimatedSize(this: *PostgresSQLQuery) usize { + _ = this; + return @sizeOf(PostgresSQLQuery); +} + +pub fn call(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(6).slice(); + var args = JSC.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const query = args.nextEat() orelse { + return globalThis.throw("query must be a string", .{}); + }; + const values = args.nextEat() orelse { + return globalThis.throw("values must be an array", .{}); + }; + + if (!query.isString()) { + return globalThis.throw("query must be a string", .{}); + } + + if (values.jsType() != .Array) { + return globalThis.throw("values must be an array", .{}); + } + + const pending_value: JSValue = args.nextEat() orelse .js_undefined; + const columns: JSValue = args.nextEat() orelse .js_undefined; + const js_bigint: JSValue = args.nextEat() orelse .false; + const js_simple: JSValue = args.nextEat() orelse .false; + + const bigint = js_bigint.isBoolean() and js_bigint.asBoolean(); + const simple = js_simple.isBoolean() and js_simple.asBoolean(); + if (simple) { + if (try values.getLength(globalThis) > 0) { + return globalThis.throwInvalidArguments("simple query cannot have parameters", .{}); + } + if (try query.getLength(globalThis) >= std.math.maxInt(i32)) { + return globalThis.throwInvalidArguments("query is too long", .{}); + } + } + if (!pending_value.jsType().isArrayLike()) { + return globalThis.throwInvalidArgumentType("query", "pendingValue", "Array"); + } + + var ptr = try bun.default_allocator.create(PostgresSQLQuery); + + const this_value = ptr.toJS(globalThis); + this_value.ensureStillAlive(); + + ptr.* = .{ + .query = try query.toBunString(globalThis), + .thisValue = JSRef.initWeak(this_value), + .flags = .{ + .bigint = bigint, + .simple = simple, + }, + }; + + js.bindingSetCached(this_value, globalThis, values); + js.pendingValueSetCached(this_value, globalThis, pending_value); + if (!columns.isUndefined()) { + js.columnsSetCached(this_value, globalThis, columns); + } + + return this_value; +} + +pub fn push(this: *PostgresSQLQuery, globalThis: *JSC.JSGlobalObject, value: JSValue) void { + var pending_value = this.pending_value.get() orelse return; + pending_value.push(globalThis, value); +} + +pub fn doDone(this: *@This(), globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + _ = globalObject; + this.flags.is_done = true; + return .js_undefined; +} +pub fn setPendingValue(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const result = callframe.argument(0); + js.pendingValueSetCached(this.thisValue.get(), globalObject, result); + return .js_undefined; +} +pub fn setMode(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const js_mode = callframe.argument(0); + if (js_mode.isEmptyOrUndefinedOrNull() or !js_mode.isNumber()) { + return globalObject.throwInvalidArgumentType("setMode", "mode", "Number"); + } + + const mode = try js_mode.coerce(i32, globalObject); + this.flags.result_mode = std.meta.intToEnum(PostgresSQLQueryResultMode, mode) catch { + return globalObject.throwInvalidArgumentTypeValue("mode", "Number", js_mode); + }; + return .js_undefined; +} + +pub fn doRun(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + var arguments_ = callframe.arguments_old(2); + const arguments = arguments_.slice(); + const connection: *PostgresSQLConnection = arguments[0].as(PostgresSQLConnection) orelse { + return globalObject.throw("connection must be a PostgresSQLConnection", .{}); + }; + + connection.poll_ref.ref(globalObject.bunVM()); + var query = arguments[1]; + + if (!query.isObject()) { + return globalObject.throwInvalidArgumentType("run", "query", "Query"); + } + + const this_value = callframe.this(); + const binding_value = js.bindingGetCached(this_value) orelse .zero; + var query_str = this.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + var writer = connection.writer(); + + if (this.flags.simple) { + debug("executeQuery", .{}); + + const can_execute = !connection.hasQueryRunning(); + if (can_execute) { + PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, writer) catch |err| { + if (!globalObject.hasException()) + return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to execute query", err)); + return error.JSError; + }; + connection.flags.is_ready_for_query = false; + this.status = .running; + } else { + this.status = .pending; + } + const stmt = bun.default_allocator.create(PostgresSQLStatement) catch { + return globalObject.throwOutOfMemory(); + }; + // Query is simple and it's the only owner of the statement + stmt.* = .{ + .signature = Signature.empty(), + .ref_count = 1, + .status = .parsing, + }; + this.statement = stmt; + // We need a strong reference to the query so that it doesn't get GC'd + connection.requests.writeItem(this) catch return globalObject.throwOutOfMemory(); + this.ref(); + this.thisValue.upgrade(globalObject); + + js.targetSetCached(this_value, globalObject, query); + if (this.status == .running) { + connection.flushDataAndResetTimeout(); + } else { + connection.resetConnectionTimeout(); + } + return .js_undefined; + } + + const columns_value: JSValue = js.columnsGetCached(this_value) orelse .js_undefined; + + var signature = Signature.generate(globalObject, query_str.slice(), binding_value, columns_value, connection.prepared_statement_id, connection.flags.use_unnamed_prepared_statements) catch |err| { + if (!globalObject.hasException()) + return globalObject.throwError(err, "failed to generate signature"); + return error.JSError; + }; + + const has_params = signature.fields.len > 0; + var did_write = false; + enqueue: { + var connection_entry_value: ?**PostgresSQLStatement = null; + if (!connection.flags.use_unnamed_prepared_statements) { + const entry = connection.statements.getOrPut(bun.default_allocator, bun.hash(signature.name)) catch |err| { + signature.deinit(); + return globalObject.throwError(err, "failed to allocate statement"); + }; + connection_entry_value = entry.value_ptr; + if (entry.found_existing) { + this.statement = connection_entry_value.?.*; + this.statement.?.ref(); + signature.deinit(); + + switch (this.statement.?.status) { + .failed => { + // If the statement failed, we need to throw the error + return globalObject.throwValue(this.statement.?.error_response.?.toJS(globalObject)); + }, + .prepared => { + if (!connection.hasQueryRunning()) { + this.flags.binary = this.statement.?.fields.len > 0; + debug("bindAndExecute", .{}); + + // bindAndExecute will bind + execute, it will change to running after binding is complete + PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, columns_value, PostgresSQLConnection.Writer, writer) catch |err| { + if (!globalObject.hasException()) + return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to bind and execute query", err)); + return error.JSError; + }; + connection.flags.is_ready_for_query = false; + this.status = .binding; + + did_write = true; + } + }, + .parsing, .pending => {}, + } + + break :enqueue; + } + } + const can_execute = !connection.hasQueryRunning(); + + if (can_execute) { + // If it does not have params, we can write and execute immediately in one go + if (!has_params) { + debug("prepareAndQueryWithSignature", .{}); + // prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete + PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| { + signature.deinit(); + if (!globalObject.hasException()) + return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to prepare and query", err)); + return error.JSError; + }; + connection.flags.is_ready_for_query = false; + this.status = .binding; + did_write = true; + } else { + debug("writeQuery", .{}); + + PostgresRequest.writeQuery(query_str.slice(), signature.prepared_statement_name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| { + signature.deinit(); + if (!globalObject.hasException()) + return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to write query", err)); + return error.JSError; + }; + writer.write(&protocol.Sync) catch |err| { + signature.deinit(); + if (!globalObject.hasException()) + return globalObject.throwValue(postgresErrorToJS(globalObject, "failed to flush", err)); + return error.JSError; + }; + connection.flags.is_ready_for_query = false; + did_write = true; + } + } + { + const stmt = bun.default_allocator.create(PostgresSQLStatement) catch { + return globalObject.throwOutOfMemory(); + }; + // we only have connection_entry_value if we are using named prepared statements + if (connection_entry_value) |entry_value| { + connection.prepared_statement_id += 1; + stmt.* = .{ .signature = signature, .ref_count = 2, .status = if (can_execute) .parsing else .pending }; + this.statement = stmt; + + entry_value.* = stmt; + } else { + stmt.* = .{ .signature = signature, .ref_count = 1, .status = if (can_execute) .parsing else .pending }; + this.statement = stmt; + } + } + } + // We need a strong reference to the query so that it doesn't get GC'd + connection.requests.writeItem(this) catch return globalObject.throwOutOfMemory(); + this.ref(); + this.thisValue.upgrade(globalObject); + + js.targetSetCached(this_value, globalObject, query); + if (did_write) { + connection.flushDataAndResetTimeout(); + } else { + connection.resetConnectionTimeout(); + } + return .js_undefined; +} + +pub fn doCancel(this: *PostgresSQLQuery, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + _ = callframe; + _ = globalObject; + _ = this; + + return .js_undefined; +} + +comptime { + const jscall = JSC.toJSHostFn(call); + @export(&jscall, .{ .name = "PostgresSQLQuery__createInstance" }); +} + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const PostgresRequest = @import("./PostgresRequest.zig"); +const PostgresSQLConnection = @import("./PostgresSQLConnection.zig"); +const PostgresSQLQuery = @This(); +const PostgresSQLStatement = @import("./PostgresSQLStatement.zig"); +const Signature = @import("./Signature.zig"); +const bun = @import("bun"); +const protocol = @import("./PostgresProtocol.zig"); +const std = @import("std"); +const CommandTag = @import("./CommandTag.zig").CommandTag; +const PostgresSQLQueryResultMode = @import("./PostgresSQLQueryResultMode.zig").PostgresSQLQueryResultMode; + +const AnyPostgresError = @import("./AnyPostgresError.zig").AnyPostgresError; +const postgresErrorToJS = @import("./AnyPostgresError.zig").postgresErrorToJS; + +const JSC = bun.JSC; +const JSGlobalObject = JSC.JSGlobalObject; +const JSRef = JSC.JSRef; +const JSValue = JSC.JSValue; + +pub const js = JSC.Codegen.JSPostgresSQLQuery; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; +pub const toJS = js.toJS; diff --git a/src/sql/postgres/PostgresSQLQueryResultMode.zig b/src/sql/postgres/PostgresSQLQueryResultMode.zig new file mode 100644 index 0000000000..d8f7c9c444 --- /dev/null +++ b/src/sql/postgres/PostgresSQLQueryResultMode.zig @@ -0,0 +1,7 @@ +pub const PostgresSQLQueryResultMode = enum(u2) { + objects = 0, + values = 1, + raw = 2, +}; + +// @sortImports diff --git a/src/sql/postgres/PostgresSQLStatement.zig b/src/sql/postgres/PostgresSQLStatement.zig new file mode 100644 index 0000000000..cc832e6909 --- /dev/null +++ b/src/sql/postgres/PostgresSQLStatement.zig @@ -0,0 +1,192 @@ +cached_structure: PostgresCachedStructure = .{}, +ref_count: u32 = 1, +fields: []protocol.FieldDescription = &[_]protocol.FieldDescription{}, +parameters: []const int4 = &[_]int4{}, +signature: Signature, +status: Status = Status.pending, +error_response: ?Error = null, +needs_duplicate_check: bool = true, +fields_flags: DataCell.Flags = .{}, + +pub const Error = union(enum) { + protocol: protocol.ErrorResponse, + postgres_error: AnyPostgresError, + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .protocol => |*err| err.deinit(), + .postgres_error => {}, + } + } + + pub fn toJS(this: *const @This(), globalObject: *JSC.JSGlobalObject) JSValue { + return switch (this.*) { + .protocol => |err| err.toJS(globalObject), + .postgres_error => |err| postgresErrorToJS(globalObject, null, err), + }; + } +}; +pub const Status = enum { + pending, + parsing, + prepared, + failed, + + pub fn isRunning(this: @This()) bool { + return this == .parsing; + } +}; +pub fn ref(this: *@This()) void { + bun.assert(this.ref_count > 0); + this.ref_count += 1; +} + +pub fn deref(this: *@This()) void { + const ref_count = this.ref_count; + this.ref_count -= 1; + + if (ref_count == 1) { + this.deinit(); + } +} + +pub fn checkForDuplicateFields(this: *PostgresSQLStatement) void { + if (!this.needs_duplicate_check) return; + this.needs_duplicate_check = false; + + var seen_numbers = std.ArrayList(u32).init(bun.default_allocator); + defer seen_numbers.deinit(); + var seen_fields = bun.StringHashMap(void).init(bun.default_allocator); + seen_fields.ensureUnusedCapacity(@intCast(this.fields.len)) catch bun.outOfMemory(); + defer seen_fields.deinit(); + + // iterate backwards + var remaining = this.fields.len; + var flags: DataCell.Flags = .{}; + while (remaining > 0) { + remaining -= 1; + const field: *protocol.FieldDescription = &this.fields[remaining]; + switch (field.name_or_index) { + .name => |*name| { + const seen = seen_fields.getOrPut(name.slice()) catch unreachable; + if (seen.found_existing) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } + + flags.has_named_columns = true; + }, + .index => |index| { + if (std.mem.indexOfScalar(u32, seen_numbers.items, index) != null) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } else { + seen_numbers.append(index) catch bun.outOfMemory(); + } + + flags.has_indexed_columns = true; + }, + .duplicate => { + flags.has_duplicate_columns = true; + }, + } + } + + this.fields_flags = flags; +} + +pub fn deinit(this: *PostgresSQLStatement) void { + debug("PostgresSQLStatement deinit", .{}); + + bun.assert(this.ref_count == 0); + + for (this.fields) |*field| { + field.deinit(); + } + bun.default_allocator.free(this.fields); + bun.default_allocator.free(this.parameters); + this.cached_structure.deinit(); + if (this.error_response) |err| { + this.error_response = null; + var _error = err; + _error.deinit(); + } + this.signature.deinit(); + bun.default_allocator.destroy(this); +} + +pub fn structure(this: *PostgresSQLStatement, owner: JSValue, globalObject: *JSC.JSGlobalObject) PostgresCachedStructure { + if (this.cached_structure.has()) { + return this.cached_structure; + } + this.checkForDuplicateFields(); + + // lets avoid most allocations + var stack_ids: [70]JSC.JSObject.ExternColumnIdentifier = undefined; + // lets de duplicate the fields early + var nonDuplicatedCount = this.fields.len; + for (this.fields) |*field| { + if (field.name_or_index == .duplicate) { + nonDuplicatedCount -= 1; + } + } + const ids = if (nonDuplicatedCount <= JSC.JSObject.maxInlineCapacity()) stack_ids[0..nonDuplicatedCount] else bun.default_allocator.alloc(JSC.JSObject.ExternColumnIdentifier, nonDuplicatedCount) catch bun.outOfMemory(); + + var i: usize = 0; + for (this.fields) |*field| { + if (field.name_or_index == .duplicate) continue; + + var id: *JSC.JSObject.ExternColumnIdentifier = &ids[i]; + switch (field.name_or_index) { + .name => |name| { + id.value.name = String.createAtomIfPossible(name.slice()); + }, + .index => |index| { + id.value.index = index; + }, + .duplicate => unreachable, + } + id.tag = switch (field.name_or_index) { + .name => 2, + .index => 1, + .duplicate => 0, + }; + i += 1; + } + + if (nonDuplicatedCount > JSC.JSObject.maxInlineCapacity()) { + this.cached_structure.set(globalObject, null, ids); + } else { + this.cached_structure.set(globalObject, JSC.JSObject.createStructure( + globalObject, + owner, + @truncate(ids.len), + ids.ptr, + ), null); + } + + return this.cached_structure; +} + +const debug = bun.Output.scoped(.Postgres, false); + +// @sortImports + +const PostgresCachedStructure = @import("./PostgresCachedStructure.zig"); +const PostgresSQLStatement = @This(); +const Signature = @import("./Signature.zig"); +const protocol = @import("./PostgresProtocol.zig"); +const std = @import("std"); +const DataCell = @import("./DataCell.zig").DataCell; + +const AnyPostgresError = @import("./AnyPostgresError.zig").AnyPostgresError; +const postgresErrorToJS = @import("./AnyPostgresError.zig").postgresErrorToJS; + +const types = @import("./PostgresTypes.zig"); +const int4 = types.int4; + +const bun = @import("bun"); +const String = bun.String; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/PostgresTypes.zig b/src/sql/postgres/PostgresTypes.zig new file mode 100644 index 0000000000..d21d7ce483 --- /dev/null +++ b/src/sql/postgres/PostgresTypes.zig @@ -0,0 +1,20 @@ +pub const @"bool" = @import("./types/bool.zig"); + +// @sortImports + +pub const bytea = @import("./types/bytea.zig"); +pub const date = @import("./types/date.zig"); +pub const json = @import("./types/json.zig"); +pub const numeric = @import("./types/numeric.zig"); +pub const string = @import("./types/PostgresString.zig"); +pub const AnyPostgresError = @import("./AnyPostgresError.zig").AnyPostgresError; +pub const Tag = @import("./types/Tag.zig").Tag; + +const int_types = @import("./types/int_types.zig"); +pub const Int32 = int_types.Int32; +pub const PostgresInt32 = int_types.int4; +pub const PostgresInt64 = int_types.int8; +pub const PostgresShort = int_types.short; +pub const int4 = int_types.int4; +pub const int8 = int_types.int8; +pub const short = int_types.short; diff --git a/src/sql/postgres/QueryBindingIterator.zig b/src/sql/postgres/QueryBindingIterator.zig new file mode 100644 index 0000000000..af2e3e78fb --- /dev/null +++ b/src/sql/postgres/QueryBindingIterator.zig @@ -0,0 +1,66 @@ +pub const QueryBindingIterator = union(enum) { + array: JSC.JSArrayIterator, + objects: ObjectIterator, + + pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!QueryBindingIterator { + if (columns.isEmptyOrUndefinedOrNull()) { + return .{ .array = try JSC.JSArrayIterator.init(array, globalObject) }; + } + + return .{ + .objects = .{ + .array = array, + .columns = columns, + .globalObject = globalObject, + .columns_count = try columns.getLength(globalObject), + .array_length = try array.getLength(globalObject), + }, + }; + } + + pub fn next(this: *QueryBindingIterator) bun.JSError!?JSC.JSValue { + return switch (this.*) { + .array => |*iter| iter.next(), + .objects => |*iter| iter.next(), + }; + } + + pub fn anyFailed(this: *const QueryBindingIterator) bool { + return switch (this.*) { + .array => false, + .objects => |*iter| iter.any_failed, + }; + } + + pub fn to(this: *QueryBindingIterator, index: u32) void { + switch (this.*) { + .array => |*iter| iter.i = index, + .objects => |*iter| { + iter.cell_i = index % iter.columns_count; + iter.row_i = index / iter.columns_count; + iter.current_row = .zero; + }, + } + } + + pub fn reset(this: *QueryBindingIterator) void { + switch (this.*) { + .array => |*iter| { + iter.i = 0; + }, + .objects => |*iter| { + iter.cell_i = 0; + iter.row_i = 0; + iter.current_row = .zero; + }, + } + } +}; + +// @sortImports + +const ObjectIterator = @import("./ObjectIterator.zig"); +const bun = @import("bun"); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/SASL.zig b/src/sql/postgres/SASL.zig new file mode 100644 index 0000000000..6fb482f40a --- /dev/null +++ b/src/sql/postgres/SASL.zig @@ -0,0 +1,95 @@ +const nonce_byte_len = 18; +const nonce_base64_len = bun.base64.encodeLenFromSize(nonce_byte_len); + +const server_signature_byte_len = 32; +const server_signature_base64_len = bun.base64.encodeLenFromSize(server_signature_byte_len); + +const salted_password_byte_len = 32; + +nonce_base64_bytes: [nonce_base64_len]u8 = .{0} ** nonce_base64_len, +nonce_len: u8 = 0, + +server_signature_base64_bytes: [server_signature_base64_len]u8 = .{0} ** server_signature_base64_len, +server_signature_len: u8 = 0, + +salted_password_bytes: [salted_password_byte_len]u8 = .{0} ** salted_password_byte_len, +salted_password_created: bool = false, + +status: SASLStatus = .init, + +pub const SASLStatus = enum { + init, + @"continue", +}; + +fn hmac(password: []const u8, data: []const u8) ?[32]u8 { + var buf = std.mem.zeroes([bun.BoringSSL.c.EVP_MAX_MD_SIZE]u8); + + // TODO: I don't think this is failable. + const result = bun.hmac.generate(password, data, .sha256, &buf) orelse return null; + + assert(result.len == 32); + return buf[0..32].*; +} + +pub fn computeSaltedPassword(this: *SASL, salt_bytes: []const u8, iteration_count: u32, connection: *PostgresSQLConnection) !void { + this.salted_password_created = true; + if (Crypto.EVP.pbkdf2(&this.salted_password_bytes, connection.password, salt_bytes, iteration_count, .sha256) == null) { + return error.PBKDFD2; + } +} + +pub fn saltedPassword(this: *const SASL) []const u8 { + assert(this.salted_password_created); + return this.salted_password_bytes[0..salted_password_byte_len]; +} + +pub fn serverSignature(this: *const SASL) []const u8 { + assert(this.server_signature_len > 0); + return this.server_signature_base64_bytes[0..this.server_signature_len]; +} + +pub fn computeServerSignature(this: *SASL, auth_string: []const u8) !void { + assert(this.server_signature_len == 0); + + const server_key = hmac(this.saltedPassword(), "Server Key") orelse return error.InvalidServerKey; + const server_signature_bytes = hmac(&server_key, auth_string) orelse return error.InvalidServerSignature; + this.server_signature_len = @intCast(bun.base64.encode(&this.server_signature_base64_bytes, &server_signature_bytes)); +} + +pub fn clientKey(this: *const SASL) [32]u8 { + return hmac(this.saltedPassword(), "Client Key").?; +} + +pub fn clientKeySignature(_: *const SASL, client_key: []const u8, auth_string: []const u8) [32]u8 { + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(client_key, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + return hmac(&sha_digest, auth_string).?; +} + +pub fn nonce(this: *SASL) []const u8 { + if (this.nonce_len == 0) { + var bytes: [nonce_byte_len]u8 = .{0} ** nonce_byte_len; + bun.csprng(&bytes); + this.nonce_len = @intCast(bun.base64.encode(&this.nonce_base64_bytes, &bytes)); + } + return this.nonce_base64_bytes[0..this.nonce_len]; +} + +pub fn deinit(this: *SASL) void { + this.nonce_len = 0; + this.salted_password_created = false; + this.server_signature_len = 0; + this.status = .init; +} + +// @sortImports + +const PostgresSQLConnection = @import("./PostgresSQLConnection.zig"); +const SASL = @This(); +const std = @import("std"); + +const bun = @import("bun"); +const JSC = bun.JSC; +const assert = bun.assert; +const Crypto = JSC.API.Bun.Crypto; diff --git a/src/sql/postgres/SSLMode.zig b/src/sql/postgres/SSLMode.zig new file mode 100644 index 0000000000..adc78ff605 --- /dev/null +++ b/src/sql/postgres/SSLMode.zig @@ -0,0 +1,9 @@ +pub const SSLMode = enum(u8) { + disable = 0, + prefer = 1, + require = 2, + verify_ca = 3, + verify_full = 4, +}; + +// @sortImports diff --git a/src/sql/postgres/Signature.zig b/src/sql/postgres/Signature.zig new file mode 100644 index 0000000000..37720ef626 --- /dev/null +++ b/src/sql/postgres/Signature.zig @@ -0,0 +1,113 @@ +fields: []const int4, +name: []const u8, +query: []const u8, +prepared_statement_name: []const u8, + +pub fn empty() Signature { + return Signature{ + .fields = &[_]int4{}, + .name = &[_]u8{}, + .query = &[_]u8{}, + .prepared_statement_name = &[_]u8{}, + }; +} + +pub fn deinit(this: *Signature) void { + if (this.prepared_statement_name.len > 0) { + bun.default_allocator.free(this.prepared_statement_name); + } + if (this.name.len > 0) { + bun.default_allocator.free(this.name); + } + if (this.fields.len > 0) { + bun.default_allocator.free(this.fields); + } + if (this.query.len > 0) { + bun.default_allocator.free(this.query); + } +} + +pub fn hash(this: *const Signature) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(this.name); + hasher.update(std.mem.sliceAsBytes(this.fields)); + return hasher.final(); +} + +pub fn generate(globalObject: *JSC.JSGlobalObject, query: []const u8, array_value: JSValue, columns: JSValue, prepared_statement_id: u64, unnamed: bool) !Signature { + var fields = std.ArrayList(int4).init(bun.default_allocator); + var name = try std.ArrayList(u8).initCapacity(bun.default_allocator, query.len); + + name.appendSliceAssumeCapacity(query); + + errdefer { + fields.deinit(); + name.deinit(); + } + + var iter = try QueryBindingIterator.init(array_value, columns, globalObject); + + while (try iter.next()) |value| { + if (value.isEmptyOrUndefinedOrNull()) { + // Allow postgres to decide the type + try fields.append(0); + try name.appendSlice(".null"); + continue; + } + + const tag = try types.Tag.fromJS(globalObject, value); + + switch (tag) { + .int8 => try name.appendSlice(".int8"), + .int4 => try name.appendSlice(".int4"), + // .int4_array => try name.appendSlice(".int4_array"), + .int2 => try name.appendSlice(".int2"), + .float8 => try name.appendSlice(".float8"), + .float4 => try name.appendSlice(".float4"), + .numeric => try name.appendSlice(".numeric"), + .json, .jsonb => try name.appendSlice(".json"), + .bool => try name.appendSlice(".bool"), + .timestamp => try name.appendSlice(".timestamp"), + .timestamptz => try name.appendSlice(".timestamptz"), + .bytea => try name.appendSlice(".bytea"), + else => try name.appendSlice(".string"), + } + + switch (tag) { + .bool, .int4, .int8, .float8, .int2, .numeric, .float4, .bytea => { + // We decide the type + try fields.append(@intFromEnum(tag)); + }, + else => { + // Allow postgres to decide the type + try fields.append(0); + }, + } + } + + if (iter.anyFailed()) { + return error.InvalidQueryBinding; + } + // max u64 length is 20, max prepared_statement_name length is 63 + const prepared_statement_name = if (unnamed) "" else try std.fmt.allocPrint(bun.default_allocator, "P{s}${d}", .{ name.items[0..@min(40, name.items.len)], prepared_statement_id }); + + return Signature{ + .prepared_statement_name = prepared_statement_name, + .name = name.items, + .fields = fields.items, + .query = try bun.default_allocator.dupe(u8, query), + }; +} + +// @sortImports + +const Signature = @This(); +const bun = @import("bun"); +const std = @import("std"); +const QueryBindingIterator = @import("./QueryBindingIterator.zig").QueryBindingIterator; + +const types = @import("./PostgresTypes.zig"); +const int4 = types.int4; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/SocketMonitor.zig b/src/sql/postgres/SocketMonitor.zig new file mode 100644 index 0000000000..7765d31017 --- /dev/null +++ b/src/sql/postgres/SocketMonitor.zig @@ -0,0 +1,23 @@ +pub fn write(data: []const u8) void { + if (comptime bun.Environment.isDebug) { + DebugSocketMonitorWriter.check.call(); + if (DebugSocketMonitorWriter.enabled) { + DebugSocketMonitorWriter.write(data); + } + } +} + +pub fn read(data: []const u8) void { + if (comptime bun.Environment.isDebug) { + DebugSocketMonitorReader.check.call(); + if (DebugSocketMonitorReader.enabled) { + DebugSocketMonitorReader.write(data); + } + } +} + +// @sortImports + +const DebugSocketMonitorReader = @import("./DebugSocketMonitorReader.zig"); +const DebugSocketMonitorWriter = @import("./DebugSocketMonitorWriter.zig"); +const bun = @import("bun"); diff --git a/src/sql/postgres/Status.zig b/src/sql/postgres/Status.zig new file mode 100644 index 0000000000..f4a0e9290e --- /dev/null +++ b/src/sql/postgres/Status.zig @@ -0,0 +1,11 @@ +pub const Status = enum { + disconnected, + connecting, + // Prevent sending the startup message multiple times. + // Particularly relevant for TLS connections. + sent_startup_message, + connected, + failed, +}; + +// @sortImports diff --git a/src/sql/postgres/TLSStatus.zig b/src/sql/postgres/TLSStatus.zig new file mode 100644 index 0000000000..a711af013a --- /dev/null +++ b/src/sql/postgres/TLSStatus.zig @@ -0,0 +1,11 @@ +pub const TLSStatus = union(enum) { + none, + pending, + + /// Number of bytes sent of the 8-byte SSL request message. + /// Since we may send a partial message, we need to know how many bytes were sent. + message_sent: u8, + + ssl_not_available, + ssl_ok, +}; diff --git a/src/sql/postgres/postgres_protocol.zig b/src/sql/postgres/postgres_protocol.zig deleted file mode 100644 index 111e96b0ac..0000000000 --- a/src/sql/postgres/postgres_protocol.zig +++ /dev/null @@ -1,1551 +0,0 @@ -const std = @import("std"); -const bun = @import("bun"); -const postgres = bun.api.Postgres; -const Data = postgres.Data; -const protocol = @This(); -const PostgresInt32 = postgres.PostgresInt32; -const PostgresShort = postgres.PostgresShort; -const String = bun.String; -const debug = postgres.debug; -const JSValue = JSC.JSValue; -const JSC = bun.JSC; -const short = postgres.short; -const int4 = postgres.int4; -const int8 = postgres.int8; -const PostgresInt64 = postgres.PostgresInt64; -const types = postgres.types; -const AnyPostgresError = postgres.AnyPostgresError; -pub const ArrayList = struct { - array: *std.ArrayList(u8), - - pub fn offset(this: @This()) usize { - return this.array.items.len; - } - - pub fn write(this: @This(), bytes: []const u8) AnyPostgresError!void { - try this.array.appendSlice(bytes); - } - - pub fn pwrite(this: @This(), bytes: []const u8, i: usize) AnyPostgresError!void { - @memcpy(this.array.items[i..][0..bytes.len], bytes); - } - - pub const Writer = NewWriter(@This()); -}; - -pub const StackReader = struct { - buffer: []const u8 = "", - offset: *usize, - message_start: *usize, - - pub fn markMessageStart(this: @This()) void { - this.message_start.* = this.offset.*; - } - - pub fn ensureLength(this: @This(), length: usize) bool { - return this.buffer.len >= (this.offset.* + length); - } - - pub fn init(buffer: []const u8, offset: *usize, message_start: *usize) protocol.NewReader(StackReader) { - return .{ - .wrapped = .{ - .buffer = buffer, - .offset = offset, - .message_start = message_start, - }, - }; - } - - pub fn peek(this: StackReader) []const u8 { - return this.buffer[this.offset.*..]; - } - pub fn skip(this: StackReader, count: usize) void { - if (this.offset.* + count > this.buffer.len) { - this.offset.* = this.buffer.len; - return; - } - - this.offset.* += count; - } - pub fn ensureCapacity(this: StackReader, count: usize) bool { - return this.buffer.len >= (this.offset.* + count); - } - pub fn read(this: StackReader, count: usize) AnyPostgresError!Data { - const offset = this.offset.*; - if (!this.ensureCapacity(count)) { - return error.ShortRead; - } - - this.skip(count); - return Data{ - .temporary = this.buffer[offset..this.offset.*], - }; - } - pub fn readZ(this: StackReader) AnyPostgresError!Data { - const remaining = this.peek(); - if (bun.strings.indexOfChar(remaining, 0)) |zero| { - this.skip(zero + 1); - return Data{ - .temporary = remaining[0..zero], - }; - } - - return error.ShortRead; - } -}; - -pub fn NewWriterWrap( - comptime Context: type, - comptime offsetFn_: (fn (ctx: Context) usize), - comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) AnyPostgresError!void), - comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) AnyPostgresError!void), -) type { - return struct { - wrapped: Context, - - const writeFn = writeFunction_; - const pwriteFn = pwriteFunction_; - const offsetFn = offsetFn_; - pub const Ctx = Context; - - pub const WrappedWriter = @This(); - - pub inline fn write(this: @This(), data: []const u8) AnyPostgresError!void { - try writeFn(this.wrapped, data); - } - - pub const LengthWriter = struct { - index: usize, - context: WrappedWriter, - - pub fn write(this: LengthWriter) AnyPostgresError!void { - try this.context.pwrite(&Int32(this.context.offset() - this.index), this.index); - } - - pub fn writeExcludingSelf(this: LengthWriter) AnyPostgresError!void { - try this.context.pwrite(&Int32(this.context.offset() -| (this.index + 4)), this.index); - } - }; - - pub inline fn length(this: @This()) AnyPostgresError!LengthWriter { - const i = this.offset(); - try this.int4(0); - return LengthWriter{ - .index = i, - .context = this, - }; - } - - pub inline fn offset(this: @This()) usize { - return offsetFn(this.wrapped); - } - - pub inline fn pwrite(this: @This(), data: []const u8, i: usize) AnyPostgresError!void { - try pwriteFn(this.wrapped, data, i); - } - - pub fn int4(this: @This(), value: PostgresInt32) !void { - try this.write(std.mem.asBytes(&@byteSwap(value))); - } - - pub fn int8(this: @This(), value: PostgresInt64) !void { - try this.write(std.mem.asBytes(&@byteSwap(value))); - } - - pub fn sint4(this: @This(), value: i32) !void { - try this.write(std.mem.asBytes(&@byteSwap(value))); - } - - pub fn @"f64"(this: @This(), value: f64) !void { - try this.write(std.mem.asBytes(&@byteSwap(@as(u64, @bitCast(value))))); - } - - pub fn @"f32"(this: @This(), value: f32) !void { - try this.write(std.mem.asBytes(&@byteSwap(@as(u32, @bitCast(value))))); - } - - pub fn short(this: @This(), value: anytype) !void { - try this.write(std.mem.asBytes(&@byteSwap(@as(u16, @intCast(value))))); - } - - pub fn string(this: @This(), value: []const u8) !void { - try this.write(value); - if (value.len == 0 or value[value.len - 1] != 0) - try this.write(&[_]u8{0}); - } - - pub fn bytes(this: @This(), value: []const u8) !void { - try this.write(value); - if (value.len == 0 or value[value.len - 1] != 0) - try this.write(&[_]u8{0}); - } - - pub fn @"bool"(this: @This(), value: bool) !void { - try this.write(if (value) "t" else "f"); - } - - pub fn @"null"(this: @This()) !void { - try this.int4(std.math.maxInt(PostgresInt32)); - } - - pub fn String(this: @This(), value: bun.String) !void { - if (value.isEmpty()) { - try this.write(&[_]u8{0}); - return; - } - - var sliced = value.toUTF8(bun.default_allocator); - defer sliced.deinit(); - const slice = sliced.slice(); - - try this.write(slice); - if (slice.len == 0 or slice[slice.len - 1] != 0) - try this.write(&[_]u8{0}); - } - }; -} - -pub const FieldType = enum(u8) { - /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a localized translation of one of these. Always present. - severity = 'S', - - /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the S field except that the contents are never localized. This is present only in messages generated by PostgreSQL versions 9.6 and later. - localized_severity = 'V', - - /// Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present. - code = 'C', - - /// Message: the primary human-readable error message. This should be accurate but terse (typically one line). Always present. - message = 'M', - - /// Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines. - detail = 'D', - - /// Hint: an optional suggestion what to do about the problem. This is intended to differ from Detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines. - hint = 'H', - - /// Position: the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes. - position = 'P', - - /// Internal position: this is defined the same as the P field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The q field will always appear when this field appears. - internal_position = 'p', - - /// Internal query: the text of a failed internally-generated command. This could be, for example, an SQL query issued by a PL/pgSQL function. - internal = 'q', - - /// Where: an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first. - where = 'W', - - /// Schema name: if the error was associated with a specific database object, the name of the schema containing that object, if any. - schema = 's', - - /// Table name: if the error was associated with a specific table, the name of the table. (Refer to the schema name field for the name of the table's schema.) - table = 't', - - /// Column name: if the error was associated with a specific table column, the name of the column. (Refer to the schema and table name fields to identify the table.) - column = 'c', - - /// Data type name: if the error was associated with a specific data type, the name of the data type. (Refer to the schema name field for the name of the data type's schema.) - datatype = 'd', - - /// Constraint name: if the error was associated with a specific constraint, the name of the constraint. Refer to fields listed above for the associated table or domain. (For this purpose, indexes are treated as constraints, even if they weren't created with constraint syntax.) - constraint = 'n', - - /// File: the file name of the source-code location where the error was reported. - file = 'F', - - /// Line: the line number of the source-code location where the error was reported. - line = 'L', - - /// Routine: the name of the source-code routine reporting the error. - routine = 'R', - - _, -}; - -pub const FieldMessage = union(FieldType) { - severity: String, - localized_severity: String, - code: String, - message: String, - detail: String, - hint: String, - position: String, - internal_position: String, - internal: String, - where: String, - schema: String, - table: String, - column: String, - datatype: String, - constraint: String, - file: String, - line: String, - routine: String, - - pub fn format(this: FieldMessage, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - switch (this) { - inline else => |str| { - try std.fmt.format(writer, "{}", .{str}); - }, - } - } - - pub fn deinit(this: *FieldMessage) void { - switch (this.*) { - inline else => |*message| { - message.deref(); - }, - } - } - - pub fn decodeList(comptime Context: type, reader: NewReader(Context)) !std.ArrayListUnmanaged(FieldMessage) { - var messages = std.ArrayListUnmanaged(FieldMessage){}; - while (true) { - const field_int = try reader.int(u8); - if (field_int == 0) break; - const field: FieldType = @enumFromInt(field_int); - - var message = try reader.readZ(); - defer message.deinit(); - if (message.slice().len == 0) break; - - try messages.append(bun.default_allocator, FieldMessage.init(field, message.slice()) catch continue); - } - - return messages; - } - - pub fn init(tag: FieldType, message: []const u8) !FieldMessage { - return switch (tag) { - .severity => FieldMessage{ .severity = String.createUTF8(message) }, - // Ignore this one for now. - // .localized_severity => FieldMessage{ .localized_severity = String.createUTF8(message) }, - .code => FieldMessage{ .code = String.createUTF8(message) }, - .message => FieldMessage{ .message = String.createUTF8(message) }, - .detail => FieldMessage{ .detail = String.createUTF8(message) }, - .hint => FieldMessage{ .hint = String.createUTF8(message) }, - .position => FieldMessage{ .position = String.createUTF8(message) }, - .internal_position => FieldMessage{ .internal_position = String.createUTF8(message) }, - .internal => FieldMessage{ .internal = String.createUTF8(message) }, - .where => FieldMessage{ .where = String.createUTF8(message) }, - .schema => FieldMessage{ .schema = String.createUTF8(message) }, - .table => FieldMessage{ .table = String.createUTF8(message) }, - .column => FieldMessage{ .column = String.createUTF8(message) }, - .datatype => FieldMessage{ .datatype = String.createUTF8(message) }, - .constraint => FieldMessage{ .constraint = String.createUTF8(message) }, - .file => FieldMessage{ .file = String.createUTF8(message) }, - .line => FieldMessage{ .line = String.createUTF8(message) }, - .routine => FieldMessage{ .routine = String.createUTF8(message) }, - else => error.UnknownFieldType, - }; - } -}; - -pub fn NewReaderWrap( - comptime Context: type, - comptime markMessageStartFn_: (fn (ctx: Context) void), - comptime peekFn_: (fn (ctx: Context) []const u8), - comptime skipFn_: (fn (ctx: Context, count: usize) void), - comptime ensureCapacityFn_: (fn (ctx: Context, count: usize) bool), - comptime readFunction_: (fn (ctx: Context, count: usize) AnyPostgresError!Data), - comptime readZ_: (fn (ctx: Context) AnyPostgresError!Data), -) type { - return struct { - wrapped: Context, - const readFn = readFunction_; - const readZFn = readZ_; - const ensureCapacityFn = ensureCapacityFn_; - const skipFn = skipFn_; - const peekFn = peekFn_; - const markMessageStartFn = markMessageStartFn_; - - pub const Ctx = Context; - - pub inline fn markMessageStart(this: @This()) void { - markMessageStartFn(this.wrapped); - } - - pub inline fn read(this: @This(), count: usize) AnyPostgresError!Data { - return try readFn(this.wrapped, count); - } - - pub inline fn eatMessage(this: @This(), comptime msg_: anytype) AnyPostgresError!void { - const msg = msg_[1..]; - try this.ensureCapacity(msg.len); - - var input = try readFn(this.wrapped, msg.len); - defer input.deinit(); - if (bun.strings.eqlComptime(input.slice(), msg)) return; - return error.InvalidMessage; - } - - pub fn skip(this: @This(), count: usize) AnyPostgresError!void { - skipFn(this.wrapped, count); - } - - pub fn peek(this: @This()) []const u8 { - return peekFn(this.wrapped); - } - - pub inline fn readZ(this: @This()) AnyPostgresError!Data { - return try readZFn(this.wrapped); - } - - pub inline fn ensureCapacity(this: @This(), count: usize) AnyPostgresError!void { - if (!ensureCapacityFn(this.wrapped, count)) { - return error.ShortRead; - } - } - - pub fn int(this: @This(), comptime Int: type) !Int { - var data = try this.read(@sizeOf((Int))); - defer data.deinit(); - if (comptime Int == u8) { - return @as(Int, data.slice()[0]); - } - return @byteSwap(@as(Int, @bitCast(data.slice()[0..@sizeOf(Int)].*))); - } - - pub fn peekInt(this: @This(), comptime Int: type) ?Int { - const remain = this.peek(); - if (remain.len < @sizeOf(Int)) { - return null; - } - return @byteSwap(@as(Int, @bitCast(remain[0..@sizeOf(Int)].*))); - } - - pub fn expectInt(this: @This(), comptime Int: type, comptime value: comptime_int) !bool { - const actual = try this.int(Int); - return actual == value; - } - - pub fn int4(this: @This()) !PostgresInt32 { - return this.int(PostgresInt32); - } - - pub fn short(this: @This()) !PostgresShort { - return this.int(PostgresShort); - } - - pub fn length(this: @This()) !PostgresInt32 { - const expected = try this.int(PostgresInt32); - if (expected > -1) { - try this.ensureCapacity(@intCast(expected -| 4)); - } - - return expected; - } - - pub const bytes = read; - - pub fn String(this: @This()) !bun.String { - var result = try this.readZ(); - defer result.deinit(); - return bun.String.fromUTF8(result.slice()); - } - }; -} - -pub fn NewReader(comptime Context: type) type { - return NewReaderWrap(Context, Context.markMessageStart, Context.peek, Context.skip, Context.ensureLength, Context.read, Context.readZ); -} - -pub fn NewWriter(comptime Context: type) type { - return NewWriterWrap(Context, Context.offset, Context.write, Context.pwrite); -} - -fn decoderWrap(comptime Container: type, comptime decodeFn: anytype) type { - return struct { - pub fn decode(this: *Container, context: anytype) AnyPostgresError!void { - const Context = @TypeOf(context); - try decodeFn(this, Context, NewReader(Context){ .wrapped = context }); - } - }; -} - -fn writeWrap(comptime Container: type, comptime writeFn: anytype) type { - return struct { - pub fn write(this: *Container, context: anytype) AnyPostgresError!void { - const Context = @TypeOf(context); - try writeFn(this, Context, NewWriter(Context){ .wrapped = context }); - } - }; -} - -pub const Authentication = union(enum) { - Ok: void, - ClearTextPassword: struct {}, - MD5Password: struct { - salt: [4]u8, - }, - KerberosV5: struct {}, - SCMCredential: struct {}, - GSS: struct {}, - GSSContinue: struct { - data: Data, - }, - SSPI: struct {}, - SASL: struct {}, - SASLContinue: struct { - data: Data, - r: []const u8, - s: []const u8, - i: []const u8, - - pub fn iterationCount(this: *const @This()) !u32 { - return try std.fmt.parseInt(u32, this.i, 0); - } - }, - SASLFinal: struct { - data: Data, - }, - Unknown: void, - - pub fn deinit(this: *@This()) void { - switch (this.*) { - .MD5Password => {}, - .SASL => {}, - .SASLContinue => { - this.SASLContinue.data.zdeinit(); - }, - .SASLFinal => { - this.SASLFinal.data.zdeinit(); - }, - else => {}, - } - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const message_length = try reader.length(); - - switch (try reader.int4()) { - 0 => { - if (message_length != 8) return error.InvalidMessageLength; - this.* = .{ .Ok = {} }; - }, - 2 => { - if (message_length != 8) return error.InvalidMessageLength; - this.* = .{ - .KerberosV5 = .{}, - }; - }, - 3 => { - if (message_length != 8) return error.InvalidMessageLength; - this.* = .{ - .ClearTextPassword = .{}, - }; - }, - 5 => { - if (message_length != 12) return error.InvalidMessageLength; - var salt_data = try reader.bytes(4); - defer salt_data.deinit(); - this.* = .{ - .MD5Password = .{ - .salt = salt_data.slice()[0..4].*, - }, - }; - }, - 7 => { - if (message_length != 8) return error.InvalidMessageLength; - this.* = .{ - .GSS = .{}, - }; - }, - - 8 => { - if (message_length < 9) return error.InvalidMessageLength; - const bytes = try reader.read(message_length - 8); - this.* = .{ - .GSSContinue = .{ - .data = bytes, - }, - }; - }, - 9 => { - if (message_length != 8) return error.InvalidMessageLength; - this.* = .{ - .SSPI = .{}, - }; - }, - - 10 => { - if (message_length < 9) return error.InvalidMessageLength; - try reader.skip(message_length - 8); - this.* = .{ - .SASL = .{}, - }; - }, - - 11 => { - if (message_length < 9) return error.InvalidMessageLength; - var bytes = try reader.bytes(message_length - 8); - errdefer { - bytes.deinit(); - } - - var iter = bun.strings.split(bytes.slice(), ","); - var r: ?[]const u8 = null; - var i: ?[]const u8 = null; - var s: ?[]const u8 = null; - - while (iter.next()) |item| { - if (item.len > 2) { - const key = item[0]; - const after_equals = item[2..]; - if (key == 'r') { - r = after_equals; - } else if (key == 's') { - s = after_equals; - } else if (key == 'i') { - i = after_equals; - } - } - } - - if (r == null) { - debug("Missing r", .{}); - } - - if (s == null) { - debug("Missing s", .{}); - } - - if (i == null) { - debug("Missing i", .{}); - } - - this.* = .{ - .SASLContinue = .{ - .data = bytes, - .r = r orelse return error.InvalidMessage, - .s = s orelse return error.InvalidMessage, - .i = i orelse return error.InvalidMessage, - }, - }; - }, - - 12 => { - if (message_length < 9) return error.InvalidMessageLength; - const remaining: usize = message_length - 8; - - const bytes = try reader.read(remaining); - this.* = .{ - .SASLFinal = .{ - .data = bytes, - }, - }; - }, - - else => { - this.* = .{ .Unknown = {} }; - }, - } - } - - pub const decode = decoderWrap(Authentication, decodeInternal).decode; -}; - -pub const ParameterStatus = struct { - name: Data = .{ .empty = {} }, - value: Data = .{ .empty = {} }, - - pub fn deinit(this: *@This()) void { - this.name.deinit(); - this.value.deinit(); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const length = try reader.length(); - bun.assert(length >= 4); - - this.* = .{ - .name = try reader.readZ(), - .value = try reader.readZ(), - }; - } - - pub const decode = decoderWrap(ParameterStatus, decodeInternal).decode; -}; - -pub const BackendKeyData = struct { - process_id: u32 = 0, - secret_key: u32 = 0, - pub const decode = decoderWrap(BackendKeyData, decodeInternal).decode; - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - if (!try reader.expectInt(u32, 12)) { - return error.InvalidBackendKeyData; - } - - this.* = .{ - .process_id = @bitCast(try reader.int4()), - .secret_key = @bitCast(try reader.int4()), - }; - } -}; - -pub const ErrorResponse = struct { - messages: std.ArrayListUnmanaged(FieldMessage) = .{}, - - pub fn format(formatter: ErrorResponse, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - for (formatter.messages.items) |message| { - try std.fmt.format(writer, "{}\n", .{message}); - } - } - - pub fn deinit(this: *ErrorResponse) void { - for (this.messages.items) |*message| { - message.deinit(); - } - this.messages.deinit(bun.default_allocator); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - var remaining_bytes = try reader.length(); - if (remaining_bytes < 4) return error.InvalidMessageLength; - remaining_bytes -|= 4; - - if (remaining_bytes > 0) { - this.* = .{ - .messages = try FieldMessage.decodeList(Container, reader), - }; - } - } - - pub const decode = decoderWrap(ErrorResponse, decodeInternal).decode; - - pub fn toJS(this: ErrorResponse, globalObject: *JSC.JSGlobalObject) JSValue { - var b = bun.StringBuilder{}; - defer b.deinit(bun.default_allocator); - - // Pre-calculate capacity to avoid reallocations - for (this.messages.items) |*msg| { - b.cap += switch (msg.*) { - inline else => |m| m.utf8ByteLength(), - } + 1; - } - b.allocate(bun.default_allocator) catch {}; - - // Build a more structured error message - var severity: String = String.dead; - var code: String = String.dead; - var message: String = String.dead; - var detail: String = String.dead; - var hint: String = String.dead; - var position: String = String.dead; - var where: String = String.dead; - var schema: String = String.dead; - var table: String = String.dead; - var column: String = String.dead; - var datatype: String = String.dead; - var constraint: String = String.dead; - var file: String = String.dead; - var line: String = String.dead; - var routine: String = String.dead; - - for (this.messages.items) |*msg| { - switch (msg.*) { - .severity => |str| severity = str, - .code => |str| code = str, - .message => |str| message = str, - .detail => |str| detail = str, - .hint => |str| hint = str, - .position => |str| position = str, - .where => |str| where = str, - .schema => |str| schema = str, - .table => |str| table = str, - .column => |str| column = str, - .datatype => |str| datatype = str, - .constraint => |str| constraint = str, - .file => |str| file = str, - .line => |str| line = str, - .routine => |str| routine = str, - else => {}, - } - } - - var needs_newline = false; - construct_message: { - if (!message.isEmpty()) { - _ = b.appendStr(message); - needs_newline = true; - break :construct_message; - } - if (!detail.isEmpty()) { - if (needs_newline) { - _ = b.append("\n"); - } else { - _ = b.append(" "); - } - needs_newline = true; - _ = b.appendStr(detail); - } - if (!hint.isEmpty()) { - if (needs_newline) { - _ = b.append("\n"); - } else { - _ = b.append(" "); - } - needs_newline = true; - _ = b.appendStr(hint); - } - } - - const possible_fields = .{ - .{ "detail", detail, void }, - .{ "hint", hint, void }, - .{ "column", column, void }, - .{ "constraint", constraint, void }, - .{ "datatype", datatype, void }, - // in the past this was set to i32 but postgres returns a strings lets keep it compatible - .{ "errno", code, void }, - .{ "position", position, i32 }, - .{ "schema", schema, void }, - .{ "table", table, void }, - .{ "where", where, void }, - }; - const error_code: JSC.Error = - // https://www.postgresql.org/docs/8.1/errcodes-appendix.html - if (code.eqlComptime("42601")) - .POSTGRES_SYNTAX_ERROR - else - .POSTGRES_SERVER_ERROR; - const err = error_code.fmt(globalObject, "{s}", .{b.allocatedSlice()[0..b.len]}); - - inline for (possible_fields) |field| { - if (!field.@"1".isEmpty()) { - const value = brk: { - if (field.@"2" == i32) { - if (field.@"1".toInt32()) |val| { - break :brk JSC.JSValue.jsNumberFromInt32(val); - } - } - - break :brk field.@"1".toJS(globalObject); - }; - - err.put(globalObject, JSC.ZigString.static(field.@"0"), value); - } - } - - return err; - } -}; - -pub const PortalOrPreparedStatement = union(enum) { - portal: []const u8, - prepared_statement: []const u8, - - pub fn slice(this: @This()) []const u8 { - return switch (this) { - .portal => this.portal, - .prepared_statement => this.prepared_statement, - }; - } - - pub fn tag(this: @This()) u8 { - return switch (this) { - .portal => 'P', - .prepared_statement => 'S', - }; - } -}; - -/// Close (F) -/// Byte1('C') -/// - Identifies the message as a Close command. -/// Int32 -/// - Length of message contents in bytes, including self. -/// Byte1 -/// - 'S' to close a prepared statement; or 'P' to close a portal. -/// String -/// - The name of the prepared statement or portal to close (an empty string selects the unnamed prepared statement or portal). -pub const Close = struct { - p: PortalOrPreparedStatement, - - fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const p = this.p; - const count: u32 = @sizeOf((u32)) + 1 + p.slice().len + 1; - const header = [_]u8{ - 'C', - } ++ @byteSwap(count) ++ [_]u8{ - p.tag(), - }; - try writer.write(&header); - try writer.write(p.slice()); - try writer.write(&[_]u8{0}); - } - - pub const write = writeWrap(@This(), writeInternal); -}; - -pub const CloseComplete = [_]u8{'3'} ++ toBytes(Int32(4)); -pub const EmptyQueryResponse = [_]u8{'I'} ++ toBytes(Int32(4)); -pub const Terminate = [_]u8{'X'} ++ toBytes(Int32(4)); - -fn Int32(value: anytype) [4]u8 { - return @bitCast(@byteSwap(@as(int4, @intCast(value)))); -} - -const toBytes = std.mem.toBytes; - -pub const TransactionStatusIndicator = enum(u8) { - /// if idle (not in a transaction block) - I = 'I', - - /// if in a transaction block - T = 'T', - - /// if in a failed transaction block - E = 'E', - - _, -}; - -pub const ReadyForQuery = struct { - status: TransactionStatusIndicator = .I, - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const length = try reader.length(); - bun.assert(length >= 4); - - const status = try reader.int(u8); - this.* = .{ - .status = @enumFromInt(status), - }; - } - - pub const decode = decoderWrap(ReadyForQuery, decodeInternal).decode; -}; - -pub const null_int4 = 4294967295; - -pub const DataRow = struct { - pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) AnyPostgresError!bool) AnyPostgresError!void { - var remaining_bytes = try reader.length(); - remaining_bytes -|= 4; - - const remaining_fields: usize = @intCast(@max(try reader.short(), 0)); - - for (0..remaining_fields) |index| { - const byte_length = try reader.int4(); - switch (byte_length) { - 0 => { - var empty = Data.Empty; - if (!try forEach(context, @intCast(index), &empty)) break; - }, - null_int4 => { - if (!try forEach(context, @intCast(index), null)) break; - }, - else => { - var bytes = try reader.bytes(@intCast(byte_length)); - if (!try forEach(context, @intCast(index), &bytes)) break; - }, - } - } - } -}; - -pub const BindComplete = [_]u8{'2'} ++ toBytes(Int32(4)); - -pub const ColumnIdentifier = union(enum) { - name: Data, - index: u32, - duplicate: void, - - pub fn init(name: Data) !@This() { - if (switch (name.slice().len) { - 1..."4294967295".len => true, - 0 => return .{ .name = .{ .empty = {} } }, - else => false, - }) might_be_int: { - // use a u64 to avoid overflow - var int: u64 = 0; - for (name.slice()) |byte| { - int = int * 10 + switch (byte) { - '0'...'9' => @as(u64, byte - '0'), - else => break :might_be_int, - }; - } - - // JSC only supports indexed property names up to 2^32 - if (int < std.math.maxInt(u32)) - return .{ .index = @intCast(int) }; - } - - return .{ .name = .{ .owned = try name.toOwned() } }; - } - - pub fn deinit(this: *@This()) void { - switch (this.*) { - .name => |*name| name.deinit(), - else => {}, - } - } -}; -pub const FieldDescription = struct { - /// JavaScriptCore treats numeric property names differently than string property names. - /// so we do the work to figure out if the property name is a number ahead of time. - name_or_index: ColumnIdentifier = .{ - .name = .{ .empty = {} }, - }, - table_oid: int4 = 0, - column_index: short = 0, - type_oid: int4 = 0, - binary: bool = false, - pub fn typeTag(this: @This()) types.Tag { - return @enumFromInt(@as(short, @truncate(this.type_oid))); - } - - pub fn deinit(this: *@This()) void { - this.name_or_index.deinit(); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) AnyPostgresError!void { - var name = try reader.readZ(); - errdefer { - name.deinit(); - } - - // Field name (null-terminated string) - const field_name = try ColumnIdentifier.init(name); - // Table OID (4 bytes) - // If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. - const table_oid = try reader.int4(); - - // Column attribute number (2 bytes) - // If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. - const column_index = try reader.short(); - - // Data type OID (4 bytes) - // The object ID of the field's data type. The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. - const type_oid = try reader.int4(); - - // Data type size (2 bytes) The data type size (see pg_type.typlen). Note that negative values denote variable-width types. - // Type modifier (4 bytes) The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. - try reader.skip(6); - - // Format code (2 bytes) - // The format code being used for the field. Currently will be zero (text) or one (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero. - const binary = switch (try reader.short()) { - 0 => false, - 1 => true, - else => return error.UnknownFormatCode, - }; - this.* = .{ - .table_oid = table_oid, - .column_index = column_index, - .type_oid = type_oid, - .binary = binary, - .name_or_index = field_name, - }; - } - - pub const decode = decoderWrap(FieldDescription, decodeInternal).decode; -}; - -pub const RowDescription = struct { - fields: []FieldDescription = &[_]FieldDescription{}, - pub fn deinit(this: *@This()) void { - for (this.fields) |*field| { - field.deinit(); - } - - bun.default_allocator.free(this.fields); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - var remaining_bytes = try reader.length(); - remaining_bytes -|= 4; - - const field_count: usize = @intCast(@max(try reader.short(), 0)); - var fields = try bun.default_allocator.alloc( - FieldDescription, - field_count, - ); - var remaining = fields; - errdefer { - for (fields[0 .. field_count - remaining.len]) |*field| { - field.deinit(); - } - - bun.default_allocator.free(fields); - } - while (remaining.len > 0) { - try remaining[0].decodeInternal(Container, reader); - remaining = remaining[1..]; - } - this.* = .{ - .fields = fields, - }; - } - - pub const decode = decoderWrap(RowDescription, decodeInternal).decode; -}; - -pub const ParameterDescription = struct { - parameters: []int4 = &[_]int4{}, - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - var remaining_bytes = try reader.length(); - remaining_bytes -|= 4; - - const count = try reader.short(); - const parameters = try bun.default_allocator.alloc(int4, @intCast(@max(count, 0))); - - var data = try reader.read(@as(usize, @intCast(@max(count, 0))) * @sizeOf((int4))); - defer data.deinit(); - const input_params: []align(1) const int4 = toInt32Slice(int4, data.slice()); - for (input_params, parameters) |src, *dest| { - dest.* = @byteSwap(src); - } - - this.* = .{ - .parameters = parameters, - }; - } - - pub const decode = decoderWrap(ParameterDescription, decodeInternal).decode; -}; - -// workaround for zig compiler TODO -fn toInt32Slice(comptime Int: type, slice: []const u8) []align(1) const Int { - return @as([*]align(1) const Int, @ptrCast(slice.ptr))[0 .. slice.len / @sizeOf((Int))]; -} - -pub const NotificationResponse = struct { - pid: int4 = 0, - channel: bun.ByteList = .{}, - payload: bun.ByteList = .{}, - - pub fn deinit(this: *@This()) void { - this.channel.deinitWithAllocator(bun.default_allocator); - this.payload.deinitWithAllocator(bun.default_allocator); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const length = try reader.length(); - bun.assert(length >= 4); - - this.* = .{ - .pid = try reader.int4(), - .channel = (try reader.readZ()).toOwned(), - .payload = (try reader.readZ()).toOwned(), - }; - } - - pub const decode = decoderWrap(NotificationResponse, decodeInternal).decode; -}; - -pub const CommandComplete = struct { - command_tag: Data = .{ .empty = {} }, - - pub fn deinit(this: *@This()) void { - this.command_tag.deinit(); - } - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const length = try reader.length(); - bun.assert(length >= 4); - - const tag = try reader.readZ(); - this.* = .{ - .command_tag = tag, - }; - } - - pub const decode = decoderWrap(CommandComplete, decodeInternal).decode; -}; - -pub const Parse = struct { - name: []const u8 = "", - query: []const u8 = "", - params: []const int4 = &.{}, - - pub fn deinit(this: *Parse) void { - _ = this; - } - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const parameters = this.params; - const count: usize = @sizeOf((u32)) + @sizeOf(u16) + (parameters.len * @sizeOf(u32)) + @max(zCount(this.name), 1) + @max(zCount(this.query), 1); - const header = [_]u8{ - 'P', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(this.name); - try writer.string(this.query); - try writer.short(parameters.len); - for (parameters) |parameter| { - try writer.int4(parameter); - } - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const ParseComplete = [_]u8{'1'} ++ toBytes(Int32(4)); - -pub const PasswordMessage = struct { - password: Data = .{ .empty = {} }, - - pub fn deinit(this: *PasswordMessage) void { - this.password.deinit(); - } - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const password = this.password.slice(); - const count: usize = @sizeOf((u32)) + password.len + 1; - const header = [_]u8{ - 'p', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(password); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const CopyData = struct { - data: Data = .{ .empty = {} }, - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - const length = try reader.length(); - - const data = try reader.read(@intCast(length -| 5)); - this.* = .{ - .data = data, - }; - } - - pub const decode = decoderWrap(CopyData, decodeInternal).decode; - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const data = this.data.slice(); - const count: u32 = @sizeOf((u32)) + data.len + 1; - const header = [_]u8{ - 'd', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(data); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const CopyDone = [_]u8{'c'} ++ toBytes(Int32(4)); -pub const Sync = [_]u8{'S'} ++ toBytes(Int32(4)); -pub const Flush = [_]u8{'H'} ++ toBytes(Int32(4)); -pub const SSLRequest = toBytes(Int32(8)) ++ toBytes(Int32(80877103)); -pub const NoData = [_]u8{'n'} ++ toBytes(Int32(4)); - -pub fn writeQuery(query: []const u8, comptime Context: type, writer: NewWriter(Context)) !void { - const count: u32 = @sizeOf((u32)) + @as(u32, @intCast(query.len)) + 1; - const header = [_]u8{ - 'Q', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(query); -} -pub const SASLInitialResponse = struct { - mechanism: Data = .{ .empty = {} }, - data: Data = .{ .empty = {} }, - - pub fn deinit(this: *SASLInitialResponse) void { - this.mechanism.deinit(); - this.data.deinit(); - } - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const mechanism = this.mechanism.slice(); - const data = this.data.slice(); - const count: usize = @sizeOf(u32) + mechanism.len + 1 + data.len + @sizeOf(u32); - const header = [_]u8{ - 'p', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(mechanism); - try writer.int4(@truncate(data.len)); - try writer.write(data); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const SASLResponse = struct { - data: Data = .{ .empty = {} }, - - pub fn deinit(this: *SASLResponse) void { - this.data.deinit(); - } - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const data = this.data.slice(); - const count: usize = @sizeOf(u32) + data.len; - const header = [_]u8{ - 'p', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.write(data); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const StartupMessage = struct { - user: Data, - database: Data, - options: Data = Data{ .empty = {} }, - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const user = this.user.slice(); - const database = this.database.slice(); - const options = this.options.slice(); - const count: usize = @sizeOf((int4)) + @sizeOf((int4)) + zFieldCount("user", user) + zFieldCount("database", database) + zFieldCount("client_encoding", "UTF8") + options.len + 1; - - const header = toBytes(Int32(@as(u32, @truncate(count)))); - try writer.write(&header); - try writer.int4(196608); - - try writer.string("user"); - if (user.len > 0) - try writer.string(user); - - try writer.string("database"); - - if (database.len == 0) { - // The database to connect to. Defaults to the user name. - try writer.string(user); - } else { - try writer.string(database); - } - try writer.string("client_encoding"); - try writer.string("UTF8"); - if (options.len > 0) { - try writer.write(options); - } - try writer.write(&[_]u8{0}); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -fn zCount(slice: []const u8) usize { - return if (slice.len > 0) slice.len + 1 else 0; -} - -fn zFieldCount(prefix: []const u8, slice: []const u8) usize { - if (slice.len > 0) { - return zCount(prefix) + zCount(slice); - } - - return zCount(prefix); -} - -pub const Execute = struct { - max_rows: int4 = 0, - p: PortalOrPreparedStatement, - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - try writer.write("E"); - const length = try writer.length(); - if (this.p == .portal) - try writer.string(this.p.portal) - else - try writer.write(&[_]u8{0}); - try writer.int4(this.max_rows); - try length.write(); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const Describe = struct { - p: PortalOrPreparedStatement, - - pub fn writeInternal( - this: *const @This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const message = this.p.slice(); - try writer.write(&[_]u8{ - 'D', - }); - const length = try writer.length(); - try writer.write(&[_]u8{ - this.p.tag(), - }); - try writer.string(message); - try length.write(); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const NegotiateProtocolVersion = struct { - version: int4 = 0, - unrecognized_options: std.ArrayListUnmanaged(String) = .{}, - - pub fn decodeInternal( - this: *@This(), - comptime Container: type, - reader: NewReader(Container), - ) !void { - const length = try reader.length(); - bun.assert(length >= 4); - - const version = try reader.int4(); - this.* = .{ - .version = version, - }; - - const unrecognized_options_count: u32 = @intCast(@max(try reader.int4(), 0)); - try this.unrecognized_options.ensureTotalCapacity(bun.default_allocator, unrecognized_options_count); - errdefer { - for (this.unrecognized_options.items) |*option| { - option.deinit(); - } - this.unrecognized_options.deinit(bun.default_allocator); - } - for (0..unrecognized_options_count) |_| { - var option = try reader.readZ(); - if (option.slice().len == 0) break; - defer option.deinit(); - this.unrecognized_options.appendAssumeCapacity( - String.fromUTF8(option), - ); - } - } -}; - -pub const NoticeResponse = struct { - messages: std.ArrayListUnmanaged(FieldMessage) = .{}, - pub fn deinit(this: *NoticeResponse) void { - for (this.messages.items) |*message| { - message.deinit(); - } - this.messages.deinit(bun.default_allocator); - } - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - var remaining_bytes = try reader.length(); - remaining_bytes -|= 4; - - if (remaining_bytes > 0) { - this.* = .{ - .messages = try FieldMessage.decodeList(Container, reader), - }; - } - } - pub const decode = decoderWrap(NoticeResponse, decodeInternal).decode; - - pub fn toJS(this: NoticeResponse, globalObject: *JSC.JSGlobalObject) JSValue { - var b = bun.StringBuilder{}; - defer b.deinit(bun.default_allocator); - - for (this.messages.items) |msg| { - b.cap += switch (msg) { - inline else => |m| m.utf8ByteLength(), - } + 1; - } - b.allocate(bun.default_allocator) catch {}; - - for (this.messages.items) |msg| { - var str = switch (msg) { - inline else => |m| m.toUTF8(bun.default_allocator), - }; - defer str.deinit(); - _ = b.append(str.slice()); - _ = b.append("\n"); - } - - return JSC.ZigString.init(b.allocatedSlice()[0..b.len]).toJS(globalObject); - } -}; - -pub const CopyFail = struct { - message: Data = .{ .empty = {} }, - - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - _ = try reader.int4(); - - const message = try reader.readZ(); - this.* = .{ - .message = message, - }; - } - - pub const decode = decoderWrap(CopyFail, decodeInternal).decode; - - pub fn writeInternal( - this: *@This(), - comptime Context: type, - writer: NewWriter(Context), - ) !void { - const message = this.message.slice(); - const count: u32 = @sizeOf((u32)) + message.len + 1; - const header = [_]u8{ - 'f', - } ++ toBytes(Int32(count)); - try writer.write(&header); - try writer.string(message); - } - - pub const write = writeWrap(@This(), writeInternal).write; -}; - -pub const CopyInResponse = struct { - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - _ = reader; - _ = this; - TODO(@This()); - } - - pub const decode = decoderWrap(CopyInResponse, decodeInternal).decode; -}; - -pub const CopyOutResponse = struct { - pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { - _ = reader; - _ = this; - TODO(@This()); - } - - pub const decode = decoderWrap(CopyInResponse, decodeInternal).decode; -}; - -fn TODO(comptime Type: type) !void { - bun.Output.panic("TODO: not implemented {s}", .{bun.meta.typeBaseName(@typeName(Type))}); -} diff --git a/src/sql/postgres/protocol/ArrayList.zig b/src/sql/postgres/protocol/ArrayList.zig new file mode 100644 index 0000000000..0fff3a0c0f --- /dev/null +++ b/src/sql/postgres/protocol/ArrayList.zig @@ -0,0 +1,22 @@ +array: *std.ArrayList(u8), + +pub fn offset(this: @This()) usize { + return this.array.items.len; +} + +pub fn write(this: @This(), bytes: []const u8) AnyPostgresError!void { + try this.array.appendSlice(bytes); +} + +pub fn pwrite(this: @This(), bytes: []const u8, i: usize) AnyPostgresError!void { + @memcpy(this.array.items[i..][0..bytes.len], bytes); +} + +pub const Writer = NewWriter(@This()); + +// @sortImports + +const ArrayList = @This(); +const std = @import("std"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const NewWriter = @import("./NewWriter.zig").NewWriter; diff --git a/src/sql/postgres/protocol/Authentication.zig b/src/sql/postgres/protocol/Authentication.zig new file mode 100644 index 0000000000..91b838d332 --- /dev/null +++ b/src/sql/postgres/protocol/Authentication.zig @@ -0,0 +1,182 @@ +pub const Authentication = union(enum) { + Ok: void, + ClearTextPassword: struct {}, + MD5Password: struct { + salt: [4]u8, + }, + KerberosV5: struct {}, + SCMCredential: struct {}, + GSS: struct {}, + GSSContinue: struct { + data: Data, + }, + SSPI: struct {}, + SASL: struct {}, + SASLContinue: struct { + data: Data, + r: []const u8, + s: []const u8, + i: []const u8, + + pub fn iterationCount(this: *const @This()) !u32 { + return try std.fmt.parseInt(u32, this.i, 0); + } + }, + SASLFinal: struct { + data: Data, + }, + Unknown: void, + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .MD5Password => {}, + .SASL => {}, + .SASLContinue => { + this.SASLContinue.data.zdeinit(); + }, + .SASLFinal => { + this.SASLFinal.data.zdeinit(); + }, + else => {}, + } + } + + pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const message_length = try reader.length(); + + switch (try reader.int4()) { + 0 => { + if (message_length != 8) return error.InvalidMessageLength; + this.* = .{ .Ok = {} }; + }, + 2 => { + if (message_length != 8) return error.InvalidMessageLength; + this.* = .{ + .KerberosV5 = .{}, + }; + }, + 3 => { + if (message_length != 8) return error.InvalidMessageLength; + this.* = .{ + .ClearTextPassword = .{}, + }; + }, + 5 => { + if (message_length != 12) return error.InvalidMessageLength; + var salt_data = try reader.bytes(4); + defer salt_data.deinit(); + this.* = .{ + .MD5Password = .{ + .salt = salt_data.slice()[0..4].*, + }, + }; + }, + 7 => { + if (message_length != 8) return error.InvalidMessageLength; + this.* = .{ + .GSS = .{}, + }; + }, + + 8 => { + if (message_length < 9) return error.InvalidMessageLength; + const bytes = try reader.read(message_length - 8); + this.* = .{ + .GSSContinue = .{ + .data = bytes, + }, + }; + }, + 9 => { + if (message_length != 8) return error.InvalidMessageLength; + this.* = .{ + .SSPI = .{}, + }; + }, + + 10 => { + if (message_length < 9) return error.InvalidMessageLength; + try reader.skip(message_length - 8); + this.* = .{ + .SASL = .{}, + }; + }, + + 11 => { + if (message_length < 9) return error.InvalidMessageLength; + var bytes = try reader.bytes(message_length - 8); + errdefer { + bytes.deinit(); + } + + var iter = bun.strings.split(bytes.slice(), ","); + var r: ?[]const u8 = null; + var i: ?[]const u8 = null; + var s: ?[]const u8 = null; + + while (iter.next()) |item| { + if (item.len > 2) { + const key = item[0]; + const after_equals = item[2..]; + if (key == 'r') { + r = after_equals; + } else if (key == 's') { + s = after_equals; + } else if (key == 'i') { + i = after_equals; + } + } + } + + if (r == null) { + debug("Missing r", .{}); + } + + if (s == null) { + debug("Missing s", .{}); + } + + if (i == null) { + debug("Missing i", .{}); + } + + this.* = .{ + .SASLContinue = .{ + .data = bytes, + .r = r orelse return error.InvalidMessage, + .s = s orelse return error.InvalidMessage, + .i = i orelse return error.InvalidMessage, + }, + }; + }, + + 12 => { + if (message_length < 9) return error.InvalidMessageLength; + const remaining: usize = message_length - 8; + + const bytes = try reader.read(remaining); + this.* = .{ + .SASLFinal = .{ + .data = bytes, + }, + }; + }, + + else => { + this.* = .{ .Unknown = {} }; + }, + } + } + + pub const decode = DecoderWrap(Authentication, decodeInternal).decode; +}; + +const debug = bun.Output.scoped(.Postgres, true); + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/BackendKeyData.zig b/src/sql/postgres/protocol/BackendKeyData.zig new file mode 100644 index 0000000000..7df3e20971 --- /dev/null +++ b/src/sql/postgres/protocol/BackendKeyData.zig @@ -0,0 +1,20 @@ +process_id: u32 = 0, +secret_key: u32 = 0, +pub const decode = DecoderWrap(BackendKeyData, decodeInternal).decode; + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + if (!try reader.expectInt(u32, 12)) { + return error.InvalidBackendKeyData; + } + + this.* = .{ + .process_id = @bitCast(try reader.int4()), + .secret_key = @bitCast(try reader.int4()), + }; +} + +// @sortImports + +const BackendKeyData = @This(); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/Close.zig b/src/sql/postgres/protocol/Close.zig new file mode 100644 index 0000000000..baac29e9e8 --- /dev/null +++ b/src/sql/postgres/protocol/Close.zig @@ -0,0 +1,39 @@ +/// Close (F) +/// Byte1('C') +/// - Identifies the message as a Close command. +/// Int32 +/// - Length of message contents in bytes, including self. +/// Byte1 +/// - 'S' to close a prepared statement; or 'P' to close a portal. +/// String +/// - The name of the prepared statement or portal to close (an empty string selects the unnamed prepared statement or portal). +pub const Close = struct { + p: PortalOrPreparedStatement, + + fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), + ) !void { + const p = this.p; + const count: u32 = @sizeOf((u32)) + 1 + p.slice().len + 1; + const header = [_]u8{ + 'C', + } ++ @byteSwap(count) ++ [_]u8{ + p.tag(), + }; + try writer.write(&header); + try writer.write(p.slice()); + try writer.write(&[_]u8{0}); + } + + pub const write = WriteWrap(@This(), writeInternal); +}; + +// @sortImports + +const NewWriter = @import("./NewWriter.zig").NewWriter; + +const PortalOrPreparedStatement = @import("./PortalOrPreparedStatement.zig").PortalOrPreparedStatement; + +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; diff --git a/src/sql/postgres/protocol/ColumnIdentifier.zig b/src/sql/postgres/protocol/ColumnIdentifier.zig new file mode 100644 index 0000000000..026a6a843e --- /dev/null +++ b/src/sql/postgres/protocol/ColumnIdentifier.zig @@ -0,0 +1,40 @@ +pub const ColumnIdentifier = union(enum) { + name: Data, + index: u32, + duplicate: void, + + pub fn init(name: Data) !@This() { + if (switch (name.slice().len) { + 1..."4294967295".len => true, + 0 => return .{ .name = .{ .empty = {} } }, + else => false, + }) might_be_int: { + // use a u64 to avoid overflow + var int: u64 = 0; + for (name.slice()) |byte| { + int = int * 10 + switch (byte) { + '0'...'9' => @as(u64, byte - '0'), + else => break :might_be_int, + }; + } + + // JSC only supports indexed property names up to 2^32 + if (int < std.math.maxInt(u32)) + return .{ .index = @intCast(int) }; + } + + return .{ .name = .{ .owned = try name.toOwned() } }; + } + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .name => |*name| name.deinit(), + else => {}, + } + } +}; + +// @sortImports + +const std = @import("std"); +const Data = @import("../Data.zig").Data; diff --git a/src/sql/postgres/protocol/CommandComplete.zig b/src/sql/postgres/protocol/CommandComplete.zig new file mode 100644 index 0000000000..a9299cd1a6 --- /dev/null +++ b/src/sql/postgres/protocol/CommandComplete.zig @@ -0,0 +1,25 @@ +command_tag: Data = .{ .empty = {} }, + +pub fn deinit(this: *@This()) void { + this.command_tag.deinit(); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const length = try reader.length(); + bun.assert(length >= 4); + + const tag = try reader.readZ(); + this.* = .{ + .command_tag = tag, + }; +} + +pub const decode = DecoderWrap(CommandComplete, decodeInternal).decode; + +// @sortImports + +const CommandComplete = @This(); +const bun = @import("bun"); +const Data = @import("../Data.zig").Data; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/CopyData.zig b/src/sql/postgres/protocol/CopyData.zig new file mode 100644 index 0000000000..885bb2960e --- /dev/null +++ b/src/sql/postgres/protocol/CopyData.zig @@ -0,0 +1,40 @@ +data: Data = .{ .empty = {} }, + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const length = try reader.length(); + + const data = try reader.read(@intCast(length -| 5)); + this.* = .{ + .data = data, + }; +} + +pub const decode = DecoderWrap(CopyData, decodeInternal).decode; + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const data = this.data.slice(); + const count: u32 = @sizeOf((u32)) + data.len + 1; + const header = [_]u8{ + 'd', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(data); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const CopyData = @This(); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const Int32 = @import("../types/int_types.zig").Int32; +const NewReader = @import("./NewReader.zig").NewReader; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; diff --git a/src/sql/postgres/protocol/CopyFail.zig b/src/sql/postgres/protocol/CopyFail.zig new file mode 100644 index 0000000000..f006cafb76 --- /dev/null +++ b/src/sql/postgres/protocol/CopyFail.zig @@ -0,0 +1,42 @@ +message: Data = .{ .empty = {} }, + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + _ = try reader.int4(); + + const message = try reader.readZ(); + this.* = .{ + .message = message, + }; +} + +pub const decode = DecoderWrap(CopyFail, decodeInternal).decode; + +pub fn writeInternal( + this: *@This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const message = this.message.slice(); + const count: u32 = @sizeOf((u32)) + message.len + 1; + const header = [_]u8{ + 'f', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(message); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const CopyFail = @This(); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; + +const int_types = @import("../types/int_types.zig"); +const Int32 = int_types.Int32; diff --git a/src/sql/postgres/protocol/CopyInResponse.zig b/src/sql/postgres/protocol/CopyInResponse.zig new file mode 100644 index 0000000000..47dbdd850f --- /dev/null +++ b/src/sql/postgres/protocol/CopyInResponse.zig @@ -0,0 +1,14 @@ +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + _ = reader; + _ = this; + bun.Output.panic("TODO: not implemented {s}", .{bun.meta.typeBaseName(@typeName(@This()))}); +} + +pub const decode = DecoderWrap(CopyInResponse, decodeInternal).decode; + +// @sortImports + +const CopyInResponse = @This(); +const bun = @import("bun"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/CopyOutResponse.zig b/src/sql/postgres/protocol/CopyOutResponse.zig new file mode 100644 index 0000000000..45650a3f41 --- /dev/null +++ b/src/sql/postgres/protocol/CopyOutResponse.zig @@ -0,0 +1,14 @@ +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + _ = reader; + _ = this; + bun.Output.panic("TODO: not implemented {s}", .{bun.meta.typeBaseName(@typeName(@This()))}); +} + +pub const decode = DecoderWrap(CopyOutResponse, decodeInternal).decode; + +// @sortImports + +const CopyOutResponse = @This(); +const bun = @import("bun"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/DataRow.zig b/src/sql/postgres/protocol/DataRow.zig new file mode 100644 index 0000000000..125a25f1f2 --- /dev/null +++ b/src/sql/postgres/protocol/DataRow.zig @@ -0,0 +1,33 @@ +pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) AnyPostgresError!bool) AnyPostgresError!void { + var remaining_bytes = try reader.length(); + remaining_bytes -|= 4; + + const remaining_fields: usize = @intCast(@max(try reader.short(), 0)); + + for (0..remaining_fields) |index| { + const byte_length = try reader.int4(); + switch (byte_length) { + 0 => { + var empty = Data.Empty; + if (!try forEach(context, @intCast(index), &empty)) break; + }, + null_int4 => { + if (!try forEach(context, @intCast(index), null)) break; + }, + else => { + var bytes = try reader.bytes(@intCast(byte_length)); + if (!try forEach(context, @intCast(index), &bytes)) break; + }, + } + } +} + +pub const null_int4 = 4294967295; + +// @sortImports + +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const Data = @import("../Data.zig").Data; + +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/DecoderWrap.zig b/src/sql/postgres/protocol/DecoderWrap.zig new file mode 100644 index 0000000000..fe2b78902f --- /dev/null +++ b/src/sql/postgres/protocol/DecoderWrap.zig @@ -0,0 +1,14 @@ +pub fn DecoderWrap(comptime Container: type, comptime decodeFn: anytype) type { + return struct { + pub fn decode(this: *Container, context: anytype) AnyPostgresError!void { + const Context = @TypeOf(context); + try decodeFn(this, Context, NewReader(Context){ .wrapped = context }); + } + }; +} + +// @sortImports + +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/Describe.zig b/src/sql/postgres/protocol/Describe.zig new file mode 100644 index 0000000000..4dc9fd2728 --- /dev/null +++ b/src/sql/postgres/protocol/Describe.zig @@ -0,0 +1,28 @@ +p: PortalOrPreparedStatement, + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const message = this.p.slice(); + try writer.write(&[_]u8{ + 'D', + }); + const length = try writer.length(); + try writer.write(&[_]u8{ + this.p.tag(), + }); + try writer.string(message); + try length.write(); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const NewWriter = @import("./NewWriter.zig").NewWriter; + +const PortalOrPreparedStatement = @import("./PortalOrPreparedStatement.zig").PortalOrPreparedStatement; + +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; diff --git a/src/sql/postgres/protocol/ErrorResponse.zig b/src/sql/postgres/protocol/ErrorResponse.zig new file mode 100644 index 0000000000..e70d2215a1 --- /dev/null +++ b/src/sql/postgres/protocol/ErrorResponse.zig @@ -0,0 +1,159 @@ +messages: std.ArrayListUnmanaged(FieldMessage) = .{}, + +pub fn format(formatter: ErrorResponse, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + for (formatter.messages.items) |message| { + try std.fmt.format(writer, "{}\n", .{message}); + } +} + +pub fn deinit(this: *ErrorResponse) void { + for (this.messages.items) |*message| { + message.deinit(); + } + this.messages.deinit(bun.default_allocator); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + var remaining_bytes = try reader.length(); + if (remaining_bytes < 4) return error.InvalidMessageLength; + remaining_bytes -|= 4; + + if (remaining_bytes > 0) { + this.* = .{ + .messages = try FieldMessage.decodeList(Container, reader), + }; + } +} + +pub const decode = DecoderWrap(ErrorResponse, decodeInternal).decode; + +pub fn toJS(this: ErrorResponse, globalObject: *JSC.JSGlobalObject) JSValue { + var b = bun.StringBuilder{}; + defer b.deinit(bun.default_allocator); + + // Pre-calculate capacity to avoid reallocations + for (this.messages.items) |*msg| { + b.cap += switch (msg.*) { + inline else => |m| m.utf8ByteLength(), + } + 1; + } + b.allocate(bun.default_allocator) catch {}; + + // Build a more structured error message + var severity: String = String.dead; + var code: String = String.dead; + var message: String = String.dead; + var detail: String = String.dead; + var hint: String = String.dead; + var position: String = String.dead; + var where: String = String.dead; + var schema: String = String.dead; + var table: String = String.dead; + var column: String = String.dead; + var datatype: String = String.dead; + var constraint: String = String.dead; + var file: String = String.dead; + var line: String = String.dead; + var routine: String = String.dead; + + for (this.messages.items) |*msg| { + switch (msg.*) { + .severity => |str| severity = str, + .code => |str| code = str, + .message => |str| message = str, + .detail => |str| detail = str, + .hint => |str| hint = str, + .position => |str| position = str, + .where => |str| where = str, + .schema => |str| schema = str, + .table => |str| table = str, + .column => |str| column = str, + .datatype => |str| datatype = str, + .constraint => |str| constraint = str, + .file => |str| file = str, + .line => |str| line = str, + .routine => |str| routine = str, + else => {}, + } + } + + var needs_newline = false; + construct_message: { + if (!message.isEmpty()) { + _ = b.appendStr(message); + needs_newline = true; + break :construct_message; + } + if (!detail.isEmpty()) { + if (needs_newline) { + _ = b.append("\n"); + } else { + _ = b.append(" "); + } + needs_newline = true; + _ = b.appendStr(detail); + } + if (!hint.isEmpty()) { + if (needs_newline) { + _ = b.append("\n"); + } else { + _ = b.append(" "); + } + needs_newline = true; + _ = b.appendStr(hint); + } + } + + const possible_fields = .{ + .{ "detail", detail, void }, + .{ "hint", hint, void }, + .{ "column", column, void }, + .{ "constraint", constraint, void }, + .{ "datatype", datatype, void }, + // in the past this was set to i32 but postgres returns a strings lets keep it compatible + .{ "errno", code, void }, + .{ "position", position, i32 }, + .{ "schema", schema, void }, + .{ "table", table, void }, + .{ "where", where, void }, + }; + const error_code: JSC.Error = + // https://www.postgresql.org/docs/8.1/errcodes-appendix.html + if (code.eqlComptime("42601")) + .POSTGRES_SYNTAX_ERROR + else + .POSTGRES_SERVER_ERROR; + const err = error_code.fmt(globalObject, "{s}", .{b.allocatedSlice()[0..b.len]}); + + inline for (possible_fields) |field| { + if (!field.@"1".isEmpty()) { + const value = brk: { + if (field.@"2" == i32) { + if (field.@"1".toInt32()) |val| { + break :brk JSC.JSValue.jsNumberFromInt32(val); + } + } + + break :brk field.@"1".toJS(globalObject); + }; + + err.put(globalObject, JSC.ZigString.static(field.@"0"), value); + } + } + + return err; +} + +// @sortImports + +const ErrorResponse = @This(); +const std = @import("std"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const FieldMessage = @import("./FieldMessage.zig").FieldMessage; +const NewReader = @import("./NewReader.zig").NewReader; + +const bun = @import("bun"); +const String = bun.String; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/protocol/Execute.zig b/src/sql/postgres/protocol/Execute.zig new file mode 100644 index 0000000000..648d39da4f --- /dev/null +++ b/src/sql/postgres/protocol/Execute.zig @@ -0,0 +1,28 @@ +max_rows: int4 = 0, +p: PortalOrPreparedStatement, + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + try writer.write("E"); + const length = try writer.length(); + if (this.p == .portal) + try writer.string(this.p.portal) + else + try writer.write(&[_]u8{0}); + try writer.int4(this.max_rows); + try length.write(); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const NewWriter = @import("./NewWriter.zig").NewWriter; +const PortalOrPreparedStatement = @import("./PortalOrPreparedStatement.zig").PortalOrPreparedStatement; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; + +const int_types = @import("../types/int_types.zig"); +const int4 = int_types.int4; diff --git a/src/sql/postgres/protocol/FieldDescription.zig b/src/sql/postgres/protocol/FieldDescription.zig new file mode 100644 index 0000000000..860176c5b3 --- /dev/null +++ b/src/sql/postgres/protocol/FieldDescription.zig @@ -0,0 +1,70 @@ +/// JavaScriptCore treats numeric property names differently than string property names. +/// so we do the work to figure out if the property name is a number ahead of time. +name_or_index: ColumnIdentifier = .{ + .name = .{ .empty = {} }, +}, +table_oid: int4 = 0, +column_index: short = 0, +type_oid: int4 = 0, +binary: bool = false, +pub fn typeTag(this: @This()) types.Tag { + return @enumFromInt(@as(short, @truncate(this.type_oid))); +} + +pub fn deinit(this: *@This()) void { + this.name_or_index.deinit(); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) AnyPostgresError!void { + var name = try reader.readZ(); + errdefer { + name.deinit(); + } + + // Field name (null-terminated string) + const field_name = try ColumnIdentifier.init(name); + // Table OID (4 bytes) + // If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. + const table_oid = try reader.int4(); + + // Column attribute number (2 bytes) + // If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. + const column_index = try reader.short(); + + // Data type OID (4 bytes) + // The object ID of the field's data type. The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. + const type_oid = try reader.int4(); + + // Data type size (2 bytes) The data type size (see pg_type.typlen). Note that negative values denote variable-width types. + // Type modifier (4 bytes) The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. + try reader.skip(6); + + // Format code (2 bytes) + // The format code being used for the field. Currently will be zero (text) or one (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero. + const binary = switch (try reader.short()) { + 0 => false, + 1 => true, + else => return error.UnknownFormatCode, + }; + this.* = .{ + .table_oid = table_oid, + .column_index = column_index, + .type_oid = type_oid, + .binary = binary, + .name_or_index = field_name, + }; +} + +pub const decode = DecoderWrap(FieldDescription, decodeInternal).decode; + +// @sortImports + +const FieldDescription = @This(); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const ColumnIdentifier = @import("./ColumnIdentifier.zig").ColumnIdentifier; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; + +const types = @import("../PostgresTypes.zig"); +const int4 = types.int4; +const short = types.short; diff --git a/src/sql/postgres/protocol/FieldMessage.zig b/src/sql/postgres/protocol/FieldMessage.zig new file mode 100644 index 0000000000..d3d2c1fdbf --- /dev/null +++ b/src/sql/postgres/protocol/FieldMessage.zig @@ -0,0 +1,87 @@ +pub const FieldMessage = union(FieldType) { + severity: String, + localized_severity: String, + code: String, + message: String, + detail: String, + hint: String, + position: String, + internal_position: String, + internal: String, + where: String, + schema: String, + table: String, + column: String, + datatype: String, + constraint: String, + file: String, + line: String, + routine: String, + + pub fn format(this: FieldMessage, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this) { + inline else => |str| { + try std.fmt.format(writer, "{}", .{str}); + }, + } + } + + pub fn deinit(this: *FieldMessage) void { + switch (this.*) { + inline else => |*message| { + message.deref(); + }, + } + } + + pub fn decodeList(comptime Context: type, reader: NewReader(Context)) !std.ArrayListUnmanaged(FieldMessage) { + var messages = std.ArrayListUnmanaged(FieldMessage){}; + while (true) { + const field_int = try reader.int(u8); + if (field_int == 0) break; + const field: FieldType = @enumFromInt(field_int); + + var message = try reader.readZ(); + defer message.deinit(); + if (message.slice().len == 0) break; + + try messages.append(bun.default_allocator, FieldMessage.init(field, message.slice()) catch continue); + } + + return messages; + } + + pub fn init(tag: FieldType, message: []const u8) !FieldMessage { + return switch (tag) { + .severity => FieldMessage{ .severity = String.createUTF8(message) }, + // Ignore this one for now. + // .localized_severity => FieldMessage{ .localized_severity = String.createUTF8(message) }, + .code => FieldMessage{ .code = String.createUTF8(message) }, + .message => FieldMessage{ .message = String.createUTF8(message) }, + .detail => FieldMessage{ .detail = String.createUTF8(message) }, + .hint => FieldMessage{ .hint = String.createUTF8(message) }, + .position => FieldMessage{ .position = String.createUTF8(message) }, + .internal_position => FieldMessage{ .internal_position = String.createUTF8(message) }, + .internal => FieldMessage{ .internal = String.createUTF8(message) }, + .where => FieldMessage{ .where = String.createUTF8(message) }, + .schema => FieldMessage{ .schema = String.createUTF8(message) }, + .table => FieldMessage{ .table = String.createUTF8(message) }, + .column => FieldMessage{ .column = String.createUTF8(message) }, + .datatype => FieldMessage{ .datatype = String.createUTF8(message) }, + .constraint => FieldMessage{ .constraint = String.createUTF8(message) }, + .file => FieldMessage{ .file = String.createUTF8(message) }, + .line => FieldMessage{ .line = String.createUTF8(message) }, + .routine => FieldMessage{ .routine = String.createUTF8(message) }, + else => error.UnknownFieldType, + }; + } +}; + +// @sortImports + +const std = @import("std"); +const FieldType = @import("./FieldType.zig").FieldType; +const NewReader = @import("./NewReader.zig").NewReader; + +const bun = @import("bun"); +const String = bun.String; diff --git a/src/sql/postgres/protocol/FieldType.zig b/src/sql/postgres/protocol/FieldType.zig new file mode 100644 index 0000000000..b5e6c860fe --- /dev/null +++ b/src/sql/postgres/protocol/FieldType.zig @@ -0,0 +1,57 @@ +pub const FieldType = enum(u8) { + /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a localized translation of one of these. Always present. + severity = 'S', + + /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the S field except that the contents are never localized. This is present only in messages generated by PostgreSQL versions 9.6 and later. + localized_severity = 'V', + + /// Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present. + code = 'C', + + /// Message: the primary human-readable error message. This should be accurate but terse (typically one line). Always present. + message = 'M', + + /// Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines. + detail = 'D', + + /// Hint: an optional suggestion what to do about the problem. This is intended to differ from Detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines. + hint = 'H', + + /// Position: the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes. + position = 'P', + + /// Internal position: this is defined the same as the P field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The q field will always appear when this field appears. + internal_position = 'p', + + /// Internal query: the text of a failed internally-generated command. This could be, for example, an SQL query issued by a PL/pgSQL function. + internal = 'q', + + /// Where: an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first. + where = 'W', + + /// Schema name: if the error was associated with a specific database object, the name of the schema containing that object, if any. + schema = 's', + + /// Table name: if the error was associated with a specific table, the name of the table. (Refer to the schema name field for the name of the table's schema.) + table = 't', + + /// Column name: if the error was associated with a specific table column, the name of the column. (Refer to the schema and table name fields to identify the table.) + column = 'c', + + /// Data type name: if the error was associated with a specific data type, the name of the data type. (Refer to the schema name field for the name of the data type's schema.) + datatype = 'd', + + /// Constraint name: if the error was associated with a specific constraint, the name of the constraint. Refer to fields listed above for the associated table or domain. (For this purpose, indexes are treated as constraints, even if they weren't created with constraint syntax.) + constraint = 'n', + + /// File: the file name of the source-code location where the error was reported. + file = 'F', + + /// Line: the line number of the source-code location where the error was reported. + line = 'L', + + /// Routine: the name of the source-code routine reporting the error. + routine = 'R', + + _, +}; diff --git a/src/sql/postgres/protocol/NegotiateProtocolVersion.zig b/src/sql/postgres/protocol/NegotiateProtocolVersion.zig new file mode 100644 index 0000000000..9b80f0fdd2 --- /dev/null +++ b/src/sql/postgres/protocol/NegotiateProtocolVersion.zig @@ -0,0 +1,44 @@ +version: int4 = 0, +unrecognized_options: std.ArrayListUnmanaged(String) = .{}, + +pub fn decodeInternal( + this: *@This(), + comptime Container: type, + reader: NewReader(Container), +) !void { + const length = try reader.length(); + bun.assert(length >= 4); + + const version = try reader.int4(); + this.* = .{ + .version = version, + }; + + const unrecognized_options_count: u32 = @intCast(@max(try reader.int4(), 0)); + try this.unrecognized_options.ensureTotalCapacity(bun.default_allocator, unrecognized_options_count); + errdefer { + for (this.unrecognized_options.items) |*option| { + option.deinit(); + } + this.unrecognized_options.deinit(bun.default_allocator); + } + for (0..unrecognized_options_count) |_| { + var option = try reader.readZ(); + if (option.slice().len == 0) break; + defer option.deinit(); + this.unrecognized_options.appendAssumeCapacity( + String.fromUTF8(option), + ); + } +} + +// @sortImports + +const std = @import("std"); +const NewReader = @import("./NewReader.zig").NewReader; + +const int_types = @import("../types/int_types.zig"); +const int4 = int_types.int4; + +const bun = @import("bun"); +const String = bun.String; diff --git a/src/sql/postgres/protocol/NewReader.zig b/src/sql/postgres/protocol/NewReader.zig new file mode 100644 index 0000000000..932d4d334d --- /dev/null +++ b/src/sql/postgres/protocol/NewReader.zig @@ -0,0 +1,118 @@ +pub fn NewReaderWrap( + comptime Context: type, + comptime markMessageStartFn_: (fn (ctx: Context) void), + comptime peekFn_: (fn (ctx: Context) []const u8), + comptime skipFn_: (fn (ctx: Context, count: usize) void), + comptime ensureCapacityFn_: (fn (ctx: Context, count: usize) bool), + comptime readFunction_: (fn (ctx: Context, count: usize) AnyPostgresError!Data), + comptime readZ_: (fn (ctx: Context) AnyPostgresError!Data), +) type { + return struct { + wrapped: Context, + const readFn = readFunction_; + const readZFn = readZ_; + const ensureCapacityFn = ensureCapacityFn_; + const skipFn = skipFn_; + const peekFn = peekFn_; + const markMessageStartFn = markMessageStartFn_; + + pub const Ctx = Context; + + pub inline fn markMessageStart(this: @This()) void { + markMessageStartFn(this.wrapped); + } + + pub inline fn read(this: @This(), count: usize) AnyPostgresError!Data { + return try readFn(this.wrapped, count); + } + + pub inline fn eatMessage(this: @This(), comptime msg_: anytype) AnyPostgresError!void { + const msg = msg_[1..]; + try this.ensureCapacity(msg.len); + + var input = try readFn(this.wrapped, msg.len); + defer input.deinit(); + if (bun.strings.eqlComptime(input.slice(), msg)) return; + return error.InvalidMessage; + } + + pub fn skip(this: @This(), count: usize) AnyPostgresError!void { + skipFn(this.wrapped, count); + } + + pub fn peek(this: @This()) []const u8 { + return peekFn(this.wrapped); + } + + pub inline fn readZ(this: @This()) AnyPostgresError!Data { + return try readZFn(this.wrapped); + } + + pub inline fn ensureCapacity(this: @This(), count: usize) AnyPostgresError!void { + if (!ensureCapacityFn(this.wrapped, count)) { + return error.ShortRead; + } + } + + pub fn int(this: @This(), comptime Int: type) !Int { + var data = try this.read(@sizeOf((Int))); + defer data.deinit(); + if (comptime Int == u8) { + return @as(Int, data.slice()[0]); + } + return @byteSwap(@as(Int, @bitCast(data.slice()[0..@sizeOf(Int)].*))); + } + + pub fn peekInt(this: @This(), comptime Int: type) ?Int { + const remain = this.peek(); + if (remain.len < @sizeOf(Int)) { + return null; + } + return @byteSwap(@as(Int, @bitCast(remain[0..@sizeOf(Int)].*))); + } + + pub fn expectInt(this: @This(), comptime Int: type, comptime value: comptime_int) !bool { + const actual = try this.int(Int); + return actual == value; + } + + pub fn int4(this: @This()) !PostgresInt32 { + return this.int(PostgresInt32); + } + + pub fn short(this: @This()) !PostgresShort { + return this.int(PostgresShort); + } + + pub fn length(this: @This()) !PostgresInt32 { + const expected = try this.int(PostgresInt32); + if (expected > -1) { + try this.ensureCapacity(@intCast(expected -| 4)); + } + + return expected; + } + + pub const bytes = read; + + pub fn String(this: @This()) !bun.String { + var result = try this.readZ(); + defer result.deinit(); + return bun.String.fromUTF8(result.slice()); + } + }; +} + +pub fn NewReader(comptime Context: type) type { + return NewReaderWrap(Context, Context.markMessageStart, Context.peek, Context.skip, Context.ensureLength, Context.read, Context.readZ); +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const Data = @import("../Data.zig").Data; + +const int_types = @import("../types/int_types.zig"); +const PostgresInt32 = int_types.PostgresInt32; +const PostgresShort = int_types.PostgresShort; diff --git a/src/sql/postgres/protocol/NewWriter.zig b/src/sql/postgres/protocol/NewWriter.zig new file mode 100644 index 0000000000..6f6a800328 --- /dev/null +++ b/src/sql/postgres/protocol/NewWriter.zig @@ -0,0 +1,125 @@ +pub fn NewWriterWrap( + comptime Context: type, + comptime offsetFn_: (fn (ctx: Context) usize), + comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) AnyPostgresError!void), + comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) AnyPostgresError!void), +) type { + return struct { + wrapped: Context, + + const writeFn = writeFunction_; + const pwriteFn = pwriteFunction_; + const offsetFn = offsetFn_; + pub const Ctx = Context; + + pub const WrappedWriter = @This(); + + pub inline fn write(this: @This(), data: []const u8) AnyPostgresError!void { + try writeFn(this.wrapped, data); + } + + pub const LengthWriter = struct { + index: usize, + context: WrappedWriter, + + pub fn write(this: LengthWriter) AnyPostgresError!void { + try this.context.pwrite(&Int32(this.context.offset() - this.index), this.index); + } + + pub fn writeExcludingSelf(this: LengthWriter) AnyPostgresError!void { + try this.context.pwrite(&Int32(this.context.offset() -| (this.index + 4)), this.index); + } + }; + + pub inline fn length(this: @This()) AnyPostgresError!LengthWriter { + const i = this.offset(); + try this.int4(0); + return LengthWriter{ + .index = i, + .context = this, + }; + } + + pub inline fn offset(this: @This()) usize { + return offsetFn(this.wrapped); + } + + pub inline fn pwrite(this: @This(), data: []const u8, i: usize) AnyPostgresError!void { + try pwriteFn(this.wrapped, data, i); + } + + pub fn int4(this: @This(), value: PostgresInt32) !void { + try this.write(std.mem.asBytes(&@byteSwap(value))); + } + + pub fn int8(this: @This(), value: PostgresInt64) !void { + try this.write(std.mem.asBytes(&@byteSwap(value))); + } + + pub fn sint4(this: @This(), value: i32) !void { + try this.write(std.mem.asBytes(&@byteSwap(value))); + } + + pub fn @"f64"(this: @This(), value: f64) !void { + try this.write(std.mem.asBytes(&@byteSwap(@as(u64, @bitCast(value))))); + } + + pub fn @"f32"(this: @This(), value: f32) !void { + try this.write(std.mem.asBytes(&@byteSwap(@as(u32, @bitCast(value))))); + } + + pub fn short(this: @This(), value: anytype) !void { + try this.write(std.mem.asBytes(&@byteSwap(@as(u16, @intCast(value))))); + } + + pub fn string(this: @This(), value: []const u8) !void { + try this.write(value); + if (value.len == 0 or value[value.len - 1] != 0) + try this.write(&[_]u8{0}); + } + + pub fn bytes(this: @This(), value: []const u8) !void { + try this.write(value); + if (value.len == 0 or value[value.len - 1] != 0) + try this.write(&[_]u8{0}); + } + + pub fn @"bool"(this: @This(), value: bool) !void { + try this.write(if (value) "t" else "f"); + } + + pub fn @"null"(this: @This()) !void { + try this.int4(std.math.maxInt(PostgresInt32)); + } + + pub fn String(this: @This(), value: bun.String) !void { + if (value.isEmpty()) { + try this.write(&[_]u8{0}); + return; + } + + var sliced = value.toUTF8(bun.default_allocator); + defer sliced.deinit(); + const slice = sliced.slice(); + + try this.write(slice); + if (slice.len == 0 or slice[slice.len - 1] != 0) + try this.write(&[_]u8{0}); + } + }; +} + +pub fn NewWriter(comptime Context: type) type { + return NewWriterWrap(Context, Context.offset, Context.write, Context.pwrite); +} + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const int_types = @import("../types/int_types.zig"); +const Int32 = int_types.Int32; +const PostgresInt32 = int_types.PostgresInt32; +const PostgresInt64 = int_types.PostgresInt64; diff --git a/src/sql/postgres/protocol/NoticeResponse.zig b/src/sql/postgres/protocol/NoticeResponse.zig new file mode 100644 index 0000000000..1e84eef072 --- /dev/null +++ b/src/sql/postgres/protocol/NoticeResponse.zig @@ -0,0 +1,53 @@ +messages: std.ArrayListUnmanaged(FieldMessage) = .{}, +pub fn deinit(this: *NoticeResponse) void { + for (this.messages.items) |*message| { + message.deinit(); + } + this.messages.deinit(bun.default_allocator); +} +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + var remaining_bytes = try reader.length(); + remaining_bytes -|= 4; + + if (remaining_bytes > 0) { + this.* = .{ + .messages = try FieldMessage.decodeList(Container, reader), + }; + } +} +pub const decode = DecoderWrap(NoticeResponse, decodeInternal).decode; + +pub fn toJS(this: NoticeResponse, globalObject: *JSC.JSGlobalObject) JSValue { + var b = bun.StringBuilder{}; + defer b.deinit(bun.default_allocator); + + for (this.messages.items) |msg| { + b.cap += switch (msg) { + inline else => |m| m.utf8ByteLength(), + } + 1; + } + b.allocate(bun.default_allocator) catch {}; + + for (this.messages.items) |msg| { + var str = switch (msg) { + inline else => |m| m.toUTF8(bun.default_allocator), + }; + defer str.deinit(); + _ = b.append(str.slice()); + _ = b.append("\n"); + } + + return JSC.ZigString.init(b.allocatedSlice()[0..b.len]).toJS(globalObject); +} + +// @sortImports + +const NoticeResponse = @This(); +const bun = @import("bun"); +const std = @import("std"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const FieldMessage = @import("./FieldMessage.zig").FieldMessage; +const NewReader = @import("./NewReader.zig").NewReader; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/protocol/NotificationResponse.zig b/src/sql/postgres/protocol/NotificationResponse.zig new file mode 100644 index 0000000000..936490602d --- /dev/null +++ b/src/sql/postgres/protocol/NotificationResponse.zig @@ -0,0 +1,31 @@ +pid: int4 = 0, +channel: bun.ByteList = .{}, +payload: bun.ByteList = .{}, + +pub fn deinit(this: *@This()) void { + this.channel.deinitWithAllocator(bun.default_allocator); + this.payload.deinitWithAllocator(bun.default_allocator); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const length = try reader.length(); + bun.assert(length >= 4); + + this.* = .{ + .pid = try reader.int4(), + .channel = (try reader.readZ()).toOwned(), + .payload = (try reader.readZ()).toOwned(), + }; +} + +pub const decode = DecoderWrap(NotificationResponse, decodeInternal).decode; + +// @sortImports + +const NotificationResponse = @This(); +const bun = @import("bun"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; + +const types = @import("../PostgresTypes.zig"); +const int4 = types.int4; diff --git a/src/sql/postgres/protocol/ParameterDescription.zig b/src/sql/postgres/protocol/ParameterDescription.zig new file mode 100644 index 0000000000..8be2737fd6 --- /dev/null +++ b/src/sql/postgres/protocol/ParameterDescription.zig @@ -0,0 +1,37 @@ +parameters: []int4 = &[_]int4{}, + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + var remaining_bytes = try reader.length(); + remaining_bytes -|= 4; + + const count = try reader.short(); + const parameters = try bun.default_allocator.alloc(int4, @intCast(@max(count, 0))); + + var data = try reader.read(@as(usize, @intCast(@max(count, 0))) * @sizeOf((int4))); + defer data.deinit(); + const input_params: []align(1) const int4 = toInt32Slice(int4, data.slice()); + for (input_params, parameters) |src, *dest| { + dest.* = @byteSwap(src); + } + + this.* = .{ + .parameters = parameters, + }; +} + +pub const decode = DecoderWrap(ParameterDescription, decodeInternal).decode; + +// workaround for zig compiler TODO +fn toInt32Slice(comptime Int: type, slice: []const u8) []align(1) const Int { + return @as([*]align(1) const Int, @ptrCast(slice.ptr))[0 .. slice.len / @sizeOf((Int))]; +} + +// @sortImports + +const ParameterDescription = @This(); +const bun = @import("bun"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; + +const types = @import("../PostgresTypes.zig"); +const int4 = types.int4; diff --git a/src/sql/postgres/protocol/ParameterStatus.zig b/src/sql/postgres/protocol/ParameterStatus.zig new file mode 100644 index 0000000000..9575c0302d --- /dev/null +++ b/src/sql/postgres/protocol/ParameterStatus.zig @@ -0,0 +1,27 @@ +name: Data = .{ .empty = {} }, +value: Data = .{ .empty = {} }, + +pub fn deinit(this: *@This()) void { + this.name.deinit(); + this.value.deinit(); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const length = try reader.length(); + bun.assert(length >= 4); + + this.* = .{ + .name = try reader.readZ(), + .value = try reader.readZ(), + }; +} + +pub const decode = DecoderWrap(ParameterStatus, decodeInternal).decode; + +// @sortImports + +const ParameterStatus = @This(); +const bun = @import("bun"); +const Data = @import("../Data.zig").Data; +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/Parse.zig b/src/sql/postgres/protocol/Parse.zig new file mode 100644 index 0000000000..af14f63461 --- /dev/null +++ b/src/sql/postgres/protocol/Parse.zig @@ -0,0 +1,43 @@ +name: []const u8 = "", +query: []const u8 = "", +params: []const int4 = &.{}, + +pub fn deinit(this: *Parse) void { + _ = this; +} + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const parameters = this.params; + const count: usize = @sizeOf((u32)) + @sizeOf(u16) + (parameters.len * @sizeOf(u32)) + @max(zCount(this.name), 1) + @max(zCount(this.query), 1); + const header = [_]u8{ + 'P', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(this.name); + try writer.string(this.query); + try writer.short(parameters.len); + for (parameters) |parameter| { + try writer.int4(parameter); + } +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const Parse = @This(); +const std = @import("std"); +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; + +const types = @import("../PostgresTypes.zig"); +const Int32 = types.Int32; +const int4 = types.int4; + +const zHelpers = @import("./zHelpers.zig"); +const zCount = zHelpers.zCount; diff --git a/src/sql/postgres/protocol/PasswordMessage.zig b/src/sql/postgres/protocol/PasswordMessage.zig new file mode 100644 index 0000000000..222e37b7da --- /dev/null +++ b/src/sql/postgres/protocol/PasswordMessage.zig @@ -0,0 +1,31 @@ +password: Data = .{ .empty = {} }, + +pub fn deinit(this: *PasswordMessage) void { + this.password.deinit(); +} + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const password = this.password.slice(); + const count: usize = @sizeOf((u32)) + password.len + 1; + const header = [_]u8{ + 'p', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(password); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const PasswordMessage = @This(); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const Int32 = @import("../types/int_types.zig").Int32; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; diff --git a/src/sql/postgres/protocol/PortalOrPreparedStatement.zig b/src/sql/postgres/protocol/PortalOrPreparedStatement.zig new file mode 100644 index 0000000000..575f5a07bd --- /dev/null +++ b/src/sql/postgres/protocol/PortalOrPreparedStatement.zig @@ -0,0 +1,18 @@ +pub const PortalOrPreparedStatement = union(enum) { + portal: []const u8, + prepared_statement: []const u8, + + pub fn slice(this: @This()) []const u8 { + return switch (this) { + .portal => this.portal, + .prepared_statement => this.prepared_statement, + }; + } + + pub fn tag(this: @This()) u8 { + return switch (this) { + .portal => 'P', + .prepared_statement => 'S', + }; + } +}; diff --git a/src/sql/postgres/protocol/ReadyForQuery.zig b/src/sql/postgres/protocol/ReadyForQuery.zig new file mode 100644 index 0000000000..baee6bea3b --- /dev/null +++ b/src/sql/postgres/protocol/ReadyForQuery.zig @@ -0,0 +1,18 @@ +const ReadyForQuery = @This(); +status: TransactionStatusIndicator = .I, +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + const length = try reader.length(); + bun.assert(length >= 4); + + const status = try reader.int(u8); + this.* = .{ + .status = @enumFromInt(status), + }; +} + +pub const decode = DecoderWrap(ReadyForQuery, decodeInternal).decode; + +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; +const TransactionStatusIndicator = @import("./TransactionStatusIndicator.zig").TransactionStatusIndicator; +const bun = @import("bun"); diff --git a/src/sql/postgres/protocol/RowDescription.zig b/src/sql/postgres/protocol/RowDescription.zig new file mode 100644 index 0000000000..e3068d4aee --- /dev/null +++ b/src/sql/postgres/protocol/RowDescription.zig @@ -0,0 +1,44 @@ +fields: []FieldDescription = &[_]FieldDescription{}, +pub fn deinit(this: *@This()) void { + for (this.fields) |*field| { + field.deinit(); + } + + bun.default_allocator.free(this.fields); +} + +pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void { + var remaining_bytes = try reader.length(); + remaining_bytes -|= 4; + + const field_count: usize = @intCast(@max(try reader.short(), 0)); + var fields = try bun.default_allocator.alloc( + FieldDescription, + field_count, + ); + var remaining = fields; + errdefer { + for (fields[0 .. field_count - remaining.len]) |*field| { + field.deinit(); + } + + bun.default_allocator.free(fields); + } + while (remaining.len > 0) { + try remaining[0].decodeInternal(Container, reader); + remaining = remaining[1..]; + } + this.* = .{ + .fields = fields, + }; +} + +pub const decode = DecoderWrap(RowDescription, decodeInternal).decode; + +// @sortImports + +const FieldDescription = @import("./FieldDescription.zig"); +const RowDescription = @This(); +const bun = @import("bun"); +const DecoderWrap = @import("./DecoderWrap.zig").DecoderWrap; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/SASLInitialResponse.zig b/src/sql/postgres/protocol/SASLInitialResponse.zig new file mode 100644 index 0000000000..8c5ee5cf14 --- /dev/null +++ b/src/sql/postgres/protocol/SASLInitialResponse.zig @@ -0,0 +1,36 @@ +mechanism: Data = .{ .empty = {} }, +data: Data = .{ .empty = {} }, + +pub fn deinit(this: *SASLInitialResponse) void { + this.mechanism.deinit(); + this.data.deinit(); +} + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const mechanism = this.mechanism.slice(); + const data = this.data.slice(); + const count: usize = @sizeOf(u32) + mechanism.len + 1 + data.len + @sizeOf(u32); + const header = [_]u8{ + 'p', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.string(mechanism); + try writer.int4(@truncate(data.len)); + try writer.write(data); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const SASLInitialResponse = @This(); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const Int32 = @import("../types/int_types.zig").Int32; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; diff --git a/src/sql/postgres/protocol/SASLResponse.zig b/src/sql/postgres/protocol/SASLResponse.zig new file mode 100644 index 0000000000..314fabd9e2 --- /dev/null +++ b/src/sql/postgres/protocol/SASLResponse.zig @@ -0,0 +1,31 @@ +data: Data = .{ .empty = {} }, + +pub fn deinit(this: *SASLResponse) void { + this.data.deinit(); +} + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const data = this.data.slice(); + const count: usize = @sizeOf(u32) + data.len; + const header = [_]u8{ + 'p', + } ++ toBytes(Int32(count)); + try writer.write(&header); + try writer.write(data); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const SASLResponse = @This(); +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const Int32 = @import("../types/int_types.zig").Int32; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const toBytes = std.mem.toBytes; diff --git a/src/sql/postgres/protocol/StackReader.zig b/src/sql/postgres/protocol/StackReader.zig new file mode 100644 index 0000000000..85fb93b5a9 --- /dev/null +++ b/src/sql/postgres/protocol/StackReader.zig @@ -0,0 +1,66 @@ +buffer: []const u8 = "", +offset: *usize, +message_start: *usize, + +pub fn markMessageStart(this: @This()) void { + this.message_start.* = this.offset.*; +} + +pub fn ensureLength(this: @This(), length: usize) bool { + return this.buffer.len >= (this.offset.* + length); +} + +pub fn init(buffer: []const u8, offset: *usize, message_start: *usize) NewReader(StackReader) { + return .{ + .wrapped = .{ + .buffer = buffer, + .offset = offset, + .message_start = message_start, + }, + }; +} + +pub fn peek(this: StackReader) []const u8 { + return this.buffer[this.offset.*..]; +} +pub fn skip(this: StackReader, count: usize) void { + if (this.offset.* + count > this.buffer.len) { + this.offset.* = this.buffer.len; + return; + } + + this.offset.* += count; +} +pub fn ensureCapacity(this: StackReader, count: usize) bool { + return this.buffer.len >= (this.offset.* + count); +} +pub fn read(this: StackReader, count: usize) AnyPostgresError!Data { + const offset = this.offset.*; + if (!this.ensureCapacity(count)) { + return error.ShortRead; + } + + this.skip(count); + return Data{ + .temporary = this.buffer[offset..this.offset.*], + }; +} +pub fn readZ(this: StackReader) AnyPostgresError!Data { + const remaining = this.peek(); + if (bun.strings.indexOfChar(remaining, 0)) |zero| { + this.skip(zero + 1); + return Data{ + .temporary = remaining[0..zero], + }; + } + + return error.ShortRead; +} + +// @sortImports + +const StackReader = @This(); +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const Data = @import("../Data.zig").Data; +const NewReader = @import("./NewReader.zig").NewReader; diff --git a/src/sql/postgres/protocol/StartupMessage.zig b/src/sql/postgres/protocol/StartupMessage.zig new file mode 100644 index 0000000000..d52f65a878 --- /dev/null +++ b/src/sql/postgres/protocol/StartupMessage.zig @@ -0,0 +1,52 @@ +user: Data, +database: Data, +options: Data = Data{ .empty = {} }, + +pub fn writeInternal( + this: *const @This(), + comptime Context: type, + writer: NewWriter(Context), +) !void { + const user = this.user.slice(); + const database = this.database.slice(); + const options = this.options.slice(); + const count: usize = @sizeOf((int4)) + @sizeOf((int4)) + zFieldCount("user", user) + zFieldCount("database", database) + zFieldCount("client_encoding", "UTF8") + options.len + 1; + + const header = toBytes(Int32(@as(u32, @truncate(count)))); + try writer.write(&header); + try writer.int4(196608); + + try writer.string("user"); + if (user.len > 0) + try writer.string(user); + + try writer.string("database"); + + if (database.len == 0) { + // The database to connect to. Defaults to the user name. + try writer.string(user); + } else { + try writer.string(database); + } + try writer.string("client_encoding"); + try writer.string("UTF8"); + if (options.len > 0) { + try writer.write(options); + } + try writer.write(&[_]u8{0}); +} + +pub const write = WriteWrap(@This(), writeInternal).write; + +// @sortImports + +const std = @import("std"); +const Data = @import("../Data.zig").Data; +const NewWriter = @import("./NewWriter.zig").NewWriter; +const WriteWrap = @import("./WriteWrap.zig").WriteWrap; +const zFieldCount = @import("./zHelpers.zig").zFieldCount; +const toBytes = std.mem.toBytes; + +const int_types = @import("../types/int_types.zig"); +const Int32 = int_types.Int32; +const int4 = int_types.int4; diff --git a/src/sql/postgres/protocol/TransactionStatusIndicator.zig b/src/sql/postgres/protocol/TransactionStatusIndicator.zig new file mode 100644 index 0000000000..9650d394f1 --- /dev/null +++ b/src/sql/postgres/protocol/TransactionStatusIndicator.zig @@ -0,0 +1,12 @@ +pub const TransactionStatusIndicator = enum(u8) { + /// if idle (not in a transaction block) + I = 'I', + + /// if in a transaction block + T = 'T', + + /// if in a failed transaction block + E = 'E', + + _, +}; diff --git a/src/sql/postgres/protocol/WriteWrap.zig b/src/sql/postgres/protocol/WriteWrap.zig new file mode 100644 index 0000000000..0fc4470b69 --- /dev/null +++ b/src/sql/postgres/protocol/WriteWrap.zig @@ -0,0 +1,14 @@ +pub fn WriteWrap(comptime Container: type, comptime writeFn: anytype) type { + return struct { + pub fn write(this: *Container, context: anytype) AnyPostgresError!void { + const Context = @TypeOf(context); + try writeFn(this, Context, NewWriter(Context){ .wrapped = context }); + } + }; +} + +// @sortImports + +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const NewWriter = @import("./NewWriter.zig").NewWriter; diff --git a/src/sql/postgres/protocol/zHelpers.zig b/src/sql/postgres/protocol/zHelpers.zig new file mode 100644 index 0000000000..9e28f9d0d1 --- /dev/null +++ b/src/sql/postgres/protocol/zHelpers.zig @@ -0,0 +1,11 @@ +pub fn zCount(slice: []const u8) usize { + return if (slice.len > 0) slice.len + 1 else 0; +} + +pub fn zFieldCount(prefix: []const u8, slice: []const u8) usize { + if (slice.len > 0) { + return zCount(prefix) + zCount(slice); + } + + return zCount(prefix); +} diff --git a/src/sql/postgres/types/PostgresString.zig b/src/sql/postgres/types/PostgresString.zig new file mode 100644 index 0000000000..f2e4cb4292 --- /dev/null +++ b/src/sql/postgres/types/PostgresString.zig @@ -0,0 +1,52 @@ +pub const to = 25; +pub const from = [_]short{1002}; + +pub fn toJSWithType( + globalThis: *JSC.JSGlobalObject, + comptime Type: type, + value: Type, +) AnyPostgresError!JSValue { + switch (comptime Type) { + [:0]u8, []u8, []const u8, [:0]const u8 => { + var str = bun.String.fromUTF8(value); + defer str.deinit(); + return str.toJS(globalThis); + }, + + bun.String => { + return value.toJS(globalThis); + }, + + *Data => { + var str = bun.String.fromUTF8(value.slice()); + defer str.deinit(); + defer value.deinit(); + return str.toJS(globalThis); + }, + + else => { + @compileError("unsupported type " ++ @typeName(Type)); + }, + } +} + +pub fn toJS( + globalThis: *JSC.JSGlobalObject, + value: anytype, +) !JSValue { + var str = try toJSWithType(globalThis, @TypeOf(value), value); + defer str.deinit(); + return str.toJS(globalThis); +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const Data = @import("../Data.zig").Data; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/postgres_types.zig b/src/sql/postgres/types/Tag.zig similarity index 72% rename from src/sql/postgres/postgres_types.zig rename to src/sql/postgres/types/Tag.zig index 4119ad23da..7c77c5a390 100644 --- a/src/sql/postgres/postgres_types.zig +++ b/src/sql/postgres/types/Tag.zig @@ -1,14 +1,3 @@ -const std = @import("std"); -const bun = @import("bun"); -const postgres = bun.api.Postgres; -const Data = postgres.Data; -const String = bun.String; -const JSValue = JSC.JSValue; -const JSC = bun.JSC; -const short = postgres.short; -const int4 = postgres.int4; -const AnyPostgresError = postgres.AnyPostgresError; - // select b.typname, b.oid, b.typarray // from pg_catalog.pg_type a // left join pg_catalog.pg_type b on b.oid = a.typelem @@ -402,153 +391,21 @@ pub const Tag = enum(short) { } }; -pub const string = struct { - pub const to = 25; - pub const from = [_]short{1002}; +const @"bool" = @import("./bool.zig"); - pub fn toJSWithType( - globalThis: *JSC.JSGlobalObject, - comptime Type: type, - value: Type, - ) AnyPostgresError!JSValue { - switch (comptime Type) { - [:0]u8, []u8, []const u8, [:0]const u8 => { - var str = String.fromUTF8(value); - defer str.deinit(); - return str.toJS(globalThis); - }, +// @sortImports - bun.String => { - return value.toJS(globalThis); - }, +const bun = @import("bun"); +const bytea = @import("./bytea.zig"); +const date = @import("./date.zig"); +const json = @import("./json.zig"); +const numeric = @import("./numeric.zig"); +const std = @import("std"); +const string = @import("./PostgresString.zig"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; - *Data => { - var str = String.fromUTF8(value.slice()); - defer str.deinit(); - defer value.deinit(); - return str.toJS(globalThis); - }, +const int_types = @import("./int_types.zig"); +const short = int_types.short; - else => { - @compileError("unsupported type " ++ @typeName(Type)); - }, - } - } - - pub fn toJS( - globalThis: *JSC.JSGlobalObject, - value: anytype, - ) !JSValue { - var str = try toJSWithType(globalThis, @TypeOf(value), value); - defer str.deinit(); - return str.toJS(globalThis); - } -}; - -pub const numeric = struct { - pub const to = 0; - pub const from = [_]short{ 21, 23, 26, 700, 701 }; - - pub fn toJS( - _: *JSC.JSGlobalObject, - value: anytype, - ) AnyPostgresError!JSValue { - return JSValue.jsNumber(value); - } -}; - -pub const json = struct { - pub const to = 114; - pub const from = [_]short{ 114, 3802 }; - - pub fn toJS( - globalObject: *JSC.JSGlobalObject, - value: *Data, - ) AnyPostgresError!JSValue { - defer value.deinit(); - var str = bun.String.fromUTF8(value.slice()); - defer str.deref(); - const parse_result = JSValue.parse(str.toJS(globalObject), globalObject); - if (parse_result.AnyPostgresError()) { - return globalObject.throwValue(parse_result); - } - - return parse_result; - } -}; - -pub const @"bool" = struct { - pub const to = 16; - pub const from = [_]short{16}; - - pub fn toJS( - _: *JSC.JSGlobalObject, - value: bool, - ) AnyPostgresError!JSValue { - return JSValue.jsBoolean(value); - } -}; - -pub const date = struct { - pub const to = 1184; - pub const from = [_]short{ 1082, 1114, 1184 }; - - // Postgres stores timestamp and timestampz as microseconds since 2000-01-01 - // This is a signed 64-bit integer. - const POSTGRES_EPOCH_DATE = 946684800000; - - pub fn fromBinary(bytes: []const u8) f64 { - const microseconds = std.mem.readInt(i64, bytes[0..8], .big); - const double_microseconds: f64 = @floatFromInt(microseconds); - return (double_microseconds / std.time.us_per_ms) + POSTGRES_EPOCH_DATE; - } - - pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSValue) i64 { - const double_value = if (value.isDate()) - value.getUnixTimestamp() - else if (value.isNumber()) - value.asNumber() - else if (value.isString()) brk: { - var str = value.toBunString(globalObject) catch @panic("unreachable"); - defer str.deref(); - break :brk str.parseDate(globalObject); - } else return 0; - - const unix_timestamp: i64 = @intFromFloat(double_value); - return (unix_timestamp - POSTGRES_EPOCH_DATE) * std.time.us_per_ms; - } - - pub fn toJS( - globalObject: *JSC.JSGlobalObject, - value: anytype, - ) JSValue { - switch (@TypeOf(value)) { - i64 => { - // Convert from Postgres timestamp (μs since 2000-01-01) to Unix timestamp (ms) - const ms = @divFloor(value, std.time.us_per_ms) + POSTGRES_EPOCH_DATE; - return JSValue.fromDateNumber(globalObject, @floatFromInt(ms)); - }, - *Data => { - defer value.deinit(); - return JSValue.fromDateString(globalObject, value.sliceZ().ptr); - }, - else => @compileError("unsupported type " ++ @typeName(@TypeOf(value))), - } - } -}; - -pub const bytea = struct { - pub const to = 17; - pub const from = [_]short{17}; - - pub fn toJS( - globalObject: *JSC.JSGlobalObject, - value: *Data, - ) AnyPostgresError!JSValue { - defer value.deinit(); - - // var slice = value.slice()[@min(1, value.len)..]; - // _ = slice; - return JSValue.createBuffer(globalObject, value.slice(), null); - } -}; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/types/bool.zig b/src/sql/postgres/types/bool.zig new file mode 100644 index 0000000000..0a00d07084 --- /dev/null +++ b/src/sql/postgres/types/bool.zig @@ -0,0 +1,20 @@ +pub const to = 16; +pub const from = [_]short{16}; + +pub fn toJS( + _: *JSC.JSGlobalObject, + value: bool, +) AnyPostgresError!JSValue { + return JSValue.jsBoolean(value); +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/types/bytea.zig b/src/sql/postgres/types/bytea.zig new file mode 100644 index 0000000000..dec468e524 --- /dev/null +++ b/src/sql/postgres/types/bytea.zig @@ -0,0 +1,25 @@ +pub const to = 17; +pub const from = [_]short{17}; + +pub fn toJS( + globalObject: *JSC.JSGlobalObject, + value: *Data, +) AnyPostgresError!JSValue { + defer value.deinit(); + + // var slice = value.slice()[@min(1, value.len)..]; + // _ = slice; + return JSValue.createBuffer(globalObject, value.slice(), null); +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const Data = @import("../Data.zig").Data; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/types/date.zig b/src/sql/postgres/types/date.zig new file mode 100644 index 0000000000..cdec908240 --- /dev/null +++ b/src/sql/postgres/types/date.zig @@ -0,0 +1,57 @@ +pub const to = 1184; +pub const from = [_]short{ 1082, 1114, 1184 }; + +// Postgres stores timestamp and timestampz as microseconds since 2000-01-01 +// This is a signed 64-bit integer. +const POSTGRES_EPOCH_DATE = 946684800000; + +pub fn fromBinary(bytes: []const u8) f64 { + const microseconds = std.mem.readInt(i64, bytes[0..8], .big); + const double_microseconds: f64 = @floatFromInt(microseconds); + return (double_microseconds / std.time.us_per_ms) + POSTGRES_EPOCH_DATE; +} + +pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSValue) i64 { + const double_value = if (value.isDate()) + value.getUnixTimestamp() + else if (value.isNumber()) + value.asNumber() + else if (value.isString()) brk: { + var str = value.toBunString(globalObject) catch @panic("unreachable"); + defer str.deref(); + break :brk str.parseDate(globalObject); + } else return 0; + + const unix_timestamp: i64 = @intFromFloat(double_value); + return (unix_timestamp - POSTGRES_EPOCH_DATE) * std.time.us_per_ms; +} + +pub fn toJS( + globalObject: *JSC.JSGlobalObject, + value: anytype, +) JSValue { + switch (@TypeOf(value)) { + i64 => { + // Convert from Postgres timestamp (μs since 2000-01-01) to Unix timestamp (ms) + const ms = @divFloor(value, std.time.us_per_ms) + POSTGRES_EPOCH_DATE; + return JSValue.fromDateNumber(globalObject, @floatFromInt(ms)); + }, + *Data => { + defer value.deinit(); + return JSValue.fromDateString(globalObject, value.sliceZ().ptr); + }, + else => @compileError("unsupported type " ++ @typeName(@TypeOf(value))), + } +} + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); +const Data = @import("../Data.zig").Data; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/types/int_types.zig b/src/sql/postgres/types/int_types.zig new file mode 100644 index 0000000000..5489a5309d --- /dev/null +++ b/src/sql/postgres/types/int_types.zig @@ -0,0 +1,10 @@ +pub const int4 = u32; +pub const PostgresInt32 = int4; +pub const int8 = i64; +pub const PostgresInt64 = int8; +pub const short = u16; +pub const PostgresShort = u16; + +pub fn Int32(value: anytype) [4]u8 { + return @bitCast(@byteSwap(@as(int4, @intCast(value)))); +} diff --git a/src/sql/postgres/types/json.zig b/src/sql/postgres/types/json.zig new file mode 100644 index 0000000000..0aaa37c173 --- /dev/null +++ b/src/sql/postgres/types/json.zig @@ -0,0 +1,29 @@ +pub const to = 114; +pub const from = [_]short{ 114, 3802 }; + +pub fn toJS( + globalObject: *JSC.JSGlobalObject, + value: *Data, +) AnyPostgresError!JSValue { + defer value.deinit(); + var str = bun.String.fromUTF8(value.slice()); + defer str.deref(); + const parse_result = JSValue.parse(str.toJS(globalObject), globalObject); + if (parse_result.AnyPostgresError()) { + return globalObject.throwValue(parse_result); + } + + return parse_result; +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; +const Data = @import("../Data.zig").Data; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; diff --git a/src/sql/postgres/types/numeric.zig b/src/sql/postgres/types/numeric.zig new file mode 100644 index 0000000000..01897396dc --- /dev/null +++ b/src/sql/postgres/types/numeric.zig @@ -0,0 +1,20 @@ +pub const to = 0; +pub const from = [_]short{ 21, 23, 26, 700, 701 }; + +pub fn toJS( + _: *JSC.JSGlobalObject, + value: anytype, +) AnyPostgresError!JSValue { + return JSValue.jsNumber(value); +} + +// @sortImports + +const bun = @import("bun"); +const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; + +const int_types = @import("./int_types.zig"); +const short = int_types.short; + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; From 75902e6a2131428a4d2a1696a29f408fc359f4d2 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 7 Jul 2025 19:24:32 -0700 Subject: [PATCH 132/147] fix(s3) fix loading http endpoint from env (#20876) --- src/env_loader.zig | 14 ++++++---- test/internal/ban-words.test.ts | 2 +- test/js/bun/s3/s3.test.ts | 45 +++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/env_loader.zig b/src/env_loader.zig index dd6cfbd78a..78ce9e1cfe 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -85,9 +85,7 @@ pub const Loader = struct { this.get("bamboo.buildKey")) != null; } - pub fn loadTracy(this: *const Loader) void { - _ = this; // autofix - } + pub fn loadTracy(_: *const Loader) void {} pub fn getS3Credentials(this: *Loader) s3.S3Credentials { if (this.aws_credentials) |credentials| { @@ -100,6 +98,7 @@ pub const Loader = struct { var endpoint: []const u8 = ""; var bucket: []const u8 = ""; var session_token: []const u8 = ""; + var insecure_http: bool = false; if (this.get("S3_ACCESS_KEY_ID")) |access_key| { accessKeyId = access_key; @@ -118,9 +117,13 @@ pub const Loader = struct { region = region_; } if (this.get("S3_ENDPOINT")) |endpoint_| { - endpoint = bun.URL.parse(endpoint_).hostWithPath(); + const url = bun.URL.parse(endpoint_); + endpoint = url.hostWithPath(); + insecure_http = url.isHTTP(); } else if (this.get("AWS_ENDPOINT")) |endpoint_| { - endpoint = bun.URL.parse(endpoint_).hostWithPath(); + const url = bun.URL.parse(endpoint_); + endpoint = url.hostWithPath(); + insecure_http = url.isHTTP(); } if (this.get("S3_BUCKET")) |bucket_| { bucket = bucket_; @@ -140,6 +143,7 @@ pub const Loader = struct { .endpoint = endpoint, .bucket = bucket, .sessionToken = session_token, + .insecure_http = insecure_http, }; return this.aws_credentials.?; diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 9f7d7d92cd..fccdb519b9 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -42,7 +42,7 @@ const words: Record ".stdFile()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 18 }, ".stdDir()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 49 }, ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 280 }, - "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 174 }, + "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 173 }, "global.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 28 }, "globalObject.hasException": { reason: "Incompatible with strict exception checks. Use a CatchScope instead.", limit: 47 }, diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index df6688b805..49e37a188a 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -3,7 +3,7 @@ import { S3Client, s3 as defaultS3, file, randomUUIDv7, which } from "bun"; import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import child_process from "child_process"; import { randomUUID } from "crypto"; -import { getSecret, tempDirWithFiles } from "harness"; +import { bunRun, getSecret, tempDirWithFiles } from "harness"; import path from "path"; const s3 = (...args) => defaultS3.file(...args); const S3 = (...args) => new S3Client(...args); @@ -21,8 +21,11 @@ function isDockerEnabled(): boolean { return false; } } - -const allCredentials = [ +type S3Credentials = S3Options & { + service: string; +}; +let minioCredentials: S3Credentials | undefined; +const allCredentials: S3Credentials[] = [ { accessKeyId: getSecret("S3_R2_ACCESS_KEY"), secretAccessKey: getSecret("S3_R2_SECRET_KEY"), @@ -73,13 +76,14 @@ if (isDockerEnabled()) { stdio: "ignore", }); - allCredentials.push({ + minioCredentials = { endpoint: "http://localhost:9000", // MinIO endpoint accessKeyId: "minioadmin", secretAccessKey: "minioadmin", bucket: "buntest", service: "MinIO" as string, - }); + }; + allCredentials.push(minioCredentials); } describe("Virtual Hosted-Style", () => { @@ -1332,3 +1336,34 @@ for (let credentials of allCredentials) { }); }); } +describe.skipIf(!minioCredentials)("http endpoint should work when using env variables", () => { + const testDir = tempDirWithFiles("minio-credential-test", { + "index.mjs": ` + import { s3, randomUUIDv7 } from "bun"; + import { expect } from "bun:test"; + const name = randomUUIDv7("hex") + ".txt"; + const s3file = s3.file(name); + await s3file.write("Hello Bun!"); + try { + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + process.stdout.write(text); + } finally { + await s3file.unlink(); + } + `, + }); + for (const endpoint of ["S3_ENDPOINT", "AWS_ENDPOINT"]) { + it(endpoint, async () => { + const { stdout, stderr } = await bunRun(path.join(testDir, "index.mjs"), { + // @ts-ignore + [endpoint]: minioCredentials!.endpoint as string, + "S3_BUCKET": minioCredentials!.bucket as string, + "S3_ACCESS_KEY_ID": minioCredentials!.accessKeyId as string, + "S3_SECRET_ACCESS_KEY": minioCredentials!.secretAccessKey as string, + }); + expect(stderr).toBe(""); + expect(stdout).toBe("Hello Bun!"); + }); + } +}); From 05e8a6dd4d2b07239e6957410a4cd0cb56cb44e6 Mon Sep 17 00:00:00 2001 From: Kai Tamkun <13513421+heimskr@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:29:53 -0700 Subject: [PATCH 133/147] Add support for vm.constants.DONT_CONTEXTIFY (#20088) --- src/bun.js/bindings/ErrorCode.cpp | 2 + src/bun.js/bindings/ErrorCode.ts | 1 + src/bun.js/bindings/NodeVM.cpp | 576 ++++++++++++++---- src/bun.js/bindings/NodeVM.h | 143 +++-- src/bun.js/bindings/NodeVMModule.cpp | 51 +- src/bun.js/bindings/NodeVMScript.cpp | 101 ++- src/bun.js/bindings/NodeVMScript.h | 3 +- src/bun.js/bindings/NodeVMScriptFetcher.h | 22 +- .../bindings/NodeVMSourceTextModule.cpp | 64 +- src/bun.js/bindings/NodeVMSyntheticModule.cpp | 10 +- src/bun.js/bindings/ZigGlobalObject.cpp | 18 +- src/bun.js/bindings/ZigGlobalObject.h | 6 +- .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + src/js/node/vm.ts | 26 +- .../test-vm-context-dont-contextify.js | 185 ++++++ .../parallel/test-vm-module-dynamic-import.js | 117 ++++ .../test-vm-module-dynamic-namespace.js | 26 + .../test-vm-module-referrer-realm.mjs | 70 +++ .../test-vm-no-dynamic-import-callback.js | 20 + .../sequential/test-vm-timeout-rethrow.js | 44 ++ test/no-validate-exceptions.txt | 1 + 22 files changed, 1212 insertions(+), 276 deletions(-) create mode 100644 test/js/node/test/parallel/test-vm-context-dont-contextify.js create mode 100644 test/js/node/test/parallel/test-vm-module-dynamic-import.js create mode 100644 test/js/node/test/parallel/test-vm-module-dynamic-namespace.js create mode 100644 test/js/node/test/parallel/test-vm-module-referrer-realm.mjs create mode 100644 test/js/node/test/parallel/test-vm-no-dynamic-import-callback.js create mode 100644 test/js/node/test/sequential/test-vm-timeout-rethrow.js diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 0a5ab24e12..bf8768a6af 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -2499,6 +2499,8 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_NOT_MODULE, "Provided module is not an instance of Module"_s)); case ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT, "Linked modules must use the same context"_s)); + case ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s)); default: { break; diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index ecd518b692..33dcec4582 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -296,6 +296,7 @@ const errors: ErrorCodeMapping = [ ["ERR_VM_MODULE_DIFFERENT_CONTEXT", Error], ["ERR_VM_MODULE_LINK_FAILURE", Error], ["ERR_VM_MODULE_CACHED_DATA_REJECTED", Error], + ["ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING", TypeError], ["HPE_INVALID_HEADER_TOKEN", Error], ["HPE_HEADER_OVERFLOW", Error], ]; diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index c0aef23763..8d91cc4a4b 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -55,16 +55,23 @@ #include "JavaScriptCore/FunctionCodeBlock.h" #include "JavaScriptCore/JIT.h" #include "JavaScriptCore/ProgramCodeBlock.h" +#include "JavaScriptCore/GlobalObjectMethodTable.h" #include "NodeVMScriptFetcher.h" #include "wtf/FileHandle.h" #include "../vm/SigintWatcher.h" +#include "JavaScriptCore/GetterSetter.h" + namespace Bun { using namespace WebCore; +static JSInternalPromise* moduleLoaderImportModuleInner(NodeVMGlobalObject* globalObject, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleName, JSC::JSValue parameters, const JSC::SourceOrigin& sourceOrigin); + namespace NodeVM { +static JSInternalPromise* importModuleInner(JSGlobalObject* globalObject, JSString* moduleName, JSValue parameters, const SourceOrigin& sourceOrigin, JSValue dynamicImportCallback, JSValue owner); + bool extractCachedData(JSValue cachedDataValue, WTF::Vector& outCachedData) { if (!cachedDataValue.isCell()) { @@ -126,6 +133,8 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c if (actuallyValid) { auto exception = error.toErrorObject(globalObject, sourceCode, -1); + RETURN_IF_EXCEPTION(throwScope, nullptr); + throwException(globalObject, throwScope, exception); return nullptr; } @@ -174,6 +183,7 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c { DeferGC deferGC(vm); programCodeBlock = ProgramCodeBlock::create(vm, programExecutable, unlinkedProgramCodeBlock, scope); + RETURN_IF_EXCEPTION(throwScope, nullptr); } if (!programCodeBlock || programCodeBlock->numberOfFunctionExprs() == 0) { @@ -193,6 +203,7 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c RefPtr producedBytecode = getBytecode(globalObject, programExecutable, sourceCode); if (producedBytecode) { JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, producedBytecode->span()); + RETURN_IF_EXCEPTION(throwScope, nullptr); function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedData"_s), buffer); function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedDataProduced"_s), jsBoolean(true)); } else { @@ -201,39 +212,84 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c } } else { function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedDataRejected"_s), jsBoolean(bytecodeAccepted == TriState::False)); + RETURN_IF_EXCEPTION(throwScope, nullptr); } return function; } -JSInternalPromise* importModule(JSGlobalObject* globalObject, JSString* moduleNameValue, JSValue parameters, const SourceOrigin& sourceOrigin) +JSInternalPromise* importModule(JSGlobalObject* globalObject, JSString* moduleName, JSValue parameters, const SourceOrigin& sourceOrigin) { - if (auto* fetcher = sourceOrigin.fetcher(); !fetcher || fetcher->fetcherType() != ScriptFetcher::Type::NodeVM) { - return nullptr; - } - VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - auto* fetcher = static_cast(sourceOrigin.fetcher()); - - JSValue dynamicImportCallback = fetcher->dynamicImportCallback(); - - if (!dynamicImportCallback || !dynamicImportCallback.isCallable()) { + if (auto* fetcher = sourceOrigin.fetcher(); !fetcher || fetcher->fetcherType() != ScriptFetcher::Type::NodeVM) { + if (!sourceOrigin.url().isEmpty()) { + if (auto* nodeVmGlobalObject = jsDynamicCast(globalObject)) { + if (nodeVmGlobalObject->dynamicImportCallback()) { + RELEASE_AND_RETURN(scope, moduleLoaderImportModuleInner(nodeVmGlobalObject, globalObject->moduleLoader(), moduleName, parameters, sourceOrigin)); + } + } + } return nullptr; } - JSFunction* owner = fetcher->owner(); + auto* fetcher = static_cast(sourceOrigin.fetcher()); + + if (fetcher->isUsingDefaultLoader()) { + return nullptr; + } + + JSValue dynamicImportCallback = fetcher->dynamicImportCallback(); + + if (isUseMainContextDefaultLoaderConstant(globalObject, dynamicImportCallback)) { + auto defer = fetcher->temporarilyUseDefaultLoader(); + Zig::GlobalObject* zigGlobalObject = defaultGlobalObject(globalObject); + RELEASE_AND_RETURN(scope, zigGlobalObject->moduleLoaderImportModule(zigGlobalObject, zigGlobalObject->moduleLoader(), moduleName, parameters, sourceOrigin)); + } else if (!dynamicImportCallback || !dynamicImportCallback.isCallable()) { + throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s)); + return nullptr; + } + + RELEASE_AND_RETURN(scope, importModuleInner(globalObject, moduleName, parameters, sourceOrigin, dynamicImportCallback, fetcher->owner())); +} + +static JSInternalPromise* importModuleInner(JSGlobalObject* globalObject, JSString* moduleName, JSValue parameters, const SourceOrigin& sourceOrigin, JSValue dynamicImportCallback, JSValue owner) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (parameters.isObject()) { + if (JSValue with = asObject(parameters)->getIfPropertyExists(globalObject, vm.propertyNames->with)) { + parameters = with; + } + RETURN_IF_EXCEPTION(scope, nullptr); + } MarkedArgumentBuffer args; - args.append(moduleNameValue); - args.append(owner ? owner : jsUndefined()); + args.append(moduleName); + if (owner) { + args.append(owner); + } else if (auto* nodeVmGlobalObject = jsDynamicCast(globalObject)) { + if (nodeVmGlobalObject->isNotContextified()) { + args.append(nodeVmGlobalObject->specialSandbox()); + } else { + args.append(nodeVmGlobalObject->contextifiedObject()); + } + } else { + args.append(jsUndefined()); + } args.append(parameters); JSValue result = AsyncContextFrame::call(globalObject, dynamicImportCallback, jsUndefined(), args); RETURN_IF_EXCEPTION(scope, nullptr); + if (result.isUndefinedOrNull()) { + throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_VM_MODULE_NOT_MODULE, "Provided module is not an instance of Module"_s)); + return nullptr; + } + if (auto* promise = jsDynamicCast(result)) { return promise; } @@ -267,7 +323,7 @@ JSInternalPromise* importModule(JSGlobalObject* globalObject, JSString* moduleNa promise = promise->then(globalObject, transformer, nullptr); RETURN_IF_EXCEPTION(scope, nullptr); - return promise; + RELEASE_AND_RETURN(scope, promise); } // Helper function to create an anonymous function expression with parameters @@ -368,9 +424,11 @@ JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::So std::span bytes = bytecode->span(); JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, bytes); - RETURN_IF_EXCEPTION(scope, {}); - ASSERT(buffer); + + if (!buffer) { + return throwVMError(globalObject, scope, "Failed to create buffer"_s); + } return JSValue::encode(buffer); } @@ -386,6 +444,7 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtrstack(); size_t stack_size = e_stack.size(); @@ -411,8 +470,12 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey) +std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer) { + if (importer) { + *importer = jsUndefined(); + } + outOptions = {}; // If options is provided, validate name and origin properties @@ -440,10 +503,19 @@ std::optional getNodeVMContextOptions(JSGlobalObject* globa } } - auto codeGenerationValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, codeGenerationKey)); + JSValue importModuleDynamicallyValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "importModuleDynamically"_s)); RETURN_IF_EXCEPTION(scope, {}); - if (codeGenerationValue) { + if (importModuleDynamicallyValue) { + if (importer && importModuleDynamicallyValue && (importModuleDynamicallyValue.isCallable() || isUseMainContextDefaultLoaderConstant(globalObject, importModuleDynamicallyValue))) { + *importer = importModuleDynamicallyValue; + } + } + + JSValue codeGenerationValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, codeGenerationKey)); + RETURN_IF_EXCEPTION(scope, {}); + + if (codeGenerationValue) { if (codeGenerationValue.isUndefined()) { return std::nullopt; } @@ -462,6 +534,7 @@ std::optional getNodeVMContextOptions(JSGlobalObject* globa } outOptions.allowStrings = allowStringsValue.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); } auto allowWasmValue = codeGenerationObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "wasm"_s)); @@ -472,6 +545,7 @@ std::optional getNodeVMContextOptions(JSGlobalObject* globa } outOptions.allowWasm = allowWasmValue.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); } } @@ -500,13 +574,28 @@ NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSV auto* zigGlobalObject = defaultGlobalObject(globalObject); JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); if (scopeValue.isUndefined()) { + if (auto* specialSandbox = jsDynamicCast(context)) { + return specialSandbox->parentGlobal(); + } + + if (auto* proxy = jsDynamicCast(context)) { + if (auto* nodeVmGlobalObject = jsDynamicCast(proxy->target())) { + return nodeVmGlobalObject; + } + } + if (canThrow) { INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); } return nullptr; } - NodeVMGlobalObject* nodeVmGlobalObject = jsDynamicCast(scopeValue); + auto* nodeVmGlobalObject = jsDynamicCast(scopeValue); + + if (!nodeVmGlobalObject) { + nodeVmGlobalObject = jsDynamicCast(context); + } + if (!nodeVmGlobalObject) { if (canThrow) { INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); @@ -525,12 +614,98 @@ JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, return {}; } +bool isContext(JSGlobalObject* globalObject, JSValue value) +{ + auto* zigGlobalObject = defaultGlobalObject(globalObject); + + if (zigGlobalObject->vmModuleContextMap()->has(asObject(value))) { + return true; + } + + if (value.inherits(NodeVMSpecialSandbox::info())) { + return true; + } + + if (auto* proxy = jsDynamicCast(value); proxy && proxy->target()) { + return proxy->target()->inherits(NodeVMGlobalObject::info()); + } + + return false; +} + +bool getContextArg(JSGlobalObject* globalObject, JSValue& contextArg) +{ + if (contextArg.isUndefined()) { + contextArg = JSC::constructEmptyObject(globalObject); + } else if (contextArg.isSymbol()) { + Zig::GlobalObject* zigGlobalObject = defaultGlobalObject(globalObject); + if (contextArg == zigGlobalObject->m_nodeVMDontContextify.get(zigGlobalObject)) { + contextArg = JSC::constructEmptyObject(globalObject); + return true; + } + } + + return false; +} + +bool isUseMainContextDefaultLoaderConstant(JSGlobalObject* globalObject, JSValue value) +{ + if (value.isSymbol()) { + Zig::GlobalObject* zigGlobalObject = defaultGlobalObject(globalObject); + if (value == zigGlobalObject->m_nodeVMUseMainContextDefaultLoader.get(zigGlobalObject)) { + return true; + } + } + + return false; +} + } // namespace NodeVM using namespace NodeVM; -NodeVMGlobalObject::NodeVMGlobalObject(JSC::VM& vm, JSC::Structure* structure) +template JSC::GCClient::IsoSubspace* NodeVMSpecialSandbox::subspaceFor(JSC::VM& vm) +{ + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForNodeVMSpecialSandbox.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNodeVMSpecialSandbox = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForNodeVMSpecialSandbox.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForNodeVMSpecialSandbox = std::forward(space); }); +} + +NodeVMSpecialSandbox* NodeVMSpecialSandbox::create(VM& vm, Structure* structure, NodeVMGlobalObject* globalObject) +{ + NodeVMSpecialSandbox* ptr = new (NotNull, allocateCell(vm)) NodeVMSpecialSandbox(vm, structure, globalObject); + ptr->finishCreation(vm); + return ptr; +} + +JSC::Structure* NodeVMSpecialSandbox::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +NodeVMSpecialSandbox::NodeVMSpecialSandbox(VM& vm, Structure* structure, NodeVMGlobalObject* globalObject) : Base(vm, structure) +{ + m_parentGlobal.set(vm, this, globalObject); +} + +void NodeVMSpecialSandbox::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +const JSC::ClassInfo NodeVMSpecialSandbox::s_info = { "NodeVMSpecialSandbox"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMSpecialSandbox) }; + +NodeVMGlobalObject::NodeVMGlobalObject(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions contextOptions, JSValue importer) + : Base(vm, structure, &globalObjectMethodTable()) + , m_dynamicImportCallback(vm, this, importer) + , m_contextOptions(contextOptions) { } @@ -547,10 +722,10 @@ template JSC::GCClient::IsoSubspace* NodeVMG [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForNodeVMGlobalObject; }); } -NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options) +NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options, JSValue importer) { - auto* cell = new (NotNull, JSC::allocateCell(vm)) NodeVMGlobalObject(vm, structure); - cell->finishCreation(vm, options); + auto* cell = new (NotNull, JSC::allocateCell(vm)) NodeVMGlobalObject(vm, structure, options, importer); + cell->finishCreation(vm); return cell; } @@ -560,11 +735,40 @@ Structure* NodeVMGlobalObject::createStructure(JSC::VM& vm, JSC::JSValue prototy return JSC::Structure::create(vm, nullptr, prototype, JSC::TypeInfo(JSC::GlobalObjectType, StructureFlags & ~IsImmutablePrototypeExoticObject), info()); } -void NodeVMGlobalObject::finishCreation(JSC::VM& vm, NodeVMContextOptions options) +const JSC::GlobalObjectMethodTable& NodeVMGlobalObject::globalObjectMethodTable() +{ + static const JSC::GlobalObjectMethodTable table { + &supportsRichSourceInfo, + &shouldInterruptScript, + &javaScriptRuntimeFlags, + nullptr, // queueTaskToEventLoop + nullptr, // shouldInterruptScriptBeforeTimeout, + &moduleLoaderImportModule, + nullptr, // moduleLoaderResolve + nullptr, // moduleLoaderFetch + nullptr, // moduleLoaderCreateImportMetaProperties + nullptr, // moduleLoaderEvaluate + nullptr, // promiseRejectionTracker + &reportUncaughtExceptionAtEventLoop, + ¤tScriptExecutionOwner, + &scriptExecutionStatus, + nullptr, // reportViolationForUnsafeEval + nullptr, // defaultLanguage + nullptr, // compileStreaming + nullptr, // instantiateStreaming + nullptr, + &codeForEval, + &canCompileStrings, + &trustedScriptStructure, + }; + return table; +} + +void NodeVMGlobalObject::finishCreation(JSC::VM& vm) { Base::finishCreation(vm); - setEvalEnabled(options.allowStrings, "Code generation from strings disallowed for this context"_s); - setWebAssemblyEnabled(options.allowWasm, "Wasm code generation disallowed by embedder"_s); + setEvalEnabled(m_contextOptions.allowStrings, "Code generation from strings disallowed for this context"_s); + setWebAssemblyEnabled(m_contextOptions.allowWasm, "Wasm code generation disallowed by embedder"_s); vm.ensureTerminationException(); } @@ -619,18 +823,27 @@ bool NodeVMGlobalObject::put(JSCell* cell, JSGlobalObject* globalObject, Propert bool isFunction = value.isCallable(); if (slot.isStrictMode() && !isDeclared && isContextualStore && !isFunction) { - return Base::put(cell, globalObject, propertyName, value, slot); + RELEASE_AND_RETURN(scope, Base::put(cell, globalObject, propertyName, value, slot)); } if (!isDeclared && value.isSymbol()) { - return Base::put(cell, globalObject, propertyName, value, slot); + RELEASE_AND_RETURN(scope, Base::put(cell, globalObject, propertyName, value, slot)); + } + + if (thisObject->m_contextOptions.notContextified) { + JSObject* specialSandbox = thisObject->specialSandbox(); + slot.setThisValue(specialSandbox); + RELEASE_AND_RETURN(scope, specialSandbox->putInline(globalObject, propertyName, value, slot)); } slot.setThisValue(sandbox); + bool result = sandbox->methodTable()->put(sandbox, globalObject, propertyName, value, slot); + RETURN_IF_EXCEPTION(scope, false); - if (!sandbox->methodTable()->put(sandbox, globalObject, propertyName, value, slot)) { + if (!result) { return false; } + RETURN_IF_EXCEPTION(scope, false); if (isDeclaredOnSandbox && getter.isAccessor() and (getter.attributes() & PropertyAttribute::DontEnum) == 0) { @@ -638,21 +851,54 @@ bool NodeVMGlobalObject::put(JSCell* cell, JSGlobalObject* globalObject, Propert } slot.setThisValue(thisValue); - - return Base::put(cell, globalObject, propertyName, value, slot); + RELEASE_AND_RETURN(scope, Base::put(cell, globalObject, propertyName, value, slot)); } // This is copy-pasted from JSC's ProxyObject.cpp static const ASCIILiteral s_proxyAlreadyRevokedErrorMessage { "Proxy has already been revoked. No more operations are allowed to be performed on it"_s }; +bool NodeVMSpecialSandbox::getOwnPropertySlot(JSObject* cell, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(cell); + NodeVMGlobalObject* parentGlobal = thisObject->parentGlobal(); + + if (propertyName.uid()->utf8() == "globalThis") [[unlikely]] { + slot.disableCaching(); + slot.setThisValue(thisObject); + slot.setValue(thisObject, slot.attributes(), thisObject); + return true; + } + + bool result = parentGlobal->getOwnPropertySlot(parentGlobal, globalObject, propertyName, slot); + RETURN_IF_EXCEPTION(scope, false); + + if (result) { + return true; + } + + RELEASE_AND_RETURN(scope, Base::getOwnPropertySlot(cell, globalObject, propertyName, slot)); +} + bool NodeVMGlobalObject::getOwnPropertySlot(JSObject* cell, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot) { VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* thisObject = jsCast(cell); - if (thisObject->m_sandbox) { - auto* contextifiedObject = thisObject->m_sandbox.get(); + + bool notContextified = thisObject->isNotContextified(); + + if (notContextified && propertyName.uid()->utf8() == "globalThis") [[unlikely]] { + slot.disableCaching(); + slot.setThisValue(thisObject); + slot.setValue(thisObject, slot.attributes(), thisObject->specialSandbox()); + return true; + } + + if (JSObject* contextifiedObject = thisObject->contextifiedObject()) { slot.setThisValue(contextifiedObject); // Unfortunately we must special case ProxyObjects. Why? // @@ -719,8 +965,12 @@ bool NodeVMGlobalObject::getOwnPropertySlot(JSObject* cell, JSGlobalObject* glob goto try_from_global; } - if (contextifiedObject->getPropertySlot(globalObject, propertyName, slot)) { - return true; + if (!notContextified) { + bool result = contextifiedObject->getPropertySlot(globalObject, propertyName, slot); + RETURN_IF_EXCEPTION(scope, false); + if (result) { + return true; + } } try_from_global: @@ -729,47 +979,61 @@ bool NodeVMGlobalObject::getOwnPropertySlot(JSObject* cell, JSGlobalObject* glob RETURN_IF_EXCEPTION(scope, false); } - return Base::getOwnPropertySlot(cell, globalObject, propertyName, slot); + bool result = Base::getOwnPropertySlot(cell, globalObject, propertyName, slot); + RETURN_IF_EXCEPTION(scope, false); + + if (result) { + return true; + } + + if (thisObject->m_contextOptions.notContextified) { + JSObject* specialSandbox = thisObject->specialSandbox(); + RELEASE_AND_RETURN(scope, JSObject::getOwnPropertySlot(specialSandbox, globalObject, propertyName, slot)); + } + + return false; } bool NodeVMGlobalObject::defineOwnProperty(JSObject* cell, JSGlobalObject* globalObject, PropertyName propertyName, const PropertyDescriptor& descriptor, bool shouldThrow) { - // if (!propertyName.isSymbol()) - // printf("defineOwnProperty called for %s\n", propertyName.publicName()->utf8().data()); - auto* thisObject = jsCast(cell); - if (!thisObject->m_sandbox) { - return Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow); - } - - auto* contextifiedObject = thisObject->m_sandbox.get(); VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsCast(cell); + if (!thisObject->m_sandbox) { + RELEASE_AND_RETURN(scope, Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow)); + } + + auto* contextifiedObject = thisObject->isNotContextified() ? thisObject->specialSandbox() : thisObject->m_sandbox.get(); + PropertySlot slot(globalObject, PropertySlot::InternalMethodType::GetOwnProperty, nullptr); bool isDeclaredOnGlobalProxy = globalObject->JSC::JSGlobalObject::getOwnPropertySlot(globalObject, globalObject, propertyName, slot); // If the property is set on the global as neither writable nor // configurable, don't change it on the global or sandbox. if (isDeclaredOnGlobalProxy && (slot.attributes() & PropertyAttribute::ReadOnly) != 0 && (slot.attributes() & PropertyAttribute::DontDelete) != 0) { - return Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow); + RELEASE_AND_RETURN(scope, Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow)); } if (descriptor.isAccessorDescriptor()) { - return contextifiedObject->defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow); + RELEASE_AND_RETURN(scope, JSObject::defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow)); } bool isDeclaredOnSandbox = contextifiedObject->getPropertySlot(globalObject, propertyName, slot); RETURN_IF_EXCEPTION(scope, false); if (isDeclaredOnSandbox && !isDeclaredOnGlobalProxy) { - return contextifiedObject->defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow); + RELEASE_AND_RETURN(scope, JSObject::defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow)); } - if (!contextifiedObject->defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow)) { + bool result = JSObject::defineOwnProperty(contextifiedObject, contextifiedObject->globalObject(), propertyName, descriptor, shouldThrow); + RETURN_IF_EXCEPTION(scope, false); + + if (!result) { return false; } - return Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow); + RELEASE_AND_RETURN(scope, Base::defineOwnProperty(cell, globalObject, propertyName, descriptor, shouldThrow)); } DEFINE_VISIT_CHILDREN(NodeVMGlobalObject); @@ -780,6 +1044,8 @@ void NodeVMGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(cell, visitor); auto* thisObject = jsCast(cell); visitor.append(thisObject->m_sandbox); + visitor.append(thisObject->m_specialSandbox); + visitor.append(thisObject->m_dynamicImportCallback); } JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -792,41 +1058,44 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject return ERR::INVALID_ARG_TYPE(scope, globalObject, "code"_s, "string"_s, code); JSValue contextArg = callFrame->argument(1); - if (contextArg.isUndefined()) { - contextArg = JSC::constructEmptyObject(globalObject); - } + bool notContextified = getContextArg(globalObject, contextArg); - if (!contextArg.isObject()) + if (!contextArg.isObject()) { return ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextArg); + } JSObject* sandbox = asObject(contextArg); JSValue contextOptionsArg = callFrame->argument(2); - NodeVMContextOptions contextOptions {}; - if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, contextOptionsArg, contextOptions, "contextCodeGeneration")) { + JSValue globalObjectDynamicImportCallback; + + if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, contextOptionsArg, contextOptions, "contextCodeGeneration", &globalObjectDynamicImportCallback)) { return *encodedException; } + contextOptions.notContextified = notContextified; + // Create context and run code auto* context = NodeVMGlobalObject::create(vm, defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), - contextOptions); + contextOptions, globalObjectDynamicImportCallback); context->setContextifiedObject(sandbox); JSValue optionsArg = callFrame->argument(2); + JSValue scriptDynamicImportCallback; ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); - } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + } else if (!options.fromJS(globalObject, vm, scope, optionsArg, &scriptDynamicImportCallback)) { RETURN_IF_EXCEPTION(scope, {}); } - RefPtr fetcher(NodeVMScriptFetcher::create(vm, options.importer)); + RefPtr fetcher(NodeVMScriptFetcher::create(vm, scriptDynamicImportCallback, jsUndefined())); SourceCode sourceCode( JSC::StringSourceProvider::create( @@ -862,19 +1131,21 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInThisContext, (JSGlobalObject * globalObjec return ERR::INVALID_ARG_TYPE(throwScope, globalObject, "code"_s, "string"_s, sourceStringValue); } - auto sourceString = sourceStringValue.toWTFString(globalObject); + String sourceString = sourceStringValue.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); + JSValue importer; + JSValue optionsArg = callFrame->argument(1); ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - } else if (!options.fromJS(globalObject, vm, throwScope, optionsArg)) { + } else if (!options.fromJS(globalObject, vm, throwScope, optionsArg, &importer)) { RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); } - RefPtr fetcher(NodeVMScriptFetcher::create(vm, options.importer)); + RefPtr fetcher(NodeVMScriptFetcher::create(vm, importer, jsUndefined())); SourceCode source( JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), @@ -927,7 +1198,9 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject // Get options argument JSValue optionsArg = callFrame->argument(2); CompileFunctionOptions options; - if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + JSValue importer; + + if (!options.fromJS(globalObject, vm, scope, optionsArg, &importer)) { RETURN_IF_EXCEPTION(scope, {}); options = {}; options.parsingContext = globalObject; @@ -949,7 +1222,7 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject // Add the function body constructFunctionArgs.append(jsString(vm, sourceString)); - RefPtr fetcher(NodeVMScriptFetcher::create(vm, options.importer)); + RefPtr fetcher(NodeVMScriptFetcher::create(vm, importer, jsUndefined())); // Create the source origin SourceOrigin sourceOrigin { WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher }; @@ -983,14 +1256,18 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject // Create the function using constructAnonymousFunction with the appropriate scope chain JSFunction* function = constructAnonymousFunction(globalObject, ArgList(constructFunctionArgs), sourceOrigin, WTFMove(options), JSC::SourceTaintedOrigin::Untainted, functionScope); - fetcher->owner(vm, function); - RETURN_IF_EXCEPTION(scope, {}); if (!function) { return throwVMError(globalObject, scope, "Failed to compile function"_s); } + fetcher->owner(vm, function); + + if (!function) { + return throwVMError(globalObject, scope, "Failed to compile function"_s); + } + return JSValue::encode(function); } @@ -999,31 +1276,16 @@ Structure* createNodeVMGlobalObjectStructure(JSC::VM& vm) return NodeVMGlobalObject::createStructure(vm, jsNull()); } -NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox) -{ - auto* targetContext = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), - NodeVMContextOptions {}); - - // Set sandbox as contextified object - targetContext->setContextifiedObject(sandbox); - - // Store context in WeakMap for isContext checks - auto* zigGlobalObject = defaultGlobalObject(globalObject); - zigGlobalObject->vmModuleContextMap()->set(vm, sandbox, targetContext); - - return targetContext; -} - JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + NodeVMContextOptions contextOptions {}; + JSValue contextArg = callFrame->argument(0); - if (contextArg.isUndefined()) { - contextArg = JSC::constructEmptyObject(globalObject); - } + bool notContextified = getContextArg(globalObject, contextArg); + RETURN_IF_EXCEPTION(scope, {}); if (!contextArg.isObject()) { return ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextArg); @@ -1036,25 +1298,48 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject * globalObject, return ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); } - NodeVMContextOptions contextOptions {}; + JSValue importer; - if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, optionsArg, contextOptions, "codeGeneration")) { + if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, optionsArg, contextOptions, "codeGeneration", &importer)) { return *encodedException; } + contextOptions.notContextified = notContextified; + JSObject* sandbox = asObject(contextArg); + if (isContext(globalObject, sandbox)) { + if (auto* proxy = jsDynamicCast(sandbox)) { + if (auto* targetContext = jsDynamicCast(proxy->target())) { + if (targetContext->isNotContextified()) { + return JSValue::encode(targetContext->specialSandbox()); + } + } + } + return JSValue::encode(sandbox); + } + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + auto* targetContext = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), - contextOptions); + zigGlobalObject->NodeVMGlobalObjectStructure(), + contextOptions, importer); + + RETURN_IF_EXCEPTION(scope, {}); // Set sandbox as contextified object targetContext->setContextifiedObject(sandbox); // Store context in WeakMap for isContext checks - auto* zigGlobalObject = defaultGlobalObject(globalObject); zigGlobalObject->vmModuleContextMap()->set(vm, sandbox, targetContext); + if (notContextified) { + auto* specialSandbox = NodeVMSpecialSandbox::create(vm, zigGlobalObject->NodeVMSpecialSandboxStructure(), targetContext); + RETURN_IF_EXCEPTION(scope, {}); + targetContext->setSpecialSandbox(specialSandbox); + return JSValue::encode(targetContext->specialSandbox()); + } + return JSValue::encode(sandbox); } @@ -1064,39 +1349,12 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_isContext, (JSGlobalObject * globalObject, Cal JSValue contextArg = callFrame->argument(0); VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - bool isContext; if (!contextArg || !contextArg.isObject()) { - isContext = false; return ERR::INVALID_ARG_TYPE(scope, globalObject, "object"_s, "object"_s, contextArg); - } else { - auto* zigGlobalObject = defaultGlobalObject(globalObject); - isContext = zigGlobalObject->vmModuleContextMap()->has(asObject(contextArg)); } - return JSValue::encode(jsBoolean(isContext)); + return JSValue::encode(jsBoolean(isContext(globalObject, contextArg))); } -// NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure) -// { -// auto* obj = new (NotNull, allocateCell(vm)) NodeVMGlobalObject(vm, structure); -// obj->finishCreation(vm); -// return obj; -// } - -// void NodeVMGlobalObject::finishCreation(VM& vm, JSObject* context) -// { -// Base::finishCreation(vm); -// // We don't need to store the context anymore since we use proxies -// } - -// DEFINE_VISIT_CHILDREN(NodeVMGlobalObject); - -// template -// void NodeVMGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) -// { -// Base::visitChildren(cell, visitor); -// // auto* thisObject = jsCast(cell); -// // visitor.append(thisObject->m_proxyTarget); -// } const ClassInfo NodeVMGlobalObject::s_info = { "NodeVMGlobalObject"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMGlobalObject) }; bool NodeVMGlobalObject::deleteProperty(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSC::DeletePropertySlot& slot) @@ -1118,19 +1376,55 @@ bool NodeVMGlobalObject::deleteProperty(JSCell* cell, JSGlobalObject* globalObje return Base::deleteProperty(cell, globalObject, propertyName, slot); } +static JSInternalPromise* moduleLoaderImportModuleInner(NodeVMGlobalObject* globalObject, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleName, JSC::JSValue parameters, const JSC::SourceOrigin& sourceOrigin) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* promise = JSInternalPromise::create(vm, globalObject->internalPromiseStructure()); + + if (sourceOrigin.fetcher() == nullptr && sourceOrigin.url().isEmpty()) { + if (globalObject->dynamicImportCallback().isCallable()) { + return NodeVM::importModuleInner(globalObject, moduleName, parameters, sourceOrigin, globalObject->dynamicImportCallback(), JSValue {}); + } + + promise->reject(globalObject, createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s)); + return promise; + } + + // Default behavior copied from JSModuleLoader::importModule + auto moduleNameString = moduleName->value(globalObject); + RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(globalObject, scope)); + + scope.release(); + promise->reject(globalObject, createError(globalObject, makeString("Could not import the module '"_s, moduleNameString.data, "'."_s))); + return promise; +} + +JSInternalPromise* NodeVMGlobalObject::moduleLoaderImportModule(JSGlobalObject* globalObject, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleName, JSC::JSValue parameters, const JSC::SourceOrigin& sourceOrigin) +{ + auto* nodeVmGlobalObject = static_cast(globalObject); + + if (JSInternalPromise* result = NodeVM::importModule(nodeVmGlobalObject, moduleName, parameters, sourceOrigin)) { + return result; + } + + return moduleLoaderImportModuleInner(nodeVmGlobalObject, moduleLoader, moduleName, parameters, sourceOrigin); +} + void NodeVMGlobalObject::getOwnPropertyNames(JSObject* cell, JSGlobalObject* globalObject, JSC::PropertyNameArray& propertyNames, JSC::DontEnumPropertiesMode mode) { auto* thisObject = jsCast(cell); - if (thisObject->m_sandbox) { - thisObject->m_sandbox->getOwnPropertyNames( - thisObject->m_sandbox.get(), - globalObject, - propertyNames, - mode); + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (thisObject->m_sandbox) [[likely]] { + thisObject->m_sandbox->getOwnPropertyNames(thisObject->m_sandbox.get(), globalObject, propertyNames, mode); + RETURN_IF_EXCEPTION(scope, ); } - Base::getOwnPropertyNames(cell, globalObject, propertyNames, mode); + RELEASE_AND_RETURN(scope, Base::getOwnPropertyNames(cell, globalObject, propertyNames, mode)); } JSC_DEFINE_HOST_FUNCTION(vmIsModuleNamespaceObject, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -1150,7 +1444,7 @@ JSC::JSValue createNodeVMBinding(Zig::GlobalObject* globalObject) defaultGlobalObject(globalObject)->NodeVMSourceTextModule(), 0); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "createContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "createContext"_s, vmModule_createContext, ImplementationVisibility::Public), 0); + JSC::JSFunction::create(vm, globalObject, 0, "createContext"_s, vmModule_createContext, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, vmModule_createContext), 0); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isContext"_s)), JSC::JSFunction::create(vm, globalObject, 0, "isContext"_s, vmModule_isContext, ImplementationVisibility::Public), 0); @@ -1190,11 +1484,24 @@ JSC::JSValue createNodeVMBinding(Zig::GlobalObject* globalObject) obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kSynthetic"_s)), JSC::jsNumber(static_cast(NodeVMModule::Type::Synthetic)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "DONT_CONTEXTIFY"_s)), + globalObject->m_nodeVMDontContextify.get(globalObject), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "USE_MAIN_CONTEXT_DEFAULT_LOADER"_s)), + globalObject->m_nodeVMUseMainContextDefaultLoader.get(globalObject), 0); return obj; } void configureNodeVM(JSC::VM& vm, Zig::GlobalObject* globalObject) { + globalObject->m_nodeVMDontContextify.initLater([](const LazyProperty::Initializer& init) { + init.set(JSC::Symbol::createWithDescription(init.vm, "vm_dont_contextify"_s)); + }); + globalObject->m_nodeVMUseMainContextDefaultLoader.initLater([](const LazyProperty::Initializer& init) { + init.set(JSC::Symbol::createWithDescription(init.vm, "vm_use_main_context_default_loader"_s)); + }); + globalObject->m_NodeVMScriptClassStructure.initLater( [](LazyClassStructure::Initializer& init) { auto prototype = NodeVMScript::createPrototype(init.vm, init.global); @@ -1238,6 +1545,11 @@ void configureNodeVM(JSC::VM& vm, Zig::GlobalObject* globalObject) [](const JSC::LazyProperty::Initializer& init) { init.set(createNodeVMGlobalObjectStructure(init.vm)); }); + + globalObject->m_cachedNodeVMSpecialSandboxStructure.initLater( + [](const JSC::LazyProperty::Initializer& init) { + init.set(NodeVMSpecialSandbox::createStructure(init.vm, init.owner, init.owner->objectPrototype())); // TODO(@heimskr): or maybe jsNull() for the prototype? + }); } BaseVMOptions::BaseVMOptions(String filename) @@ -1376,8 +1688,12 @@ bool BaseVMOptions::validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& return false; } -bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg, JSValue* importer) { + if (importer) { + *importer = jsUndefined(); + } + this->parsingContext = globalObject; bool any = BaseVMOptions::fromJS(globalObject, vm, scope, optionsArg); RETURN_IF_EXCEPTION(scope, false); @@ -1448,8 +1764,10 @@ bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& JSValue importModuleDynamicallyValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "importModuleDynamically"_s)); RETURN_IF_EXCEPTION(scope, {}); - if (importModuleDynamicallyValue && importModuleDynamicallyValue.isCallable()) { - this->importer = importModuleDynamicallyValue; + if (importModuleDynamicallyValue && (importModuleDynamicallyValue.isCallable() || isUseMainContextDefaultLoaderConstant(globalObject, importModuleDynamicallyValue))) { + if (importer) { + *importer = importModuleDynamicallyValue; + } any = true; } } diff --git a/src/bun.js/bindings/NodeVM.h b/src/bun.js/bindings/NodeVM.h index 43e85b7981..797af6aa4f 100644 --- a/src/bun.js/bindings/NodeVM.h +++ b/src/bun.js/bindings/NodeVM.h @@ -25,65 +25,19 @@ RefPtr getBytecode(JSGlobalObject* globalObject, JSC::Modul bool extractCachedData(JSValue cachedDataValue, WTF::Vector& outCachedData); String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset); JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::SourceCode& source); -NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox); bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope); -std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey); +std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer); NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value); // For vm.compileFunction we need to return an anonymous function expression. This code is adapted from/inspired by JSC::constructFunction, which is used for function declarations. JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, CompileFunctionOptions&& options, JSC::SourceTaintedOrigin sourceTaintOrigin, JSC::JSScope* scope); JSInternalPromise* importModule(JSGlobalObject* globalObject, JSString* moduleNameValue, JSValue parameters, const SourceOrigin& sourceOrigin); +bool isContext(JSC::JSGlobalObject* globalObject, JSValue); +bool getContextArg(JSC::JSGlobalObject* globalObject, JSValue& contextArg); +bool isUseMainContextDefaultLoaderConstant(JSC::JSGlobalObject* globalObject, JSValue value); } // namespace NodeVM -// This class represents a sandboxed global object for vm contexts -class NodeVMGlobalObject final : public Bun::GlobalScope { - using Base = Bun::GlobalScope; - -public: - static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::OverridesGetOwnPropertySlot | JSC::OverridesPut | JSC::OverridesGetOwnPropertyNames | JSC::GetOwnPropertySlotMayBeWrongAboutDontEnum | JSC::ProhibitsPropertyCaching; - static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction; - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); - static NodeVMGlobalObject* create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options); - static Structure* createStructure(JSC::VM& vm, JSC::JSValue prototype); - - DECLARE_INFO; - DECLARE_VISIT_CHILDREN; - - void finishCreation(JSC::VM&, NodeVMContextOptions options); - static void destroy(JSCell* cell); - void setContextifiedObject(JSC::JSObject* contextifiedObject); - JSC::JSObject* contextifiedObject() const { return m_sandbox.get(); } - void clearContextifiedObject(); - void sigintReceived(); - - // Override property access to delegate to contextified object - static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&); - static bool put(JSCell*, JSGlobalObject*, JSC::PropertyName, JSC::JSValue, JSC::PutPropertySlot&); - static void getOwnPropertyNames(JSObject*, JSGlobalObject*, JSC::PropertyNameArray&, JSC::DontEnumPropertiesMode); - static bool defineOwnProperty(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, const PropertyDescriptor& descriptor, bool shouldThrow); - static bool deleteProperty(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSC::DeletePropertySlot& slot); - -private: - NodeVMGlobalObject(JSC::VM& vm, JSC::Structure* structure); - ~NodeVMGlobalObject(); - - // The contextified object that acts as the global proxy - mutable JSC::WriteBarrier m_sandbox; -}; - -// Helper functions to create vm contexts and run code -JSC::JSValue createNodeVMBinding(Zig::GlobalObject*); -Structure* createNodeVMGlobalObjectStructure(JSC::VM&); -void configureNodeVM(JSC::VM&, Zig::GlobalObject*); - -// VM module functions -JSC_DECLARE_HOST_FUNCTION(vmModule_createContext); -JSC_DECLARE_HOST_FUNCTION(vmModule_isContext); -JSC_DECLARE_HOST_FUNCTION(vmModuleRunInNewContext); -JSC_DECLARE_HOST_FUNCTION(vmModuleRunInThisContext); - class BaseVMOptions { public: String filename; @@ -106,18 +60,103 @@ public: WTF::Vector cachedData; JSGlobalObject* parsingContext = nullptr; JSValue contextExtensions {}; - JSValue importer {}; bool produceCachedData = false; using BaseVMOptions::BaseVMOptions; - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg, JSValue* importer); }; class NodeVMContextOptions final { public: bool allowStrings = true; bool allowWasm = true; + bool notContextified = false; }; +class NodeVMGlobalObject; + +class NodeVMSpecialSandbox final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::OverridesGetOwnPropertySlot; + + static NodeVMSpecialSandbox* create(VM& vm, Structure* structure, NodeVMGlobalObject* globalObject); + + DECLARE_INFO; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&); + + NodeVMGlobalObject* parentGlobal() const { return m_parentGlobal.get(); } + +private: + WriteBarrier m_parentGlobal; + + NodeVMSpecialSandbox(VM& vm, Structure* structure, NodeVMGlobalObject* globalObject); + + void finishCreation(VM&); +}; + +// This class represents a sandboxed global object for vm contexts +class NodeVMGlobalObject final : public Bun::GlobalScope { +public: + using Base = Bun::GlobalScope; + + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::OverridesGetOwnPropertySlot | JSC::OverridesPut | JSC::OverridesGetOwnPropertyNames | JSC::GetOwnPropertySlotMayBeWrongAboutDontEnum | JSC::ProhibitsPropertyCaching; + static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + static NodeVMGlobalObject* create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options, JSValue importer); + static Structure* createStructure(JSC::VM& vm, JSC::JSValue prototype); + static const JSC::GlobalObjectMethodTable& globalObjectMethodTable(); + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + ~NodeVMGlobalObject(); + + void finishCreation(JSC::VM&); + static void destroy(JSCell* cell); + void setContextifiedObject(JSC::JSObject* contextifiedObject); + JSObject* contextifiedObject() const { return m_sandbox.get(); } + void clearContextifiedObject(); + void sigintReceived(); + bool isNotContextified() const { return m_contextOptions.notContextified; } + NodeVMSpecialSandbox* specialSandbox() const { return m_specialSandbox.get(); } + void setSpecialSandbox(NodeVMSpecialSandbox* sandbox) { m_specialSandbox.set(vm(), this, sandbox); } + JSValue dynamicImportCallback() const { return m_dynamicImportCallback.get(); } + + // Override property access to delegate to contextified object + static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&); + static bool put(JSCell*, JSGlobalObject*, JSC::PropertyName, JSC::JSValue, JSC::PutPropertySlot&); + static void getOwnPropertyNames(JSObject*, JSGlobalObject*, JSC::PropertyNameArray&, JSC::DontEnumPropertiesMode); + static bool defineOwnProperty(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, const PropertyDescriptor& descriptor, bool shouldThrow); + static bool deleteProperty(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSC::DeletePropertySlot& slot); + static JSC::JSInternalPromise* moduleLoaderImportModule(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSString* moduleNameValue, JSC::JSValue parameters, const JSC::SourceOrigin&); + +private: + // The contextified object that acts as the global proxy + WriteBarrier m_sandbox; + // A special object used when the context is not contextified. + WriteBarrier m_specialSandbox; + WriteBarrier m_dynamicImportCallback; + NodeVMContextOptions m_contextOptions {}; + + NodeVMGlobalObject(VM& vm, Structure* structure, NodeVMContextOptions contextOptions, JSValue importer); +}; + +// Helper functions to create vm contexts and run code +JSC::JSValue createNodeVMBinding(Zig::GlobalObject*); +Structure* createNodeVMGlobalObjectStructure(JSC::VM&); +void configureNodeVM(JSC::VM&, Zig::GlobalObject*); + +// VM module functions +JSC_DECLARE_HOST_FUNCTION(vmModule_createContext); +JSC_DECLARE_HOST_FUNCTION(vmModule_isContext); +JSC_DECLARE_HOST_FUNCTION(vmModuleRunInNewContext); +JSC_DECLARE_HOST_FUNCTION(vmModuleRunInThisContext); + } // namespace Bun diff --git a/src/bun.js/bindings/NodeVMModule.cpp b/src/bun.js/bindings/NodeVMModule.cpp index 4b6ce40cb2..14097cac21 100644 --- a/src/bun.js/bindings/NodeVMModule.cpp +++ b/src/bun.js/bindings/NodeVMModule.cpp @@ -29,15 +29,20 @@ JSArray* NodeVMModuleRequest::toJS(JSGlobalObject* globalObject) const JSArray* array = JSC::constructEmptyArray(globalObject, nullptr, 2); RETURN_IF_EXCEPTION(scope, {}); + array->putDirectIndex(globalObject, 0, JSC::jsString(globalObject->vm(), m_specifier)); + RETURN_IF_EXCEPTION(scope, {}); JSObject* attributes = JSC::constructEmptyObject(globalObject); RETURN_IF_EXCEPTION(scope, {}); + for (const auto& [key, value] : m_importAttributes) { attributes->putDirect(globalObject->vm(), JSC::Identifier::fromString(globalObject->vm(), key), JSC::jsString(globalObject->vm(), value), PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete); + RETURN_IF_EXCEPTION(scope, {}); } array->putDirectIndex(globalObject, 1, attributes); + RETURN_IF_EXCEPTION(scope, {}); return array; } @@ -73,6 +78,7 @@ JSValue NodeVMModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, b JSValue result {}; NodeVMGlobalObject* nodeVmGlobalObject = NodeVM::getGlobalObjectFromContext(globalObject, m_context.get(), false); + RETURN_IF_EXCEPTION(scope, {}); if (nodeVmGlobalObject) { globalObject = nodeVmGlobalObject; @@ -82,13 +88,12 @@ JSValue NodeVMModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, b if (sourceTextThis) { status(Status::Evaluating); evaluateDependencies(globalObject, record, timeout, breakOnSigint); + RETURN_IF_EXCEPTION(scope, ); sourceTextThis->initializeImportMeta(globalObject); } else if (syntheticThis) { syntheticThis->evaluate(globalObject); } - if (scope.exception()) [[unlikely]] { - return; - } + RETURN_IF_EXCEPTION(scope, ); result = record->evaluate(globalObject, jsUndefined(), jsNumber(static_cast(JSGenerator::ResumeMode::NormalMode))); }; @@ -206,11 +211,11 @@ NodeVMModule* NodeVMModule::create(JSC::VM& vm, JSC::JSGlobalObject* globalObjec JSValue disambiguator = args.at(2); if (disambiguator.isString()) { - return NodeVMSourceTextModule::create(vm, globalObject, args); + RELEASE_AND_RETURN(scope, NodeVMSourceTextModule::create(vm, globalObject, args)); } if (disambiguator.inherits(JSArray::info())) { - return NodeVMSyntheticModule::create(vm, globalObject, args); + RELEASE_AND_RETURN(scope, NodeVMSyntheticModule::create(vm, globalObject, args)); } throwArgumentTypeError(*globalObject, scope, 2, "sourceText or syntheticExportNames"_s, "Module"_s, "Module"_s, "string or array"_s); @@ -227,11 +232,14 @@ JSModuleNamespaceObject* NodeVMModule::namespaceObject(JSC::JSGlobalObject* glob if (auto* thisObject = jsDynamicCast(this)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - object = thisObject->moduleRecord(globalObject)->getModuleNamespace(globalObject); + AbstractModuleRecord* record = thisObject->moduleRecord(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + object = record->getModuleNamespace(globalObject); RETURN_IF_EXCEPTION(scope, {}); if (object) { namespaceObject(vm, object); } + RETURN_IF_EXCEPTION(scope, {}); } else { RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("NodeVMModule::namespaceObject called on an unsupported module type (%s)", classInfo()->className.characters()); } @@ -333,7 +341,7 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetNamespace, (JSC::JSGlobalObject * glob auto scope = DECLARE_THROW_SCOPE(vm); if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->namespaceObject(globalObject)); + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->namespaceObject(globalObject))); } throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); @@ -366,6 +374,7 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetModuleRequests, (JSC::JSGlobalObject * if (auto* sourceTextModule = jsDynamicCast(callFrame->thisValue())) { sourceTextModule->ensureModuleRecord(globalObject); + RETURN_IF_EXCEPTION(scope, {}); } const WTF::Vector& requests = thisObject->moduleRequests(); @@ -399,11 +408,11 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleEvaluate, (JSC::JSGlobalObject * globalOb } if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->evaluate(globalObject, timeout, breakOnSigint)); - } else { - throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); - return {}; + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->evaluate(globalObject, timeout, breakOnSigint))); } + + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); + return {}; } JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleLink, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -423,14 +432,11 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleLink, (JSC::JSGlobalObject * globalObject } if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->link(globalObject, specifiers, moduleNatives, callFrame->argument(2))); - // return thisObject->link(globalObject, linker); - // } else if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - // return thisObject->link(globalObject, specifiers, moduleNatives); - } else { - throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); - return {}; + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->link(globalObject, specifiers, moduleNatives, callFrame->argument(2)))); } + + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule"_s); + return {}; } JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleInstantiate, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -439,11 +445,11 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleInstantiate, (JSC::JSGlobalObject * globa auto scope = DECLARE_THROW_SCOPE(vm); if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->instantiate(globalObject)); + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->instantiate(globalObject))); } if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->instantiate(globalObject)); + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->instantiate(globalObject))); } throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); @@ -455,7 +461,7 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleSetExport, (JSC::JSGlobalObject * globalO VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (auto* thisObject = jsCast(callFrame->thisValue())) { + if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { JSValue nameValue = callFrame->argument(0); if (!nameValue.isString()) { Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "name"_str, "string"_s, nameValue); @@ -478,7 +484,7 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleCreateCachedData, (JSC::JSGlobalObject * auto scope = DECLARE_THROW_SCOPE(vm); if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { - return JSValue::encode(thisObject->cachedData(globalObject)); + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->cachedData(globalObject))); } throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule"_s); @@ -517,6 +523,7 @@ constructModule(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT ArgList args(callFrame); NodeVMModule* module = NodeVMModule::create(vm, globalObject, args); + RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(module); } diff --git a/src/bun.js/bindings/NodeVMScript.cpp b/src/bun.js/bindings/NodeVMScript.cpp index 42726c8275..077629b910 100644 --- a/src/bun.js/bindings/NodeVMScript.cpp +++ b/src/bun.js/bindings/NodeVMScript.cpp @@ -9,6 +9,7 @@ #include "JavaScriptCore/ProgramCodeBlock.h" #include "JavaScriptCore/SourceCodeKey.h" +#include "NodeVMScriptFetcher.h" #include "../vm/SigintWatcher.h" #include @@ -16,8 +17,12 @@ namespace Bun { using namespace NodeVM; -bool ScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +bool ScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg, JSValue* importer) { + if (importer) { + *importer = jsUndefined(); + } + bool any = BaseVMOptions::fromJS(globalObject, vm, scope, optionsArg); RETURN_IF_EXCEPTION(scope, false); @@ -64,9 +69,16 @@ bool ScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC:: JSValue importModuleDynamicallyValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "importModuleDynamically"_s)); RETURN_IF_EXCEPTION(scope, {}); - if (importModuleDynamicallyValue && importModuleDynamicallyValue.isCallable()) { - this->importer = importModuleDynamicallyValue; - any = true; + if (importModuleDynamicallyValue) { + if ((importModuleDynamicallyValue.isCallable() || isUseMainContextDefaultLoaderConstant(globalObject, importModuleDynamicallyValue))) { + if (importer) { + *importer = importModuleDynamicallyValue; + } + any = true; + } else if (!importModuleDynamicallyValue.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.importModuleDynamically"_s, "function"_s, importModuleDynamicallyValue); + return false; + } } } @@ -80,15 +92,22 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT auto scope = DECLARE_THROW_SCOPE(vm); ArgList args(callFrame); JSValue sourceArg = args.at(0); - String sourceString = sourceArg.isUndefined() ? emptyString() : sourceArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); + String sourceString; + if (sourceArg.isUndefined()) { + sourceString = emptyString(); + } else { + sourceString = sourceArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); + } JSValue optionsArg = args.at(1); ScriptOptions options(""_s); + JSValue importer; + if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); - } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + } else if (!options.fromJS(globalObject, vm, scope, optionsArg, &importer)) { RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); } @@ -108,13 +127,18 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT scope.release(); } - SourceCode source = makeSource(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)), JSC::SourceTaintedOrigin::Untainted, options.filename, TextPosition(options.lineOffset, options.columnOffset)); + RefPtr fetcher(NodeVMScriptFetcher::create(vm, importer, jsUndefined())); + + SourceCode source = makeSource(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), JSC::SourceTaintedOrigin::Untainted, options.filename, TextPosition(options.lineOffset, options.columnOffset)); RETURN_IF_EXCEPTION(scope, {}); const bool produceCachedData = options.produceCachedData; auto filename = options.filename; NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, WTFMove(source), WTFMove(options)); + RETURN_IF_EXCEPTION(scope, {}); + + fetcher->owner(vm, script); WTF::Vector& cachedData = script->cachedData(); @@ -139,6 +163,7 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT // JSC::ProgramCodeBlock::create() requires GC to be deferred. DeferGC deferGC(vm); codeBlock = JSC::ProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); + RETURN_IF_EXCEPTION(scope, {}); } JSC::CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); if (compilationResult != JSC::CompilationResult::CompilationFailed) { @@ -199,6 +224,9 @@ JSC::JSUint8Array* NodeVMScript::getBytecodeBuffer() std::span bytes = m_cachedBytecode->span(); m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); + if (!m_cachedBytecodeBuffer) { + return nullptr; + } } ASSERT(m_cachedBytecodeBuffer); @@ -333,6 +361,8 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM run(); } + RETURN_IF_EXCEPTION(scope, {}); + if (options.timeout) { vm.watchdog()->setTimeLimit(WTF::Seconds::fromMilliseconds(*oldLimit)); } @@ -351,7 +381,8 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM return {}; } - return JSValue::encode(result); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(result)); } JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -414,7 +445,7 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, } RETURN_IF_EXCEPTION(scope, {}); - return JSValue::encode(result); + RELEASE_AND_RETURN(scope, JSValue::encode(result)); } JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) @@ -433,7 +464,7 @@ JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject * globalObject, return encodedJSUndefined(); } - return JSValue::encode(jsString(vm, url)); + RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, url))); } JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedData, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) @@ -447,10 +478,10 @@ JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedData, (JSGlobalObject * globalObject, JS } if (auto* buffer = script->getBytecodeBuffer()) { - return JSValue::encode(buffer); + RELEASE_AND_RETURN(scope, JSValue::encode(buffer)); } - return JSValue::encode(jsUndefined()); + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); } JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataProduced, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) @@ -463,7 +494,7 @@ JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataProduced, (JSGlobalObject * globalOb return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); } - return JSValue::encode(jsBoolean(script->cachedDataProduced())); + RELEASE_AND_RETURN(scope, JSValue::encode(jsBoolean(script->cachedDataProduced()))); } JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) @@ -478,11 +509,11 @@ JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalOb switch (script->cachedDataRejected()) { case TriState::True: - return JSValue::encode(jsBoolean(true)); + RELEASE_AND_RETURN(scope, JSValue::encode(jsBoolean(true))); case TriState::False: - return JSValue::encode(jsBoolean(false)); + RELEASE_AND_RETURN(scope, JSValue::encode(jsBoolean(false))); default: - return JSValue::encode(jsUndefined()); + RELEASE_AND_RETURN(scope, encodedJSUndefined()); } } @@ -498,7 +529,7 @@ JSC_DEFINE_HOST_FUNCTION(scriptCreateCachedData, (JSGlobalObject * globalObject, } const JSC::SourceCode& source = script->source(); - return createCachedData(globalObject, source); + RELEASE_AND_RETURN(scope, createCachedData(globalObject, source)); } JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -519,7 +550,7 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject * globalObject, Cal JSObject* context = asObject(contextArg); ASSERT(nodeVmGlobalObject != nullptr); - return runInContext(nodeVmGlobalObject, script, context, args.at(1)); + RELEASE_AND_RETURN(scope, runInContext(nodeVmGlobalObject, script, context, args.at(1))); } JSC_DEFINE_HOST_FUNCTION(scriptRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -527,8 +558,6 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInNewContext, (JSGlobalObject * globalObject, VM& vm = JSC::getVM(globalObject); NodeVMScript* script = jsDynamicCast(callFrame->thisValue()); JSValue contextObjectValue = callFrame->argument(0); - // TODO: options - // JSValue optionsObjectValue = callFrame->argument(1); auto scope = DECLARE_THROW_SCOPE(vm); if (!script) { @@ -536,24 +565,36 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInNewContext, (JSGlobalObject * globalObject, return {}; } - if (contextObjectValue.isUndefined()) { - contextObjectValue = JSC::constructEmptyObject(globalObject); - } + bool notContextified = NodeVM::getContextArg(globalObject, contextObjectValue); if (!contextObjectValue || !contextObjectValue.isObject()) [[unlikely]] { throwTypeError(globalObject, scope, "Context must be an object"_s); return {}; } - // we don't care about options for now - // TODO: options - // bool didThrow = false; + JSValue contextOptionsArg = callFrame->argument(1); + NodeVMContextOptions contextOptions {}; + JSValue importer; - auto* zigGlobal = defaultGlobalObject(globalObject); + if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, contextOptionsArg, contextOptions, "contextCodeGeneration", &importer)) { + return *encodedException; + } + + contextOptions.notContextified = notContextified; + + auto* zigGlobalObject = defaultGlobalObject(globalObject); JSObject* context = asObject(contextObjectValue); auto* targetContext = NodeVMGlobalObject::create(vm, - zigGlobal->NodeVMGlobalObjectStructure(), - {}); + zigGlobalObject->NodeVMGlobalObjectStructure(), + contextOptions, importer); + RETURN_IF_EXCEPTION(scope, {}); + + if (notContextified) { + auto* specialSandbox = NodeVMSpecialSandbox::create(vm, zigGlobalObject->NodeVMSpecialSandboxStructure(), targetContext); + RETURN_IF_EXCEPTION(scope, {}); + targetContext->setSpecialSandbox(specialSandbox); + RELEASE_AND_RETURN(scope, runInContext(targetContext, script, targetContext->specialSandbox(), callFrame->argument(1))); + } RELEASE_AND_RETURN(scope, runInContext(targetContext, script, context, callFrame->argument(1))); } diff --git a/src/bun.js/bindings/NodeVMScript.h b/src/bun.js/bindings/NodeVMScript.h index b7e7b2dec4..5637ae939a 100644 --- a/src/bun.js/bindings/NodeVMScript.h +++ b/src/bun.js/bindings/NodeVMScript.h @@ -10,12 +10,11 @@ class ScriptOptions : public BaseVMOptions { public: WTF::Vector cachedData; std::optional timeout = std::nullopt; - JSValue importer {}; bool produceCachedData = false; using BaseVMOptions::BaseVMOptions; - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg, JSValue* importer); }; class NodeVMScriptConstructor final : public JSC::InternalFunction { diff --git a/src/bun.js/bindings/NodeVMScriptFetcher.h b/src/bun.js/bindings/NodeVMScriptFetcher.h index 0ec7416e06..b8676b2445 100644 --- a/src/bun.js/bindings/NodeVMScriptFetcher.h +++ b/src/bun.js/bindings/NodeVMScriptFetcher.h @@ -3,27 +3,39 @@ #include "root.h" #include +#include namespace Bun { // The presence of this class in a JSFunction's sourceOrigin indicates that the function was compiled by Bun's node:vm implementation. class NodeVMScriptFetcher : public JSC::ScriptFetcher { public: - static Ref create(JSC::VM& vm, JSC::JSValue dynamicImportCallback) { return adoptRef(*new NodeVMScriptFetcher(vm, dynamicImportCallback)); } + static Ref create(JSC::VM& vm, JSC::JSValue dynamicImportCallback, JSC::JSValue owner) { return adoptRef(*new NodeVMScriptFetcher(vm, dynamicImportCallback, owner)); } Type fetcherType() const final { return Type::NodeVM; } JSC::JSValue dynamicImportCallback() const { return m_dynamicImportCallback.get(); } - JSC::JSFunction* owner() const { return m_owner.get(); } - void owner(JSC::VM& vm, JSC::JSFunction* value) { m_owner.set(vm, value); } + JSC::JSValue owner() const { return m_owner.get(); } + void owner(JSC::VM& vm, JSC::JSValue value) { m_owner.set(vm, value); } + + bool isUsingDefaultLoader() const { return m_isUsingDefaultLoader; } + auto temporarilyUseDefaultLoader() + { + m_isUsingDefaultLoader = true; + return makeScopeExit([this] { + m_isUsingDefaultLoader = false; + }); + } private: JSC::Strong m_dynamicImportCallback; - JSC::Strong m_owner; + JSC::Strong m_owner; + bool m_isUsingDefaultLoader = false; - NodeVMScriptFetcher(JSC::VM& vm, JSC::JSValue dynamicImportCallback) + NodeVMScriptFetcher(JSC::VM& vm, JSC::JSValue dynamicImportCallback, JSC::JSValue owner) : m_dynamicImportCallback(vm, dynamicImportCallback) + , m_owner(vm, owner) { } }; diff --git a/src/bun.js/bindings/NodeVMSourceTextModule.cpp b/src/bun.js/bindings/NodeVMSourceTextModule.cpp index fb72e3bb84..8af22d59a5 100644 --- a/src/bun.js/bindings/NodeVMSourceTextModule.cpp +++ b/src/bun.js/bindings/NodeVMSourceTextModule.cpp @@ -1,3 +1,4 @@ +#include "NodeVMScriptFetcher.h" #include "NodeVMSourceTextModule.h" #include "NodeVMSyntheticModule.h" @@ -77,16 +78,35 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g return nullptr; } - uint32_t lineOffset = lineOffsetValue.toUInt32(globalObject); - uint32_t columnOffset = columnOffsetValue.toUInt32(globalObject); + JSValue dynamicImportCallback = args.at(8); + if (!dynamicImportCallback.isUndefined() && !dynamicImportCallback.isCallable()) { + throwArgumentTypeError(*globalObject, scope, 8, "dynamicImportCallback"_s, "Module"_s, "Module"_s, "function"_s); + return nullptr; + } - Ref sourceProvider = StringSourceProvider::create(sourceTextValue.toWTFString(globalObject), SourceOrigin {}, String {}, SourceTaintedOrigin::Untainted, + uint32_t lineOffset = lineOffsetValue.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + uint32_t columnOffset = columnOffsetValue.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + + RefPtr fetcher(NodeVMScriptFetcher::create(vm, dynamicImportCallback, moduleWrapper)); + RETURN_IF_EXCEPTION(scope, nullptr); + + SourceOrigin sourceOrigin { {}, *fetcher }; + + WTF::String sourceText = sourceTextValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + + Ref sourceProvider = StringSourceProvider::create(WTFMove(sourceText), sourceOrigin, String {}, SourceTaintedOrigin::Untainted, TextPosition { OrdinalNumber::fromZeroBasedInt(lineOffset), OrdinalNumber::fromZeroBasedInt(columnOffset) }, SourceProviderSourceType::Module); SourceCode sourceCode(WTFMove(sourceProvider), lineOffset, columnOffset); auto* zigGlobalObject = defaultGlobalObject(globalObject); - NodeVMSourceTextModule* ptr = new (NotNull, allocateCell(vm)) NodeVMSourceTextModule(vm, zigGlobalObject->NodeVMSourceTextModuleStructure(), identifierValue.toWTFString(globalObject), contextValue, WTFMove(sourceCode), moduleWrapper); + WTF::String identifier = identifierValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + NodeVMSourceTextModule* ptr = new (NotNull, allocateCell(vm)) NodeVMSourceTextModule(vm, zigGlobalObject->NodeVMSourceTextModuleStructure(), WTFMove(identifier), contextValue, WTFMove(sourceCode), moduleWrapper); + RETURN_IF_EXCEPTION(scope, nullptr); ptr->finishCreation(vm); if (!initializeImportMeta.isUndefined()) { @@ -111,7 +131,9 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; SourceCodeKey key(ptr->sourceCode(), {}, SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, DerivedContextType::None, EvalContextType::None, false, {}, std::nullopt); Ref cachedBytecode = CachedBytecode::create(std::span(cachedData), nullptr, {}); + RETURN_IF_EXCEPTION(scope, nullptr); UnlinkedModuleProgramCodeBlock* unlinkedBlock = decodeCodeBlock(vm, key, WTFMove(cachedBytecode)); + RETURN_IF_EXCEPTION(scope, nullptr); if (unlinkedBlock) { JSScope* jsScope = globalObject->globalScope(); @@ -120,9 +142,11 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g // JSC::ProgramCodeBlock::create() requires GC to be deferred. DeferGC deferGC(vm); codeBlock = ModuleProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); + RETURN_IF_EXCEPTION(scope, nullptr); } if (codeBlock) { CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); + RETURN_IF_EXCEPTION(scope, nullptr); if (compilationResult != CompilationResult::CompilationFailed) { executable->installCode(codeBlock); return ptr; @@ -184,7 +208,9 @@ JSValue NodeVMSourceTextModule::createModuleRecord(JSGlobalObject* globalObject) const auto& requests = moduleRecord->requestedModules(); if (requests.isEmpty()) { - return constructEmptyArray(globalObject, nullptr, 0); + JSArray* requestsArray = constructEmptyArray(globalObject, nullptr, 0); + RETURN_IF_EXCEPTION(scope, {}); + return requestsArray; } JSArray* requestsArray = constructEmptyArray(globalObject, nullptr, requests.size()); @@ -312,26 +338,35 @@ JSValue NodeVMSourceTextModule::link(JSGlobalObject* globalObject, JSArray* spec if (length != 0) { for (unsigned i = 0; i < length; i++) { JSValue specifierValue = specifiers->getDirectIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); JSValue moduleNativeValue = moduleNatives->getDirectIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); ASSERT(specifierValue.isString()); ASSERT(moduleNativeValue.isObject()); WTF::String specifier = specifierValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); JSObject* moduleNative = moduleNativeValue.getObject(); + RETURN_IF_EXCEPTION(scope, {}); AbstractModuleRecord* resolvedRecord = jsCast(moduleNative)->moduleRecord(globalObject); + RETURN_IF_EXCEPTION(scope, {}); record->setImportedModule(globalObject, Identifier::fromString(vm, specifier), resolvedRecord); + RETURN_IF_EXCEPTION(scope, {}); m_resolveCache.set(WTFMove(specifier), WriteBarrier { vm, this, moduleNative }); + RETURN_IF_EXCEPTION(scope, {}); } } - if (NodeVMGlobalObject* nodeVmGlobalObject = getGlobalObjectFromContext(globalObject, m_context.get(), false)) { + NodeVMGlobalObject* nodeVmGlobalObject = getGlobalObjectFromContext(globalObject, m_context.get(), false); + RETURN_IF_EXCEPTION(scope, {}); + + if (nodeVmGlobalObject) { globalObject = nodeVmGlobalObject; } Synchronousness sync = record->link(globalObject, scriptFetcher); - RETURN_IF_EXCEPTION(scope, {}); if (sync == Synchronousness::Async) { @@ -355,6 +390,7 @@ RefPtr NodeVMSourceTextModule::bytecode(JSGlobalObject* globalOb if (!m_bytecode) { if (!m_cachedExecutable) { ModuleProgramExecutable* executable = ModuleProgramExecutable::tryCreate(globalObject, m_sourceCode); + RETURN_IF_EXCEPTION(scope, nullptr); if (!executable) { if (!scope.exception()) { throwSyntaxError(globalObject, scope, "Failed to create cached executable"_s); @@ -364,6 +400,7 @@ RefPtr NodeVMSourceTextModule::bytecode(JSGlobalObject* globalOb m_cachedExecutable.set(vm, this, executable); } m_bytecode = getBytecode(globalObject, m_cachedExecutable.get(), m_sourceCode); + RETURN_IF_EXCEPTION(scope, nullptr); } return m_bytecode; @@ -371,10 +408,16 @@ RefPtr NodeVMSourceTextModule::bytecode(JSGlobalObject* globalOb JSUint8Array* NodeVMSourceTextModule::cachedData(JSGlobalObject* globalObject) { + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!m_cachedBytecodeBuffer) { RefPtr cachedBytecode = bytecode(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); std::span bytes = cachedBytecode->span(); - m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject, bytes)); + JSUint8Array* buffer = WebCore::createBuffer(globalObject, bytes); + RETURN_IF_EXCEPTION(scope, nullptr); + m_cachedBytecodeBuffer.set(vm, this, buffer); } return m_cachedBytecodeBuffer.get(); @@ -389,7 +432,11 @@ void NodeVMSourceTextModule::initializeImportMeta(JSGlobalObject* globalObject) JSModuleEnvironment* moduleEnvironment = m_moduleRecord->moduleEnvironmentMayBeNull(); ASSERT(moduleEnvironment != nullptr); + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue metaValue = moduleEnvironment->get(globalObject, globalObject->vm().propertyNames->builtinNames().metaPrivateName()); + RETURN_IF_EXCEPTION(scope, ); ASSERT(metaValue); ASSERT(metaValue.isObject()); @@ -400,6 +447,7 @@ void NodeVMSourceTextModule::initializeImportMeta(JSGlobalObject* globalObject) args.append(m_moduleWrapper.get()); JSC::call(globalObject, m_initializeImportMeta.get(), callData, jsUndefined(), args); + RETURN_IF_EXCEPTION(scope, ); } JSObject* NodeVMSourceTextModule::createPrototype(VM& vm, JSGlobalObject* globalObject) diff --git a/src/bun.js/bindings/NodeVMSyntheticModule.cpp b/src/bun.js/bindings/NodeVMSyntheticModule.cpp index 5a178a18cf..3cd49018a1 100644 --- a/src/bun.js/bindings/NodeVMSyntheticModule.cpp +++ b/src/bun.js/bindings/NodeVMSyntheticModule.cpp @@ -67,15 +67,20 @@ NodeVMSyntheticModule* NodeVMSyntheticModule::create(VM& vm, JSGlobalObject* glo WTF::HashSet exportNames; for (unsigned i = 0; i < exportNamesArray->getArrayLength(); i++) { JSValue exportNameValue = exportNamesArray->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, nullptr); if (!exportNameValue.isString()) { throwArgumentTypeError(*globalObject, scope, 2, "exportNames"_s, "Module"_s, "Module"_s, "string[]"_s); + return nullptr; } exportNames.addVoid(exportNameValue.toWTFString(globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); } auto* zigGlobalObject = defaultGlobalObject(globalObject); auto* structure = zigGlobalObject->NodeVMSyntheticModuleStructure(); - auto* ptr = new (NotNull, allocateCell(vm)) NodeVMSyntheticModule(vm, structure, identifierValue.toWTFString(globalObject), contextValue, moduleWrapperValue, WTFMove(exportNames), syntheticEvaluationStepsValue); + WTF::String identifier = identifierValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* ptr = new (NotNull, allocateCell(vm)) NodeVMSyntheticModule(vm, structure, WTFMove(identifier), contextValue, moduleWrapperValue, WTFMove(exportNames), syntheticEvaluationStepsValue); ptr->finishCreation(vm); return ptr; } @@ -204,8 +209,11 @@ void NodeVMSyntheticModule::setExport(JSGlobalObject* globalObject, WTF::String } ensureModuleRecord(globalObject); + RETURN_IF_EXCEPTION(scope, ); JSModuleNamespaceObject* namespaceObject = m_moduleRecord->getModuleNamespace(globalObject, false); + RETURN_IF_EXCEPTION(scope, ); namespaceObject->overrideExportValue(globalObject, Identifier::fromString(vm, exportName), value); + RETURN_IF_EXCEPTION(scope, ); } JSObject* NodeVMSyntheticModule::createPrototype(VM& vm, JSGlobalObject* globalObject) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index d69a6da4f6..e518145214 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -4136,22 +4136,26 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j { auto* globalObject = static_cast(jsGlobalObject); - if (JSC::JSInternalPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin)) { - return result; + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + { + JSC::JSInternalPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin); + RETURN_IF_EXCEPTION(scope, nullptr); + if (result) { + return result; + } } - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); JSC::Identifier resolvedIdentifier; auto moduleName = moduleNameValue->value(globalObject); - RETURN_IF_EXCEPTION(scope, {}); + RETURN_IF_EXCEPTION(scope, nullptr); if (globalObject->onLoadPlugins.hasVirtualModules()) { if (auto resolution = globalObject->onLoadPlugins.resolveVirtualModule(moduleName, sourceOrigin.url().protocolIsFile() ? sourceOrigin.url().fileSystemPath() : String())) { resolvedIdentifier = JSC::Identifier::fromString(vm, resolution.value()); - auto result = JSC::importModule(globalObject, resolvedIdentifier, - JSC::jsUndefined(), parameters, JSC::jsUndefined()); + auto result = JSC::importModule(globalObject, resolvedIdentifier, JSC::jsUndefined(), parameters, JSC::jsUndefined()); if (scope.exception()) [[unlikely]] { auto* promise = JSC::JSInternalPromise::create(vm, globalObject->internalPromiseStructure()); return promise->rejectWithCaughtException(globalObject, scope); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 3bf94799d4..9420108724 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -289,6 +289,7 @@ public: JSC::JSFunction* requireESMFromHijackedExtension() const { return m_commonJSRequireESMFromHijackedExtensionFunction.getInitializedOnMainThread(this); } Structure* NodeVMGlobalObjectStructure() const { return m_cachedNodeVMGlobalObjectStructure.getInitializedOnMainThread(this); } + Structure* NodeVMSpecialSandboxStructure() const { return m_cachedNodeVMSpecialSandboxStructure.getInitializedOnMainThread(this); } Structure* globalProxyStructure() const { return m_cachedGlobalProxyStructure.getInitializedOnMainThread(this); } JSObject* lazyTestModuleObject() const { return m_lazyTestModuleObject.getInitializedOnMainThread(this); } JSObject* lazyPreloadTestModuleObject() const { return m_lazyPreloadTestModuleObject.getInitializedOnMainThread(this); } @@ -576,6 +577,7 @@ public: V(private, LazyPropertyOfGlobalObject, m_lazyPreloadTestModuleObject) \ V(public, LazyPropertyOfGlobalObject, m_testMatcherUtilsObject) \ V(public, LazyPropertyOfGlobalObject, m_cachedNodeVMGlobalObjectStructure) \ + V(public, LazyPropertyOfGlobalObject, m_cachedNodeVMSpecialSandboxStructure) \ V(private, LazyPropertyOfGlobalObject, m_cachedGlobalProxyStructure) \ V(private, LazyPropertyOfGlobalObject, m_commonJSModuleObjectStructure) \ V(private, LazyPropertyOfGlobalObject, m_JSSocketAddressDTOStructure) \ @@ -617,7 +619,9 @@ public: V(public, LazyPropertyOfGlobalObject, m_statValues) \ V(public, LazyPropertyOfGlobalObject, m_bigintStatValues) \ V(public, LazyPropertyOfGlobalObject, m_statFsValues) \ - V(public, LazyPropertyOfGlobalObject, m_bigintStatFsValues) + V(public, LazyPropertyOfGlobalObject, m_bigintStatFsValues) \ + V(public, LazyPropertyOfGlobalObject, m_nodeVMDontContextify) \ + V(public, LazyPropertyOfGlobalObject, m_nodeVMUseMainContextDefaultLoader) #define DECLARE_GLOBALOBJECT_GC_MEMBER(visibility, T, name) \ visibility: \ diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 66e29275fd..427954d53c 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -36,6 +36,7 @@ public: std::unique_ptr m_clientSubspaceForRequireResolveFunction; std::unique_ptr m_clientSubspaceForBundlerPlugin; std::unique_ptr m_clientSubspaceForNodeVMGlobalObject; + std::unique_ptr m_clientSubspaceForNodeVMSpecialSandbox; std::unique_ptr m_clientSubspaceForNodeVMScript; std::unique_ptr m_clientSubspaceForNodeVMSourceTextModule; std::unique_ptr m_clientSubspaceForNodeVMSyntheticModule; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 1de228dd13..12a275ca46 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -36,6 +36,7 @@ public: std::unique_ptr m_subspaceForRequireResolveFunction; std::unique_ptr m_subspaceForBundlerPlugin; std::unique_ptr m_subspaceForNodeVMGlobalObject; + std::unique_ptr m_subspaceForNodeVMSpecialSandbox; std::unique_ptr m_subspaceForNodeVMScript; std::unique_ptr m_subspaceForNodeVMSourceTextModule; std::unique_ptr m_subspaceForNodeVMSyntheticModule; diff --git a/src/js/node/vm.ts b/src/js/node/vm.ts index 86b64c9b1e..19da675fcb 100644 --- a/src/js/node/vm.ts +++ b/src/js/node/vm.ts @@ -43,14 +43,14 @@ const { Module: ModuleNative, createContext, isContext, - // runInNewContext: moduleRunInNewContext, - // runInThisContext: moduleRunInThisContext, compileFunction, isModuleNamespaceObject, kUnlinked, kLinked, kEvaluated, kErrored, + DONT_CONTEXTIFY, + USE_MAIN_CONTEXT_DEFAULT_LOADER, } = vm; function runInContext(code, context, options) { @@ -88,7 +88,7 @@ function measureMemory() { } function validateContext(contextifiedObject) { - if (!isContext(contextifiedObject) && contextifiedObject !== constants.DONT_CONTEXTIFY) { + if (contextifiedObject !== constants.DONT_CONTEXTIFY && !isContext(contextifiedObject)) { const error = new Error('The "contextifiedObject" argument must be an vm.Context'); error.code = "ERR_INVALID_ARG_TYPE"; error.name = "TypeError"; @@ -143,7 +143,6 @@ class Module { }); } - let registry: any = { __proto__: null }; if (sourceText !== undefined) { this[kNative] = new ModuleNative( identifier, @@ -154,19 +153,8 @@ class Module { options.cachedData, options.initializeImportMeta, this, + options.importModuleDynamically ? importModuleDynamicallyWrap(options.importModuleDynamically) : undefined, ); - registry = { - __proto__: null, - initializeImportMeta: options.initializeImportMeta, - importModuleDynamically: options.importModuleDynamically - ? importModuleDynamicallyWrap(options.importModuleDynamically) - : undefined, - }; - // This will take precedence over the referrer as the object being - // passed into the callbacks. - registry.callbackReferrer = this; - // const { registerModule } = require("internal/modules/esm/utils"); - // registerModule(this[kNative], registry); } else { $assert(syntheticEvaluationSteps); this[kNative] = new ModuleNative(identifier, context, syntheticExportNames, syntheticEvaluationSteps, this); @@ -442,8 +430,8 @@ class SyntheticModule extends Module { const constants = { __proto__: null, - USE_MAIN_CONTEXT_DEFAULT_LOADER: Symbol("vm_dynamic_import_main_context_default"), - DONT_CONTEXTIFY: Symbol("vm_context_no_contextify"), + USE_MAIN_CONTEXT_DEFAULT_LOADER, + DONT_CONTEXTIFY, }; function isModule(object) { @@ -452,7 +440,7 @@ function isModule(object) { function importModuleDynamicallyWrap(importModuleDynamically) { const importModuleDynamicallyWrapper = async (...args) => { - const m: any = importModuleDynamically.$apply(this, args); + const m: any = await importModuleDynamically.$apply(this, args); if (isModuleNamespaceObject(m)) { return m; } diff --git a/test/js/node/test/parallel/test-vm-context-dont-contextify.js b/test/js/node/test/parallel/test-vm-context-dont-contextify.js new file mode 100644 index 0000000000..d75fc1438d --- /dev/null +++ b/test/js/node/test/parallel/test-vm-context-dont-contextify.js @@ -0,0 +1,185 @@ +'use strict'; + +// Check vm.constants.DONT_CONTEXTIFY works. + +const common = require('../common'); + +const assert = require('assert'); +const vm = require('vm'); +const fixtures = require('../common/fixtures'); + +{ + // Check identity of the returned object. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + // The globalThis in the new context should be reference equal to the returned object. + assert.strictEqual(vm.runInContext('globalThis', context), context); + assert(vm.isContext(context)); + assert.strictEqual(typeof context.Array, 'function'); // Can access builtins directly. + assert.deepStrictEqual(Object.keys(context), []); // Properties on the global proxy are not enumerable +} + +{ + // Check that vm.createContext can return the original context if re-passed. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const context2 = new vm.createContext(context); + assert.strictEqual(context, context2); +} + +{ + // Check that the context is vanilla and that Script.runInContext works. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const result = + new vm.Script('globalThis.hey = 1; Object.freeze(globalThis); globalThis.process') + .runInContext(context); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check Script.runInNewContext works. + const result = + new vm.Script('globalThis.hey = 1; Object.freeze(globalThis); globalThis.process') + .runInNewContext(vm.constants.DONT_CONTEXTIFY); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check that vm.runInNewContext() works + const result = vm.runInNewContext( + 'globalThis.hey = 1; Object.freeze(globalThis); globalThis.process', + vm.constants.DONT_CONTEXTIFY); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check that the global object of vanilla contexts work as expected. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + + // Check mutation via globalThis. + vm.runInContext('globalThis.foo = 1;', context); + assert.strictEqual(globalThis.foo, undefined); // Should not pollute the current context. + assert.strictEqual(context.foo, 1); + assert.strictEqual(vm.runInContext('globalThis.foo', context), 1); + assert.strictEqual(vm.runInContext('foo', context), 1); + + // Check mutation from outside. + context.foo = 2; + assert.strictEqual(context.foo, 2); + assert.strictEqual(vm.runInContext('globalThis.foo', context), 2); + assert.strictEqual(vm.runInContext('foo', context), 2); + + // Check contextual mutation. + vm.runInContext('bar = 1;', context); + assert.strictEqual(globalThis.bar, undefined); // Should not pollute the current context. + assert.strictEqual(context.bar, 1); + assert.strictEqual(vm.runInContext('globalThis.bar', context), 1); + assert.strictEqual(vm.runInContext('bar', context), 1); + + // Check adding new property from outside. + context.baz = 1; + assert.strictEqual(context.baz, 1); + assert.strictEqual(vm.runInContext('globalThis.baz', context), 1); + assert.strictEqual(vm.runInContext('baz', context), 1); + + // Check mutation via Object.defineProperty(). + vm.runInContext('Object.defineProperty(globalThis, "qux", {' + + 'enumerable: false, configurable: false, get() { return 1; } })', context); + assert.strictEqual(globalThis.qux, undefined); // Should not pollute the current context. + assert.strictEqual(context.qux, 1); + assert.strictEqual(vm.runInContext('qux', context), 1); + const desc = Object.getOwnPropertyDescriptor(context, 'qux'); + assert.strictEqual(desc.enumerable, false); + assert.strictEqual(desc.configurable, false); + assert.strictEqual(typeof desc.get, 'function'); + assert.throws(() => { context.qux = 1; }, { name: 'TypeError' }); + assert.throws(() => { Object.defineProperty(context, 'qux', { value: 1 }); }, { name: 'TypeError' }); + // Setting a value without a setter fails silently. + assert.strictEqual(vm.runInContext('qux = 2; qux', context), 1); + assert.throws(() => { + vm.runInContext('Object.defineProperty(globalThis, "qux", { value: 1 });'); + }, { name: 'TypeError' }); +} + +function checkFrozen(context) { + // Check mutation via globalThis. + vm.runInContext('globalThis.foo = 1', context); // Invoking setters on freezed object fails silently. + assert.strictEqual(context.foo, undefined); + assert.strictEqual(vm.runInContext('globalThis.foo', context), undefined); + assert.throws(() => { + vm.runInContext('foo', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check mutation from outside. + assert.throws(() => { + context.foo = 2; + }, { name: 'TypeError' }); + assert.strictEqual(context.foo, undefined); + assert.strictEqual(vm.runInContext('globalThis.foo', context), undefined); + assert.throws(() => { + vm.runInContext('foo', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check contextual mutation. + vm.runInContext('bar = 1', context); // Invoking setters on freezed object fails silently. + assert.strictEqual(context.bar, undefined); + assert.strictEqual(vm.runInContext('globalThis.bar', context), undefined); + assert.throws(() => { + vm.runInContext('bar', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check mutation via Object.defineProperty(). + assert.throws(() => { + vm.runInContext('Object.defineProperty(globalThis, "qux", {' + + 'enumerable: false, configurable: false, get() { return 1; } })', context); + }, { + name: 'TypeError' + }); + assert.strictEqual(context.qux, undefined); + assert.strictEqual(vm.runInContext('globalThis.qux', context), undefined); + assert.strictEqual(Object.getOwnPropertyDescriptor(context, 'qux'), undefined); + assert.throws(() => { Object.defineProperty(context, 'qux', { value: 1 }); }, { name: 'TypeError' }); + assert.throws(() => { + vm.runInContext('qux', context); + }, { + name: 'ReferenceError' + }); +} + +{ + // Check freezing the vanilla context's global object from within the context. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + // Only vanilla contexts' globals can be freezed. Contextified global objects cannot be freezed + // due to the presence of interceptors. + vm.runInContext('Object.freeze(globalThis)', context); + checkFrozen(context); +} + +{ + // Check freezing the vanilla context's global object from outside the context. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + Object.freeze(context); + checkFrozen(context); +} + +// Check importModuleDynamically works. +(async function() { + { + const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs'); + const namespace = await import(moduleUrl.href); + // Check dynamic import works + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const script = new vm.Script(`import(${JSON.stringify(moduleUrl)})`, { + importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, + }); + const promise = script.runInContext(context); + assert.strictEqual(await promise, namespace); + } +})().catch(common.mustNotCall()); diff --git a/test/js/node/test/parallel/test-vm-module-dynamic-import.js b/test/js/node/test/parallel/test-vm-module-dynamic-import.js new file mode 100644 index 0000000000..bd542ca920 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-dynamic-import.js @@ -0,0 +1,117 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); + +const assert = require('assert'); +const { Script, SourceTextModule } = require('vm'); + +async function testNoCallback() { + const m = new SourceTextModule(` + globalThis.importResult = import("foo"); + globalThis.importResult.catch(() => {}); + `); + await m.link(common.mustNotCall()); + await m.evaluate(); + let threw = false; + try { + await globalThis.importResult; + } catch (err) { + threw = true; + assert.strictEqual(err.code, 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING'); + } + delete globalThis.importResult; + assert(threw); +} + +async function test() { + const foo = new SourceTextModule('export const a = 1;'); + await foo.link(common.mustNotCall()); + await foo.evaluate(); + + { + const s = new Script('import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, s); + return foo; + }), + }); + + const result = s.runInThisContext(); + assert.strictEqual(await result, foo.namespace); + } + + { + const m = new SourceTextModule('globalThis.fooResult = import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, m); + return foo; + }), + }); + await m.link(common.mustNotCall()); + await m.evaluate(); + assert.strictEqual(await globalThis.fooResult, foo.namespace); + delete globalThis.fooResult; + } + + { + const s = new Script('import("foo", { with: { key: "value" } })', { + importModuleDynamically: common.mustCall((specifier, wrap, attributes) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, s); + assert.deepStrictEqual(attributes, { __proto__: null, key: 'value' }); + return foo; + }), + }); + + const result = s.runInThisContext(); + assert.strictEqual(await result, foo.namespace); + } +} + +async function testInvalid() { + const m = new SourceTextModule('globalThis.fooResult = import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + return 5; + }), + }); + await m.link(common.mustNotCall()); + await m.evaluate(); + await globalThis.fooResult.catch(common.mustCall((e) => { + assert.strictEqual(e.code, 'ERR_VM_MODULE_NOT_MODULE'); + })); + delete globalThis.fooResult; + + const s = new Script('import("bar")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + return undefined; + }), + }); + let threw = false; + try { + await s.runInThisContext(); + } catch (e) { + threw = true; + assert.strictEqual(e.code, 'ERR_VM_MODULE_NOT_MODULE'); + } + assert(threw); +} + +async function testInvalidimportModuleDynamically() { + assert.throws( + () => new Script( + 'import("foo")', + { importModuleDynamically: false }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} + +(async function() { + await testNoCallback(); + await test(); + await testInvalid(); + await testInvalidimportModuleDynamically(); +}()).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-module-dynamic-namespace.js b/test/js/node/test/parallel/test-vm-module-dynamic-namespace.js new file mode 100644 index 0000000000..84937cd78d --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-dynamic-namespace.js @@ -0,0 +1,26 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); + +const assert = require('assert'); + +const { types } = require('util'); +const { SourceTextModule } = require('vm'); + +(async () => { + const m = new SourceTextModule('globalThis.importResult = import("");', { + importModuleDynamically: common.mustCall(async (specifier, wrap) => { + const m = new SourceTextModule(''); + await m.link(() => 0); + await m.evaluate(); + return m.namespace; + }), + }); + await m.link(() => 0); + await m.evaluate(); + const ns = await globalThis.importResult; + delete globalThis.importResult; + assert.ok(types.isModuleNamespaceObject(ns)); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-module-referrer-realm.mjs b/test/js/node/test/parallel/test-vm-module-referrer-realm.mjs new file mode 100644 index 0000000000..3957f147d8 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-referrer-realm.mjs @@ -0,0 +1,70 @@ +// Flags: --experimental-vm-modules +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { Script, SourceTextModule, createContext } from 'node:vm'; + +async function test() { + const foo = new SourceTextModule('export const a = 1;'); + await foo.link(common.mustNotCall()); + await foo.evaluate(); + + const ctx = createContext({}, { + importModuleDynamically: common.mustCall((specifier, wrap) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, ctx); + return foo; + }, 2), + }); + { + const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', { + importModuleDynamically: common.mustNotCall(), + }); + + const result = s.runInContext(ctx); + assert.strictEqual(await result, foo.namespace); + } + + { + const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', { + context: ctx, + importModuleDynamically: common.mustNotCall(), + }); + await m.link(common.mustNotCall()); + await m.evaluate(); + assert.strictEqual(await ctx.fooResult, foo.namespace); + delete ctx.fooResult; + } +} + +async function testMissing() { + const ctx = createContext({}); + { + const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', { + importModuleDynamically: common.mustNotCall(), + }); + + const result = s.runInContext(ctx); + await assert.rejects(result, { + code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', + }); + } + + { + const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', { + context: ctx, + importModuleDynamically: common.mustNotCall(), + }); + await m.link(common.mustNotCall()); + await m.evaluate(); + + await assert.rejects(ctx.fooResult, { + code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', + }); + delete ctx.fooResult; + } +} + +await Promise.all([ + test(), + testMissing(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-no-dynamic-import-callback.js b/test/js/node/test/parallel/test-vm-no-dynamic-import-callback.js new file mode 100644 index 0000000000..35b553d587 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-no-dynamic-import-callback.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const { Script, compileFunction } = require('vm'); +const assert = require('assert'); + +assert.rejects(async () => { + const script = new Script('import("fs")'); + const imported = script.runInThisContext(); + await imported; +}, { + code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' +}).then(common.mustCall()); + +assert.rejects(async () => { + const imported = compileFunction('return import("fs")')(); + await imported; +}, { + code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' +}).then(common.mustCall()); diff --git a/test/js/node/test/sequential/test-vm-timeout-rethrow.js b/test/js/node/test/sequential/test-vm-timeout-rethrow.js new file mode 100644 index 0000000000..d4682fe975 --- /dev/null +++ b/test/js/node/test/sequential/test-vm-timeout-rethrow.js @@ -0,0 +1,44 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'child') { + const code = 'while(true);'; + + const ctx = vm.createContext(); + + vm.runInContext(code, ctx, { timeout: 1 }); +} else { + const proc = spawn(process.execPath, process.argv.slice(1).concat('child')); + let err = ''; + proc.stderr.on('data', function(data) { + err += data; + }); + + process.on('exit', function() { + assert.match(err, /Script execution timed out after 1ms/); + }); +} diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index e161025ce7..3823c677bc 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -1522,6 +1522,7 @@ test/js/node/test/parallel/test-vm-module-errors.js test/js/node/test/parallel/test-vm-module-import-meta.js test/js/node/test/parallel/test-vm-module-link.js test/js/node/test/parallel/test-vm-module-reevaluate.js +test/js/node/test/parallel/test-vm-module-referrer-realm.mjs test/js/node/test/parallel/test-vm-module-synthetic.js test/js/node/test/parallel/test-vm-new-script-context.js test/js/node/test/parallel/test-vm-new-script-new-context.js From c2311ed06caf2bf4144d24616a3fff394a8c36c2 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 7 Jul 2025 22:36:39 -0700 Subject: [PATCH 134/147] fix(http2) avoid sending RST multiple times per stream (#20872) --- src/bun.js/api/bun/h2_frame_parser.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 7d6ad02b8c..d022fff6f7 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -1329,6 +1329,9 @@ pub const H2FrameParser = struct { pub fn endStream(this: *H2FrameParser, stream: *Stream, rstCode: ErrorCode) void { log("HTTP_FRAME_RST_STREAM id: {} code: {}", .{ stream.id, @intFromEnum(rstCode) }); + if (stream.state == .CLOSED) { + return; + } var buffer: [FrameHeader.byteSize + 4]u8 = undefined; @memset(&buffer, 0); var writerStream = std.io.fixedBufferStream(&buffer); From d4a52f77c740b242eafa4e26aa5933e213f41740 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 7 Jul 2025 23:06:27 -0700 Subject: [PATCH 135/147] Move `ReadableStream#{text,bytes,json,blob}` to global (#20879) Co-authored-by: Jarred Sumner --- packages/bun-types/bun.d.ts | 35 ++++++++----------- packages/bun-types/deprecated.d.ts | 6 ++++ packages/bun-types/overrides.d.ts | 21 +++++++++++ test/integration/bun-types/bun-types.test.ts | 8 +++-- test/integration/bun-types/fixture/spawn.ts | 6 ++-- test/integration/bun-types/fixture/streams.ts | 7 ++++ 6 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 9c3dc25b00..a92f3982a3 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -347,17 +347,6 @@ declare module "bun" { type WorkerType = "classic" | "module"; - /** - * This type-only interface is identical at runtime to {@link ReadableStream}, - * and exists to document types that do not exist yet in libdom. - */ - interface BunReadableStream extends ReadableStream { - text(): Promise; - bytes(): Promise; - json(): Promise; - blob(): Promise; - } - interface AbstractWorker { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorker/error_event) */ onerror: ((this: AbstractWorker, ev: ErrorEvent) => any) | null; @@ -864,6 +853,8 @@ declare module "bun" { * * @param stream The stream to consume. * @returns A promise that resolves with the concatenated chunks or the concatenated chunks as a {@link Uint8Array}. + * + * @deprecated Use {@link ReadableStream.bytes} */ function readableStreamToBytes( stream: ReadableStream, @@ -876,6 +867,8 @@ declare module "bun" { * * @param stream The stream to consume. * @returns A promise that resolves with the concatenated chunks as a {@link Blob}. + * + * @deprecated Use {@link ReadableStream.blob} */ function readableStreamToBlob(stream: ReadableStream): Promise; @@ -918,6 +911,8 @@ declare module "bun" { * * @param stream The stream to consume. * @returns A promise that resolves with the concatenated chunks as a {@link String}. + * + * @deprecated Use {@link ReadableStream.text} */ function readableStreamToText(stream: ReadableStream): Promise; @@ -928,6 +923,8 @@ declare module "bun" { * * @param stream The stream to consume. * @returns A promise that resolves with the concatenated chunks as a {@link String}. + * + * @deprecated Use {@link ReadableStream.json} */ function readableStreamToJSON(stream: ReadableStream): Promise; @@ -1254,9 +1251,9 @@ declare module "bun" { */ writer(options?: { highWaterMark?: number }): FileSink; - readonly readable: BunReadableStream; - - // TODO: writable: WritableStream; + // TODO + // readonly readable: ReadableStream; + // readonly writable: WritableStream; /** * A UNIX timestamp indicating when the file was last modified. @@ -7008,7 +7005,7 @@ declare module "bun" { * * For stdout and stdin you may pass: * - * - `"pipe"`, `undefined`: The process will have a {@link BunReadableStream} for standard output/error + * - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error * - `"ignore"`, `null`: The process will have no standard output/error * - `"inherit"`: The process will inherit the standard output/error of the current process * - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented. @@ -7034,7 +7031,7 @@ declare module "bun" { /** * The file descriptor for the standard output. It may be: * - * - `"pipe"`, `undefined`: The process will have a {@link BunReadableStream} for standard output/error + * - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error * - `"ignore"`, `null`: The process will have no standard output/error * - `"inherit"`: The process will inherit the standard output/error of the current process * - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented. @@ -7046,7 +7043,7 @@ declare module "bun" { /** * The file descriptor for the standard error. It may be: * - * - `"pipe"`, `undefined`: The process will have a {@link BunReadableStream} for standard output/error + * - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error * - `"ignore"`, `null`: The process will have no standard output/error * - `"inherit"`: The process will inherit the standard output/error of the current process * - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented. @@ -7206,10 +7203,8 @@ declare module "bun" { maxBuffer?: number; } - type ReadableIO = ReadableStream | number | undefined; - type ReadableToIO = X extends "pipe" | undefined - ? BunReadableStream + ? ReadableStream : X extends BunFile | ArrayBufferView | number ? number : undefined; diff --git a/packages/bun-types/deprecated.d.ts b/packages/bun-types/deprecated.d.ts index 4fa3294a26..c1ce3138e4 100644 --- a/packages/bun-types/deprecated.d.ts +++ b/packages/bun-types/deprecated.d.ts @@ -30,6 +30,7 @@ declare module "bun" { * @deprecated Renamed to `ErrorLike` */ type Errorlike = ErrorLike; + interface TLSOptions { /** * File path to a TLS key @@ -39,6 +40,7 @@ declare module "bun" { * @deprecated since v0.6.3 - Use `key: Bun.file(path)` instead. */ keyFile?: string; + /** * File path to a TLS certificate * @@ -47,6 +49,7 @@ declare module "bun" { * @deprecated since v0.6.3 - Use `cert: Bun.file(path)` instead. */ certFile?: string; + /** * File path to a .pem file for a custom root CA * @@ -54,6 +57,9 @@ declare module "bun" { */ caFile?: string; } + + /** @deprecated This type is unused in Bun's declarations and may be removed in the future */ + type ReadableIO = ReadableStream | number | undefined; } declare namespace NodeJS { diff --git a/packages/bun-types/overrides.d.ts b/packages/bun-types/overrides.d.ts index 27e4f9700b..f798e00f80 100644 --- a/packages/bun-types/overrides.d.ts +++ b/packages/bun-types/overrides.d.ts @@ -1,5 +1,26 @@ export {}; +declare module "stream/web" { + interface ReadableStream { + /** + * Consume a ReadableStream as text + */ + text(): Promise; + /** + * Consume a ReadableStream as a Uint8Array + */ + bytes(): Promise; + /** + * Consume a ReadableStream as JSON + */ + json(): Promise; + /** + * Consume a ReadableStream as a Blob + */ + blob(): Promise; + } +} + declare global { namespace NodeJS { interface ProcessEnv extends Bun.Env, ImportMetaEnv {} diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 4564d0051b..d8e355a007 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -29,7 +29,7 @@ beforeAll(async () => { await $` cd ${BUN_TYPES_PACKAGE_ROOT} bun install - + # temp package.json with @types/bun name and version cp package.json package.json.backup `; @@ -100,7 +100,7 @@ describe("@types/bun integration test", () => { ), ); - const p = await $` + const p = await $` cd ${FIXTURE_DIR} bun run check `; @@ -124,6 +124,10 @@ describe("@types/bun integration test", () => { "Overload 1 of 3, '(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number", // This line truncates because we've seen TypeScript emit differing messages in different environments `Type '"direct"' is not assignable to type '"bytes"'`, "error TS2339: Property 'write' does not exist on type 'ReadableByteStreamController'.", + "error TS2339: Property 'json' does not exist on type 'ReadableStream>'.", + "error TS2339: Property 'bytes' does not exist on type 'ReadableStream>'.", + "error TS2339: Property 'text' does not exist on type 'ReadableStream>'.", + "error TS2339: Property 'blob' does not exist on type 'ReadableStream>'.", "websocket.ts", `error TS2353: Object literal may only specify known properties, and 'protocols' does not exist in type 'string[]'.`, diff --git a/test/integration/bun-types/fixture/spawn.ts b/test/integration/bun-types/fixture/spawn.ts index c730b6ce09..6c9273df00 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -49,7 +49,7 @@ function depromise(_promise: Promise): T { tsd.expectType(proc.pid).is(); - tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stdout).is>(); tsd.expectType(proc.stderr).is(); tsd.expectType(proc.stdin).is(); } @@ -74,8 +74,8 @@ function depromise(_promise: Promise): T { tsd.expectType(proc.stdio[3]).is(); tsd.expectType(proc.stdin).is(); - tsd.expectType(proc.stdout).is(); - tsd.expectType(proc.stderr).is(); + tsd.expectType(proc.stdout).is>(); + tsd.expectType(proc.stderr).is>(); } { diff --git a/test/integration/bun-types/fixture/streams.ts b/test/integration/bun-types/fixture/streams.ts index 19020c7c23..dea6178a00 100644 --- a/test/integration/bun-types/fixture/streams.ts +++ b/test/integration/bun-types/fixture/streams.ts @@ -38,3 +38,10 @@ await writer.close(); for await (const chunk of uint8Transform.readable) { expectType(chunk).is>(); } + +declare const stream: ReadableStream; + +expectType(stream.json()).is>(); +expectType(stream.bytes()).is>(); +expectType(stream.text()).is>(); +expectType(stream.blob()).is>(); From 454316ffc33a0cc63f85d380998efb9d0f34adc8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 7 Jul 2025 23:08:12 -0700 Subject: [PATCH 136/147] Implement `"node:module"`'s `findSourceMap` and `SourceMap` class (#20863) Co-authored-by: Claude Co-authored-by: Claude Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- bench/snippets/source-map.js | 28 ++ cmake/sources/ZigGeneratedClassesSources.txt | 1 + cmake/sources/ZigSources.txt | 1 + src/StandaloneModuleGraph.zig | 1 + src/baby_list.zig | 4 + src/bake/DevServer.zig | 6 +- src/bun.js/SavedSourceMap.zig | 3 +- src/bun.js/VirtualMachine.zig | 2 +- src/bun.js/api/sourcemap.classes.ts | 32 ++ src/bun.js/bindings/ZigGlobalObject.h | 2 + .../bindings/generated_classes_list.zig | 1 + src/bun.js/modules/NodeModuleModule.cpp | 116 ++++-- src/codegen/class-definitions.ts | 18 +- src/codegen/generate-classes.ts | 78 +++- src/logger.zig | 4 +- src/sourcemap/CodeCoverage.zig | 5 +- src/sourcemap/JSSourceMap.zig | 306 ++++++++++++++ src/sourcemap/sourcemap.zig | 379 +++++++++++++++--- test/internal/ban-words.test.ts | 2 +- test/js/node/module/module-sourcemap.test.js | 27 ++ test/js/node/module/sourcemap.test.js | 177 ++++++++ test/no-validate-exceptions.txt | 1 + 22 files changed, 1089 insertions(+), 105 deletions(-) create mode 100644 bench/snippets/source-map.js create mode 100644 src/bun.js/api/sourcemap.classes.ts create mode 100644 src/sourcemap/JSSourceMap.zig create mode 100644 test/js/node/module/module-sourcemap.test.js create mode 100644 test/js/node/module/sourcemap.test.js diff --git a/bench/snippets/source-map.js b/bench/snippets/source-map.js new file mode 100644 index 0000000000..0d41bebd41 --- /dev/null +++ b/bench/snippets/source-map.js @@ -0,0 +1,28 @@ +import { SourceMap } from "node:module"; +import { readFileSync } from "node:fs"; +import { bench, run } from "../runner.mjs"; +const json = JSON.parse(readFileSync(process.argv.at(-1), "utf-8")); + +bench("new SourceMap(json)", () => { + return new SourceMap(json); +}); + +const map = new SourceMap(json); + +const toRotate = []; +for (let j = 0; j < 10000; j++) { + if (map.findEntry(0, j).generatedColumn) { + toRotate.push(j); + if (toRotate.length > 5) break; + } +} +let i = 0; +bench("findEntry (match)", () => { + return map.findEntry(0, toRotate[i++ % 3]).generatedColumn; +}); + +bench("findEntry (no match)", () => { + return map.findEntry(0, 9999).generatedColumn; +}); + +await run(); diff --git a/cmake/sources/ZigGeneratedClassesSources.txt b/cmake/sources/ZigGeneratedClassesSources.txt index cc657aebeb..116f1cc26d 100644 --- a/cmake/sources/ZigGeneratedClassesSources.txt +++ b/cmake/sources/ZigGeneratedClassesSources.txt @@ -14,6 +14,7 @@ src/bun.js/api/server.classes.ts src/bun.js/api/Shell.classes.ts src/bun.js/api/ShellArgs.classes.ts src/bun.js/api/sockets.classes.ts +src/bun.js/api/sourcemap.classes.ts src/bun.js/api/streams.classes.ts src/bun.js/api/valkey.classes.ts src/bun.js/api/zlib.classes.ts diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 08d07b7249..1060e7fac6 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -729,6 +729,7 @@ src/shell/subproc.zig src/shell/util.zig src/shell/Yield.zig src/sourcemap/CodeCoverage.zig +src/sourcemap/JSSourceMap.zig src/sourcemap/LineOffsetTable.zig src/sourcemap/sourcemap.zig src/sourcemap/VLQ.zig diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 478a6b563e..fc5530aabc 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -237,6 +237,7 @@ pub const StandaloneModuleGraph = struct { null, std.math.maxInt(i32), std.math.maxInt(i32), + .{}, )) { .success => |x| x, .fail => { diff --git a/src/baby_list.zig b/src/baby_list.zig index f7a5272e5a..539c548a47 100644 --- a/src/baby_list.zig +++ b/src/baby_list.zig @@ -417,6 +417,10 @@ pub fn BabyList(comptime Type: type) type { @as([*]align(1) Int, @ptrCast(this.ptr[this.len .. this.len + @sizeOf(Int)]))[0] = int; this.len += @sizeOf(Int); } + + pub fn memoryCost(self: *const @This()) usize { + return self.cap; + } }; } diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 15a39c7b8a..9bdc249913 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -8040,6 +8040,7 @@ pub const SourceMapStore = struct { null, @intCast(entry.paths.len), 0, // unused + .{}, )) { .fail => |fail| { Output.debugWarn("Failed to re-parse source map: {s}", .{fail.msg}); @@ -8199,7 +8200,7 @@ const ErrorReportRequest = struct { const result: *const SourceMapStore.GetResult = &(gop.value_ptr.* orelse continue); // When before the first generated line, remap to the HMR runtime - const generated_mappings = result.mappings.items(.generated); + const generated_mappings = result.mappings.generated(); if (frame.position.line.oneBased() < generated_mappings[1].lines) { frame.source_url = .init(runtime_name); // matches value in source map frame.position = .invalid; @@ -8207,8 +8208,7 @@ const ErrorReportRequest = struct { } // Remap the frame - const remapped = SourceMap.Mapping.find( - result.mappings, + const remapped = result.mappings.find( frame.position.line.oneBased(), frame.position.column.zeroBased(), ); diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index 2784c08369..01e6286785 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -50,6 +50,7 @@ pub const SavedMappings = struct { @as(usize, @bitCast(this.data[8..16].*)), 1, @as(usize, @bitCast(this.data[16..24].*)), + .{}, ); switch (result) { .fail => |fail| { @@ -310,7 +311,7 @@ pub fn resolveMapping( const map = parse.map orelse return null; const mapping = parse.mapping orelse - SourceMap.Mapping.find(map.mappings, line, column) orelse + map.mappings.find(line, column) orelse return null; return .{ diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index a28633e557..adad143990 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -3408,7 +3408,7 @@ pub fn resolveSourceMapping( this.source_mappings.putValue(path, SavedSourceMap.Value.init(map)) catch bun.outOfMemory(); - const mapping = SourceMap.Mapping.find(map.mappings, line, column) orelse + const mapping = map.mappings.find(line, column) orelse return null; return .{ diff --git a/src/bun.js/api/sourcemap.classes.ts b/src/bun.js/api/sourcemap.classes.ts new file mode 100644 index 0000000000..9a4ebd5201 --- /dev/null +++ b/src/bun.js/api/sourcemap.classes.ts @@ -0,0 +1,32 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "SourceMap", + JSType: "0b11101110", + proto: { + findOrigin: { + fn: "findOrigin", + length: 2, + }, + findEntry: { + fn: "findEntry", + length: 2, + }, + payload: { + getter: "getPayload", + cache: true, + }, + lineLengths: { + getter: "getLineLengths", + cache: true, + }, + }, + finalize: true, + construct: true, + constructNeedsThis: true, + memoryCost: true, + estimatedSize: true, + structuredClone: false, + }), +]; diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 9420108724..b80fefee49 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -462,6 +462,8 @@ public: V(public, LazyPropertyOfGlobalObject, m_modulePrototypeUnderscoreCompileFunction) \ V(public, LazyPropertyOfGlobalObject, m_commonJSRequireESMFromHijackedExtensionFunction) \ V(public, LazyPropertyOfGlobalObject, m_nodeModuleConstructor) \ + V(public, LazyPropertyOfGlobalObject, m_nodeModuleSourceMapEntryStructure) \ + V(public, LazyPropertyOfGlobalObject, m_nodeModuleSourceMapOriginStructure) \ \ V(public, WriteBarrier, m_nextTickQueue) \ \ diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 904da43346..bfaf43245b 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -89,4 +89,5 @@ pub const Classes = struct { pub const RedisClient = api.Valkey; pub const BlockList = api.BlockList; pub const NativeZstd = api.NativeZstd; + pub const SourceMap = bun.sourcemap.JSSourceMap; }; diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index e26286c65e..ffafec3306 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -20,21 +20,22 @@ #include "ErrorCode.h" #include "GeneratedNodeModuleModule.h" +#include "ZigGeneratedClasses.h" namespace Bun { using namespace JSC; +BUN_DECLARE_HOST_FUNCTION(Bun__JSSourceMap__find); + BUN_DECLARE_HOST_FUNCTION(Resolver__nodeModulePathsForJS); JSC_DECLARE_HOST_FUNCTION(jsFunctionDebugNoop); JSC_DECLARE_HOST_FUNCTION(jsFunctionFindPath); -JSC_DECLARE_HOST_FUNCTION(jsFunctionFindSourceMap); JSC_DECLARE_HOST_FUNCTION(jsFunctionIsBuiltinModule); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleModuleConstructor); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveFileName); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveLookupPaths); -JSC_DECLARE_HOST_FUNCTION(jsFunctionSourceMap); JSC_DECLARE_HOST_FUNCTION(jsFunctionSyncBuiltinExports); JSC_DECLARE_HOST_FUNCTION(jsFunctionWrap); @@ -287,13 +288,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire, scope, JSValue::encode(Bun::JSCommonJSModule::createBoundRequireFunction(vm, globalObject, val))); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionFindSourceMap, - (JSGlobalObject * globalObject, - CallFrame* callFrame)) -{ - return JSValue::encode(jsUndefined()); -} - JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -301,15 +295,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionSourceMap, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - throwException(globalObject, scope, - createError(globalObject, "Not implemented"_s)); - return {}; -} - JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveFileName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -585,10 +570,10 @@ static JSValue getPathCacheObject(VM& vm, JSObject* moduleObject) static JSValue getSourceMapFunction(VM& vm, JSObject* moduleObject) { auto* globalObject = defaultGlobalObject(moduleObject->globalObject()); - JSFunction* sourceMapFunction = JSFunction::create( - vm, globalObject, 1, "SourceMap"_s, jsFunctionSourceMap, - ImplementationVisibility::Public, NoIntrinsic, jsFunctionSourceMap); - return sourceMapFunction; + auto* zigGlobalObject = jsCast(globalObject); + + // Return the actual SourceMap constructor from code generation + return zigGlobalObject->JSSourceMapConstructor(); } static JSValue getBuiltinModulesObject(VM& vm, JSObject* moduleObject) @@ -847,7 +832,7 @@ builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 enableCompileCache jsFunctionEnableCompileCache Function 0 -findSourceMap jsFunctionFindSourceMap Function 0 +findSourceMap Bun__JSSourceMap__find Function 1 getCompileCacheDir jsFunctionGetCompileCacheDir Function 0 globalPaths getGlobalPathsObject PropertyCallback isBuiltin jsFunctionIsBuiltinModule Function 1 @@ -918,6 +903,82 @@ const JSC::ClassInfo JSModuleConstructor::s_info = { CREATE_METHOD_TABLE(JSModuleConstructor) }; +static JSC::Structure* createNodeModuleSourceMapEntryStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 6); + PropertyOffset offset; + + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "generatedLine"), 0, offset); + RELEASE_ASSERT(offset == 0); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "generatedColumn"), 0, offset); + RELEASE_ASSERT(offset == 1); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalLine"), 0, offset); + RELEASE_ASSERT(offset == 2); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalColumn"), 0, offset); + RELEASE_ASSERT(offset == 3); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalSource"), 0, offset); + RELEASE_ASSERT(offset == 4); + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->name, 0, offset); + RELEASE_ASSERT(offset == 5); + + return structure; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeModuleSourceMapEntryObject( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue encodedGeneratedLine, + JSC::EncodedJSValue encodedGeneratedColumn, + JSC::EncodedJSValue encodedOriginalLine, + JSC::EncodedJSValue encodedOriginalColumn, + JSC::EncodedJSValue encodedOriginalSource, + JSC::EncodedJSValue encodedName) +{ + auto& vm = globalObject->vm(); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSObject* object = JSC::constructEmptyObject(vm, zigGlobalObject->m_nodeModuleSourceMapEntryStructure.getInitializedOnMainThread(zigGlobalObject)); + object->putDirectOffset(vm, 0, JSC::JSValue::decode(encodedGeneratedLine)); + object->putDirectOffset(vm, 1, JSC::JSValue::decode(encodedGeneratedColumn)); + object->putDirectOffset(vm, 2, JSC::JSValue::decode(encodedOriginalLine)); + object->putDirectOffset(vm, 3, JSC::JSValue::decode(encodedOriginalColumn)); + object->putDirectOffset(vm, 4, JSC::JSValue::decode(encodedOriginalSource)); + object->putDirectOffset(vm, 5, JSC::JSValue::decode(encodedName)); + return JSValue::encode(object); +} + +static JSC::Structure* createNodeModuleSourceMapOriginStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 4); + PropertyOffset offset; + + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->name, 0, offset); + RELEASE_ASSERT(offset == 0); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "line"), 0, offset); + RELEASE_ASSERT(offset == 1); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "column"), 0, offset); + RELEASE_ASSERT(offset == 2); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "fileName"), 0, offset); + RELEASE_ASSERT(offset == 3); + + return structure; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeModuleSourceMapOriginObject( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue encodedName, + JSC::EncodedJSValue encodedLine, + JSC::EncodedJSValue encodedColumn, + JSC::EncodedJSValue encodedSource) +{ + auto& vm = globalObject->vm(); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSObject* object = JSC::constructEmptyObject(vm, zigGlobalObject->m_nodeModuleSourceMapOriginStructure.getInitializedOnMainThread(zigGlobalObject)); + object->putDirectOffset(vm, 0, JSC::JSValue::decode(encodedName)); + object->putDirectOffset(vm, 1, JSC::JSValue::decode(encodedLine)); + object->putDirectOffset(vm, 2, JSC::JSValue::decode(encodedColumn)); + object->putDirectOffset(vm, 3, JSC::JSValue::decode(encodedSource)); + return JSValue::encode(object); +} + void addNodeModuleConstructorProperties(JSC::VM& vm, Zig::GlobalObject* globalObject) { @@ -928,6 +989,15 @@ void addNodeModuleConstructorProperties(JSC::VM& vm, init.set(moduleConstructor); }); + globalObject->m_nodeModuleSourceMapEntryStructure.initLater( + [](const Zig::GlobalObject::Initializer& init) { + init.set(createNodeModuleSourceMapEntryStructure(init.vm, init.owner)); + }); + globalObject->m_nodeModuleSourceMapOriginStructure.initLater( + [](const Zig::GlobalObject::Initializer& init) { + init.set(createNodeModuleSourceMapOriginStructure(init.vm, init.owner)); + }); + globalObject->m_moduleRunMainFunction.initLater( [](const Zig::GlobalObject::Initializer& init) { JSFunction* runMainFunction = JSFunction::create( diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index a4736ee949..ab123f9d5f 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -92,9 +92,21 @@ export class ClassDefinition { */ name: string; /** - * Class constructor is newable. + * Class constructor is newable. Called before the JSValue corresponding to + * the object is created. Throwing an exception prevents the object from being + * created. */ construct?: boolean; + + /** + * Class constructor needs `this` value. + * + * Makes the code generator call the Zig constructor function **after** the + * JSValue is instantiated. Only use this if you must, as it probably isn't + * good for GC since it means if the constructor throws the GC will have to + * clean up the object that never reached JS. + */ + constructNeedsThis?: boolean; /** * Class constructor is callable. In JS, ES6 class constructors are not * callable. @@ -168,10 +180,6 @@ export class ClassDefinition { final?: boolean; - // Do not try to track the `this` value in the constructor automatically. - // That is a memory leak. - wantsThis?: never; - /** * Class has an `estimatedSize` function that reports external allocations to GC. * Called from any thread. diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 1399a5cd46..08b824b674 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -406,10 +406,17 @@ function generatePrototype(typeName, obj) { var staticPrototypeValues = ""; if (obj.construct) { - externs += ` + if (obj.constructNeedsThis) { + externs += ` +extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "construct")}(JSC::JSGlobalObject*, JSC::CallFrame*, JSC::EncodedJSValue); +JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); +`; + } else { + externs += ` extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "construct")}(JSC::JSGlobalObject*, JSC::CallFrame*); JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); `; + } } if (obj.structuredClone) { @@ -622,7 +629,8 @@ function generateConstructorImpl(typeName, obj: ClassDefinition) { externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "estimatedSize")}(void* ptr);` + "\n"; } - return ` + return ( + ` ${renderStaticDecls(classSymbolName, typeName, fields, obj.supportsObjectCreate || false)} ${hashTable} @@ -635,14 +643,14 @@ void ${name}::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, ${protot } ${name}::${name}(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure, ${ - obj.call ? classSymbolName(typeName, "call") : "call" - }, construct) { + obj.call ? classSymbolName(typeName, "call") : "call" + }, construct) { } ${name}* ${name}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, ${prototypeName( - typeName, - )}* prototype) { + typeName, + )}* prototype) { ${name}* ptr = new (NotNull, JSC::allocateCell<${name}>(vm)) ${name}(vm, structure); ptr->finishCreation(vm, globalObject, prototype); return ptr; @@ -653,6 +661,10 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::call(JSC::JSGlobalObject* Zig::GlobalObject *globalObject = reinterpret_cast(lexicalGlobalObject); JSC::VM &vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + +${ + !obj.constructNeedsThis + ? ` void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame); if (!ptr || scope.exception()) [[unlikely]] { @@ -661,6 +673,21 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::call(JSC::JSGlobalObject* Structure* structure = globalObject->${className(typeName)}Structure(); ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); +` + : ` + Structure* structure = globalObject->${className(typeName)}Structure(); + ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, nullptr); + + void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame, JSValue::encode(instance)); + if (scope.exception()) [[unlikely]] { + ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new ${typeName}() allocated memory without checking for exceptions."); + return JSValue::encode(JSC::jsUndefined()); + } + + instance->m_ctx = ptr; +` +} + RETURN_IF_EXCEPTION(scope, {}); ${ obj.estimatedSize @@ -694,7 +721,10 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj functionGlobalObject->${className(typeName)}Structure() ); } - + +` + + (!obj.constructNeedsThis + ? ` void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame); if (scope.exception()) [[unlikely]] { @@ -704,6 +734,19 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj ASSERT_WITH_MESSAGE(ptr, "Incorrect exception handling: new ${typeName} returned a null pointer, indicating an exception - but did not throw an exception."); ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); +` + : ` + ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, nullptr); + + void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame, JSValue::encode(instance)); + if (scope.exception()) [[unlikely]] { + ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new ${typeName}() allocated memory without checking for exceptions."); + return JSValue::encode(JSC::jsUndefined()); + } + + instance->m_ctx = ptr; + `) + + ` ${ obj.estimatedSize ? ` @@ -728,7 +771,8 @@ ${ } - `; + ` + ); } function renderCachedFieldsHeader(typeName, klass, proto, values) { @@ -1788,6 +1832,7 @@ function generateZig( proto = {}, own = {}, construct, + constructNeedsThis = false, finalize, noConstructor = false, overridesToJS = false, @@ -1913,7 +1958,21 @@ const JavaScriptCoreBindings = struct { if (construct && !noConstructor) { exports.set("construct", classSymbolName(typeName, "construct")); - output += ` + if (constructNeedsThis) { + output += ` + pub fn ${classSymbolName(typeName, "construct")}(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, thisValue: jsc.JSValue) callconv(jsc.conv) ?*anyopaque { + if (comptime Environment.enable_logs) log_zig_constructor("${typeName}", callFrame); + return @as(*${typeName}, ${typeName}.constructor(globalObject, callFrame, thisValue) catch |err| switch (err) { + error.JSError => return null, + error.OutOfMemory => { + globalObject.throwOutOfMemory() catch {}; + return null; + }, + }); + } + `; + } else { + output += ` pub fn ${classSymbolName(typeName, "construct")}(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) ?*anyopaque { if (comptime Environment.enable_logs) log_zig_constructor("${typeName}", callFrame); return @as(*${typeName}, ${typeName}.constructor(globalObject, callFrame) catch |err| switch (err) { @@ -1925,6 +1984,7 @@ const JavaScriptCoreBindings = struct { }); } `; + } } if (call) { diff --git a/src/logger.zig b/src/logger.zig index 6f03fc5a7e..8fdea0d6ca 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -64,8 +64,8 @@ pub const Kind = enum(u8) { pub const Loc = struct { start: i32 = -1, - pub inline fn toNullable(loc: *Loc) ?Loc { - return if (loc.start == -1) null else loc.*; + pub inline fn toNullable(loc: Loc) ?Loc { + return if (loc.start == -1) null else loc; } pub const toUsize = i; diff --git a/src/sourcemap/CodeCoverage.zig b/src/sourcemap/CodeCoverage.zig index 167f7d2ad7..d920ba42ac 100644 --- a/src/sourcemap/CodeCoverage.zig +++ b/src/sourcemap/CodeCoverage.zig @@ -1,7 +1,6 @@ const bun = @import("bun"); const std = @import("std"); const LineOffsetTable = bun.sourcemap.LineOffsetTable; -const SourceMap = bun.sourcemap; const Bitset = bun.bit_set.DynamicBitSetUnmanaged; const LinesHits = @import("../baby_list.zig").BabyList(u32); const Output = bun.Output; @@ -561,7 +560,7 @@ pub const ByteRangeMapping = struct { } const column_position = byte_offset -| line_start_byte_offset; - if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| { + if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |*point| { if (point.original.lines < 0) continue; const line: u32 = @as(u32, @intCast(point.original.lines)); @@ -605,7 +604,7 @@ pub const ByteRangeMapping = struct { const column_position = byte_offset -| line_start_byte_offset; - if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| { + if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |point| { if (point.original.lines < 0) continue; const line: u32 = @as(u32, @intCast(point.original.lines)); diff --git a/src/sourcemap/JSSourceMap.zig b/src/sourcemap/JSSourceMap.zig new file mode 100644 index 0000000000..99afae53e4 --- /dev/null +++ b/src/sourcemap/JSSourceMap.zig @@ -0,0 +1,306 @@ +/// This implements the JavaScript SourceMap class from Node.js. +/// +const JSSourceMap = @This(); + +sourcemap: *bun.sourcemap.ParsedSourceMap, +sources: []bun.String = &.{}, +names: []bun.String = &.{}, + +fn findSourceMap( + globalObject: *JSGlobalObject, + callFrame: *CallFrame, +) bun.JSError!JSValue { + const source_url_value = callFrame.argument(0); + if (!source_url_value.isString()) { + return .js_undefined; + } + + var source_url_string = try bun.String.fromJS(source_url_value, globalObject); + defer source_url_string.deref(); + + var source_url_slice = source_url_string.toUTF8(bun.default_allocator); + defer source_url_slice.deinit(); + + var source_url = source_url_slice.slice(); + if (bun.strings.hasPrefix(source_url, "node:") or bun.strings.hasPrefix(source_url, "bun:") or bun.strings.hasPrefix(source_url, "data:")) { + return .js_undefined; + } + + if (bun.strings.indexOf(source_url, "://")) |source_url_index| { + if (bun.strings.eqlComptime(source_url[0..source_url_index], "file")) { + const path = bun.JSC.URL.pathFromFileURL(source_url_string); + + if (path.tag == .Dead) { + return globalObject.ERR(.INVALID_URL, "Invalid URL: {s}", .{source_url}).throw(); + } + + // Replace the file:// URL with the absolute path. + source_url_string.deref(); + source_url_slice.deinit(); + source_url_string = path; + source_url_slice = path.toUTF8(bun.default_allocator); + source_url = source_url_slice.slice(); + } + } + + const vm = globalObject.bunVM(); + const source_map = vm.source_mappings.get(source_url) orelse return .js_undefined; + const fake_sources_array = bun.default_allocator.alloc(bun.String, 1) catch return globalObject.throwOutOfMemory(); + fake_sources_array[0] = source_url_string.dupeRef(); + + const this = bun.new(JSSourceMap, .{ + .sourcemap = source_map, + .sources = fake_sources_array, + .names = &.{}, + }); + + return this.toJS(globalObject); +} + +pub fn constructor( + globalObject: *JSGlobalObject, + callFrame: *CallFrame, + thisValue: JSValue, +) bun.JSError!*JSSourceMap { + const payload_arg = callFrame.argument(0); + const options_arg = callFrame.argument(1); + + try globalObject.validateObject("payload", payload_arg, .{}); + + var line_lengths: JSValue = .zero; + if (options_arg.isObject()) { + // Node doesn't check it further than this. + if (try options_arg.getIfPropertyExists(globalObject, "lineLengths")) |lengths| { + if (lengths.jsType().isArray()) { + line_lengths = lengths; + } + } + } + + // Parse the payload to create a proper sourcemap + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + + // Extract mappings string from payload + const mappings_value = try payload_arg.getStringish(globalObject, "mappings") orelse { + return globalObject.throwInvalidArguments("payload 'mappings' must be a string", .{}); + }; + defer mappings_value.deref(); + + const mappings_str = mappings_value.toUTF8(arena_allocator); + defer mappings_str.deinit(); + + var names = std.ArrayList(bun.String).init(bun.default_allocator); + errdefer { + for (names.items) |*str| { + str.deref(); + } + names.deinit(); + } + + var sources = std.ArrayList(bun.String).init(bun.default_allocator); + errdefer { + for (sources.items) |*str| { + str.deref(); + } + sources.deinit(); + } + + if (try payload_arg.getArray(globalObject, "sources")) |sources_value| { + var iter = try sources_value.arrayIterator(globalObject); + while (try iter.next()) |source| { + const source_str = try source.toBunString(globalObject); + try sources.append(source_str); + } + } + + if (try payload_arg.getArray(globalObject, "names")) |names_value| { + var iter = try names_value.arrayIterator(globalObject); + while (try iter.next()) |name| { + const name_str = try name.toBunString(globalObject); + try names.append(name_str); + } + } + + // Parse the VLQ mappings + const parse_result = bun.sourcemap.Mapping.parse( + bun.default_allocator, + mappings_str.slice(), + null, // estimated_mapping_count + @intCast(sources.items.len), // sources_count + std.math.maxInt(i32), + .{ .allow_names = true, .sort = true }, + ); + + const mapping_list = switch (parse_result) { + .success => |parsed| parsed, + .fail => |fail| { + if (fail.loc.toNullable()) |loc| { + return globalObject.throwValue(globalObject.createSyntaxErrorInstance("{s} at {d}", .{ fail.msg, loc.start })); + } + return globalObject.throwValue(globalObject.createSyntaxErrorInstance("{s}", .{fail.msg})); + }, + }; + + const source_map = bun.new(JSSourceMap, .{ + .sourcemap = bun.new(bun.sourcemap.ParsedSourceMap, mapping_list), + .sources = sources.items, + .names = names.items, + }); + + if (payload_arg != .zero) { + js.payloadSetCached(thisValue, globalObject, payload_arg); + } + if (line_lengths != .zero) { + js.lineLengthsSetCached(thisValue, globalObject, line_lengths); + } + + return source_map; +} + +pub fn memoryCost(this: *const JSSourceMap) usize { + return @sizeOf(JSSourceMap) + this.sources.len * @sizeOf(bun.String) + this.sourcemap.memoryCost(); +} + +pub fn estimatedSize(this: *JSSourceMap) usize { + return this.memoryCost(); +} + +// The cached value should handle this. +pub fn getPayload(_: *JSSourceMap, _: *JSGlobalObject) JSValue { + return .js_undefined; +} + +// The cached value should handle this. +pub fn getLineLengths(_: *JSSourceMap, _: *JSGlobalObject) JSValue { + return .js_undefined; +} + +fn getLineColumn(globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError![2]i32 { + const line_number_value = callFrame.argument(0); + const column_number_value = callFrame.argument(1); + + return .{ + // Node.js does no validations. + try line_number_value.coerce(i32, globalObject), + try column_number_value.coerce(i32, globalObject), + }; +} + +fn mappingNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue { + const name_index = mapping.nameIndex(); + if (name_index >= 0) { + if (this.sourcemap.mappings.getName(name_index)) |name| { + return bun.String.createUTF8ForJS(globalObject, name); + } else { + const index: usize = @intCast(name_index); + if (index < this.names.len) { + return this.names[index].toJS(globalObject); + } + } + } + return .js_undefined; +} + +fn sourceNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue { + const source_index = mapping.sourceIndex(); + if (source_index >= 0 and source_index < @as(i32, @intCast(this.sources.len))) { + return this.sources[@intCast(source_index)].toJS(globalObject); + } + + return .js_undefined; +} + +extern fn Bun__createNodeModuleSourceMapOriginObject( + globalObject: *JSGlobalObject, + name: JSValue, + line: JSValue, + column: JSValue, + source: JSValue, +) JSValue; + +extern fn Bun__createNodeModuleSourceMapEntryObject( + globalObject: *JSGlobalObject, + generatedLine: JSValue, + generatedColumn: JSValue, + originalLine: JSValue, + originalColumn: JSValue, + source: JSValue, + name: JSValue, +) JSValue; + +pub fn findOrigin(this: *JSSourceMap, globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + const line_number, const column_number = try getLineColumn(globalObject, callFrame); + + const mapping = this.sourcemap.mappings.find(line_number, column_number) orelse return JSC.JSValue.createEmptyObject(globalObject, 0); + const name = try mappingNameToJS(this, globalObject, &mapping); + const source = try sourceNameToJS(this, globalObject, &mapping); + return Bun__createNodeModuleSourceMapOriginObject( + globalObject, + name, + JSC.JSValue.jsNumber(mapping.originalLine()), + JSC.JSValue.jsNumber(mapping.originalColumn()), + source, + ); +} + +pub fn findEntry(this: *JSSourceMap, globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { + const line_number, const column_number = try getLineColumn(globalObject, callFrame); + + const mapping = this.sourcemap.mappings.find(line_number, column_number) orelse return JSC.JSValue.createEmptyObject(globalObject, 0); + + const name = try mappingNameToJS(this, globalObject, &mapping); + const source = try sourceNameToJS(this, globalObject, &mapping); + return Bun__createNodeModuleSourceMapEntryObject( + globalObject, + JSC.JSValue.jsNumber(mapping.generatedLine()), + JSC.JSValue.jsNumber(mapping.generatedColumn()), + JSC.JSValue.jsNumber(mapping.originalLine()), + JSC.JSValue.jsNumber(mapping.originalColumn()), + source, + name, + ); +} + +pub fn deinit(this: *JSSourceMap) void { + for (this.sources) |*str| { + str.deref(); + } + bun.default_allocator.free(this.sources); + + for (this.names) |*name| { + name.deref(); + } + + bun.default_allocator.free(this.names); + + this.sourcemap.deref(); + bun.destroy(this); +} + +pub fn finalize(this: *JSSourceMap) void { + this.deinit(); +} + +comptime { + const jsFunctionFindSourceMap = JSC.toJSHostFn(findSourceMap); + @export(&jsFunctionFindSourceMap, .{ .name = "Bun__JSSourceMap__find" }); +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const string = bun.string; + +const JSC = bun.JSC; +const CallFrame = JSC.CallFrame; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; + +pub const js = JSC.Codegen.JSSourceMap; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; +pub const toJS = js.toJS; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index bb4d969fe0..e664edf77d 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -10,7 +10,7 @@ const JSPrinter = bun.js_printer; const URL = bun.URL; const FileSystem = bun.fs.FileSystem; -const SourceMap = @This(); +pub const SourceMap = @This(); const debug = bun.Output.scoped(.SourceMap, false); /// Coordinates in source maps are stored using relative offsets for size @@ -42,7 +42,11 @@ pub const ParseUrlResultHint = union(enum) { /// In order to fetch source contents, you need to know the /// index, but you cant know the index until the mappings /// are loaded. So pass in line+col. - all: struct { line: i32, column: i32 }, + all: struct { + line: i32, + column: i32, + include_names: bool = false, + }, }; pub const ParseUrl = struct { @@ -179,19 +183,46 @@ pub fn parseJSON( }; const map = if (hint != .source_only) map: { - const map_data = switch (Mapping.parse( + var map_data = switch (Mapping.parse( alloc, mappings_str.data.e_string.slice(arena), null, std.math.maxInt(i32), std.math.maxInt(i32), + .{ .allow_names = hint == .all and hint.all.include_names, .sort = true }, )) { .success => |x| x, .fail => |fail| return fail.err, }; + if (hint == .all and hint.all.include_names and map_data.mappings.impl == .with_names) { + if (json.get("names")) |names| { + if (names.data == .e_array) { + var names_list = try std.ArrayListUnmanaged(bun.Semver.String).initCapacity(alloc, names.data.e_array.items.len); + errdefer names_list.deinit(alloc); + + var names_buffer = std.ArrayListUnmanaged(u8){}; + errdefer names_buffer.deinit(alloc); + + for (names.data.e_array.items.slice()) |*item| { + if (item.data != .e_string) { + return error.InvalidSourceMap; + } + + const str = try item.data.e_string.string(arena); + + names_list.appendAssumeCapacity(try bun.Semver.String.initAppendIfNeeded(alloc, &names_buffer, str)); + } + + map_data.mappings.names = names_list.items; + map_data.mappings.names_buffer = .fromList(names_buffer); + } + } + } + const ptr = bun.new(ParsedSourceMap, map_data); ptr.external_source_names = source_paths_slice.?; + break :map ptr; } else null; errdefer if (map) |m| m.deref(); @@ -199,7 +230,7 @@ pub fn parseJSON( const mapping, const source_index = switch (hint) { .source_only => |index| .{ null, index }, .all => |loc| brk: { - const mapping = Mapping.find(map.?.mappings, loc.line, loc.column) orelse + const mapping = map.?.mappings.find(loc.line, loc.column) orelse break :brk .{ null, null }; break :brk .{ mapping, std.math.cast(u32, mapping.source_index) }; }, @@ -234,8 +265,206 @@ pub const Mapping = struct { generated: LineColumnOffset, original: LineColumnOffset, source_index: i32, + name_index: i32 = -1, - pub const List = bun.MultiArrayList(Mapping); + /// Optimization: if we don't care about the "names" column, then don't store the names. + pub const MappingWithoutName = struct { + generated: LineColumnOffset, + original: LineColumnOffset, + source_index: i32, + + pub fn toNamed(this: *const MappingWithoutName) Mapping { + return .{ + .generated = this.generated, + .original = this.original, + .source_index = this.source_index, + .name_index = -1, + }; + } + }; + + pub const List = struct { + impl: Value = .{ .without_names = .{} }, + names: []const bun.Semver.String = &[_]bun.Semver.String{}, + names_buffer: bun.ByteList = .{}, + + pub const Value = union(enum) { + without_names: bun.MultiArrayList(MappingWithoutName), + with_names: bun.MultiArrayList(Mapping), + + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .without_names => |*list| list.memoryCost(), + .with_names => |*list| list.memoryCost(), + }; + } + + pub fn ensureTotalCapacity(this: *Value, allocator: std.mem.Allocator, count: usize) !void { + switch (this.*) { + inline else => |*list| try list.ensureTotalCapacity(allocator, count), + } + } + }; + + fn ensureWithNames(this: *List, allocator: std.mem.Allocator) !void { + if (this.impl == .with_names) return; + + var without_names = this.impl.without_names; + var with_names = bun.MultiArrayList(Mapping){}; + try with_names.ensureTotalCapacity(allocator, without_names.len); + defer without_names.deinit(allocator); + + with_names.len = without_names.len; + var old_slices = without_names.slice(); + var new_slices = with_names.slice(); + + @memcpy(new_slices.items(.generated), old_slices.items(.generated)); + @memcpy(new_slices.items(.original), old_slices.items(.original)); + @memcpy(new_slices.items(.source_index), old_slices.items(.source_index)); + @memset(new_slices.items(.name_index), -1); + + this.impl = .{ .with_names = with_names }; + } + + fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: i32, column: i32) ?usize { + var count = line_column_offsets.len; + var index: usize = 0; + while (count > 0) { + const step = count / 2; + const i: usize = index + step; + const mapping = line_column_offsets[i]; + if (mapping.lines < line or (mapping.lines == line and mapping.columns <= column)) { + index = i + 1; + count -|= step + 1; + } else { + count = step; + } + } + + if (index > 0) { + if (line_column_offsets[index - 1].lines == line) { + return index - 1; + } + } + + return null; + } + + pub fn findIndex(this: *const List, line: i32, column: i32) ?usize { + switch (this.impl) { + inline else => |*list| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + return i; + } + }, + } + + return null; + } + + const SortContext = struct { + generated: []const LineColumnOffset, + pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool { + const a = ctx.generated[a_index]; + const b = ctx.generated[b_index]; + + return a.lines < b.lines or (a.lines == b.lines and a.columns <= b.columns); + } + }; + + pub fn sort(this: *List) void { + switch (this.impl) { + .without_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + .with_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + } + } + + pub fn append(this: *List, allocator: std.mem.Allocator, mapping: *const Mapping) !void { + switch (this.impl) { + .without_names => |*list| { + try list.append(allocator, .{ + .generated = mapping.generated, + .original = mapping.original, + .source_index = mapping.source_index, + }); + }, + .with_names => |*list| { + try list.append(allocator, mapping.*); + }, + } + } + + pub fn find(this: *const List, line: i32, column: i32) ?Mapping { + switch (this.impl) { + inline else => |*list, tag| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + if (tag == .without_names) { + return list.get(i).toNamed(); + } else { + return list.get(i); + } + } + }, + } + + return null; + } + pub fn generated(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.generated), + }; + } + + pub fn original(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.original), + }; + } + + pub fn sourceIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.source_index), + }; + } + + pub fn nameIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.name_index), + }; + } + + pub fn deinit(self: *List, allocator: std.mem.Allocator) void { + switch (self.impl) { + inline else => |*list| list.deinit(allocator), + } + + self.names_buffer.deinitWithAllocator(allocator); + allocator.free(self.names); + } + + pub fn getName(this: *List, index: i32) ?[]const u8 { + if (index < 0) return null; + const i: usize = @intCast(index); + + if (i >= this.names.len) return null; + + if (this.impl == .with_names) { + const str: *const bun.Semver.String = &this.names[i]; + return str.slice(this.names_buffer.slice()); + } + + return null; + } + + pub fn memoryCost(this: *const List) usize { + return this.impl.memoryCost() + this.names_buffer.memoryCost() + + (this.names.len * @sizeOf(bun.Semver.String)); + } + + pub fn ensureTotalCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void { + try this.impl.ensureTotalCapacity(allocator, count); + } + }; pub const Lookup = struct { mapping: Mapping, @@ -244,6 +473,8 @@ pub const Mapping = struct { /// use `getSourceCode` to access this as a Slice prefetched_source_code: ?[]const u8, + name: ?[]const u8 = null, + /// This creates a bun.String if the source remap *changes* the source url, /// which is only possible if the executed file differs from the source file: /// @@ -336,58 +567,28 @@ pub const Mapping = struct { } }; - pub inline fn generatedLine(mapping: Mapping) i32 { + pub inline fn generatedLine(mapping: *const Mapping) i32 { return mapping.generated.lines; } - pub inline fn generatedColumn(mapping: Mapping) i32 { + pub inline fn generatedColumn(mapping: *const Mapping) i32 { return mapping.generated.columns; } - pub inline fn sourceIndex(mapping: Mapping) i32 { + pub inline fn sourceIndex(mapping: *const Mapping) i32 { return mapping.source_index; } - pub inline fn originalLine(mapping: Mapping) i32 { + pub inline fn originalLine(mapping: *const Mapping) i32 { return mapping.original.lines; } - pub inline fn originalColumn(mapping: Mapping) i32 { + pub inline fn originalColumn(mapping: *const Mapping) i32 { return mapping.original.columns; } - pub fn find(mappings: Mapping.List, line: i32, column: i32) ?Mapping { - if (findIndex(mappings, line, column)) |i| { - return mappings.get(i); - } - - return null; - } - - pub fn findIndex(mappings: Mapping.List, line: i32, column: i32) ?usize { - const generated = mappings.items(.generated); - - var count = generated.len; - var index: usize = 0; - while (count > 0) { - const step = count / 2; - const i: usize = index + step; - const mapping = generated[i]; - if (mapping.lines < line or (mapping.lines == line and mapping.columns <= column)) { - index = i + 1; - count -|= step + 1; - } else { - count = step; - } - } - - if (index > 0) { - if (generated[index - 1].lines == line) { - return index - 1; - } - } - - return null; + pub inline fn nameIndex(mapping: *const Mapping) i32 { + return mapping.name_index; } pub fn parse( @@ -396,19 +597,35 @@ pub const Mapping = struct { estimated_mapping_count: ?usize, sources_count: i32, input_line_count: usize, + options: struct { + allow_names: bool = false, + sort: bool = false, + }, ) ParseResult { debug("parse mappings ({d} bytes)", .{bytes.len}); var mapping = Mapping.List{}; + errdefer mapping.deinit(allocator); + if (estimated_mapping_count) |count| { - mapping.ensureTotalCapacity(allocator, count) catch unreachable; + mapping.ensureTotalCapacity(allocator, count) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{}, + }, + }; + }; } var generated = LineColumnOffset{ .lines = 0, .columns = 0 }; var original = LineColumnOffset{ .lines = 0, .columns = 0 }; + var name_index: i32 = 0; var source_index: i32 = 0; var needs_sort = false; var remain = bytes; + var has_names = false; while (remain.len > 0) { if (remain[0] == ';') { generated.columns = 0; @@ -558,28 +775,70 @@ pub const Mapping = struct { if (remain.len > 0) { switch (remain[0]) { ',' => { + // 4 column, but there's more on this line. remain = remain[1..]; }, + // 4 column, and there's no more on this line. ';' => {}, + + // 5th column: the name else => |c| { - return .{ - .fail = .{ - .msg = "Invalid character after mapping", - .err = error.InvalidSourceMap, - .value = @as(i32, @intCast(c)), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; + // Read the name index + const name_index_delta = decodeVLQ(remain, 0); + if (name_index_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Invalid name index delta", + .err = error.InvalidNameIndexDelta, + .value = @intCast(c), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[name_index_delta.start..]; + + if (options.allow_names) { + name_index += name_index_delta.value; + if (!has_names) { + mapping.ensureWithNames(allocator) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + }; + } + has_names = true; + } + + if (remain.len > 0) { + switch (remain[0]) { + // There's more on this line. + ',' => { + remain = remain[1..]; + }, + // That's the end of the line. + ';' => {}, + else => {}, + } + } }, } } - mapping.append(allocator, .{ + mapping.append(allocator, &.{ .generated = generated, .original = original, .source_index = source_index, + .name_index = name_index, }) catch bun.outOfMemory(); } + if (needs_sort and options.sort) { + mapping.sort(); + } + return .{ .success = .{ .ref_count = .init(), .mappings = mapping, @@ -622,6 +881,7 @@ pub const ParsedSourceMap = struct { input_line_count: usize = 0, mappings: Mapping.List = .{}, + /// If this is empty, this implies that the source code is a single file /// transpiled on-demand. If there are items, then it means this is a file /// loaded without transpilation but with external sources. This array @@ -710,16 +970,20 @@ pub const ParsedSourceMap = struct { return @ptrFromInt(this.underlying_provider.data); } - pub fn writeVLQs(map: ParsedSourceMap, writer: anytype) !void { + pub fn memoryCost(this: *const ParsedSourceMap) usize { + return @sizeOf(ParsedSourceMap) + this.mappings.memoryCost() + this.external_source_names.len * @sizeOf([]const u8); + } + + pub fn writeVLQs(map: *const ParsedSourceMap, writer: anytype) !void { var last_col: i32 = 0; var last_src: i32 = 0; var last_ol: i32 = 0; var last_oc: i32 = 0; var current_line: i32 = 0; for ( - map.mappings.items(.generated), - map.mappings.items(.original), - map.mappings.items(.source_index), + map.mappings.generated(), + map.mappings.original(), + map.mappings.sourceIndex(), 0.., ) |gen, orig, source_index, i| { if (current_line != gen.lines) { @@ -1056,7 +1320,7 @@ pub fn find( line: i32, column: i32, ) ?Mapping { - return Mapping.find(this.mapping, line, column); + return this.mapping.find(line, column); } pub const SourceMapShifts = struct { @@ -1671,6 +1935,7 @@ const assert = bun.assert; pub const coverage = @import("./CodeCoverage.zig"); pub const VLQ = @import("./VLQ.zig"); pub const LineOffsetTable = @import("./LineOffsetTable.zig"); +pub const JSSourceMap = @import("./JSSourceMap.zig"); const decodeVLQAssumeValid = VLQ.decodeAssumeValid; const decodeVLQ = VLQ.decode; diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index fccdb519b9..1a78466ae7 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 243, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1867 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1866 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 179 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 }, diff --git a/test/js/node/module/module-sourcemap.test.js b/test/js/node/module/module-sourcemap.test.js new file mode 100644 index 0000000000..dc847c01b2 --- /dev/null +++ b/test/js/node/module/module-sourcemap.test.js @@ -0,0 +1,27 @@ +const { test, expect } = require("bun:test"); + +test("SourceMap is available from node:module", () => { + const module = require("node:module"); + expect(module.SourceMap).toBeDefined(); + expect(typeof module.SourceMap).toBe("function"); +}); + +test("SourceMap from require('module') works", () => { + const module = require("module"); + expect(module.SourceMap).toBeDefined(); + expect(typeof module.SourceMap).toBe("function"); +}); + +test("Can create SourceMap instance from node:module", () => { + const { SourceMap } = require("node:module"); + const payload = { + version: 3, + sources: ["test.js"], + names: [], + mappings: "AAAA", + }; + + const sourceMap = new SourceMap(payload); + expect(sourceMap).toBeInstanceOf(SourceMap); + expect(sourceMap.payload).toBe(payload); +}); diff --git a/test/js/node/module/sourcemap.test.js b/test/js/node/module/sourcemap.test.js new file mode 100644 index 0000000000..d9ff675cdf --- /dev/null +++ b/test/js/node/module/sourcemap.test.js @@ -0,0 +1,177 @@ +const { test, expect } = require("bun:test"); +const { SourceMap } = require("node:module"); + +test("SourceMap class exists", () => { + expect(SourceMap).toBeDefined(); + expect(typeof SourceMap).toBe("function"); + expect(SourceMap.name).toBe("SourceMap"); +}); + +test("SourceMap constructor requires payload", () => { + expect(() => { + new SourceMap(); + }).toThrowErrorMatchingInlineSnapshot(`"The "payload" argument must be of type object. Received undefined"`); +}); + +test("SourceMap payload must be an object", () => { + expect(() => { + new SourceMap("not an object"); + }).toThrowErrorMatchingInlineSnapshot( + `"The "payload" argument must be of type object. Received type string ('not an object')"`, + ); +}); + +test("SourceMap instance has expected methods", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + expect(typeof sourceMap.findOrigin).toBe("function"); + expect(typeof sourceMap.findEntry).toBe("function"); + expect(sourceMap.findOrigin.length).toBe(2); + expect(sourceMap.findEntry.length).toBe(2); +}); + +test("SourceMap payload getter", () => { + const payload = { + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }; + const sourceMap = new SourceMap(payload); + + expect(sourceMap.payload).toBe(payload); +}); + +test("SourceMap lineLengths getter", () => { + const payload = { + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }; + const lineLengths = [10, 20, 30]; + const sourceMap = new SourceMap(payload, { lineLengths }); + + expect(sourceMap.lineLengths).toBe(lineLengths); +}); + +test("SourceMap lineLengths undefined when not provided", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + expect(sourceMap.lineLengths).toBeUndefined(); +}); +test("SourceMap findEntry returns mapping data", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + const result = sourceMap.findEntry(0, 0); + + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap findOrigin returns origin data", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + const result = sourceMap.findOrigin(0, 0); + + expect(result).toMatchInlineSnapshot(` + { + "column": 0, + "fileName": "test.js", + "line": 0, + "name": undefined, + } + `); +}); + +test("SourceMap with names returns name property correctly", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + names: ["myFunction", "myVariable"], + mappings: "AAAAA,CAACC", // Both segments reference names + }); + + const result = sourceMap.findEntry(0, 0); + const resultWithName = sourceMap.findEntry(0, 6); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": "myFunction", + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); + expect(resultWithName).toMatchInlineSnapshot(` + { + "generatedColumn": 1, + "generatedLine": 0, + "name": "myVariable", + "originalColumn": 1, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap without names has undefined name property", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + const result = sourceMap.findEntry(0, 0); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap with invalid name index has undefined name property", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAAA,CAACC", // Both segments reference names + }); + + const result = sourceMap.findEntry(0, 0); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 3823c677bc..e1a5bffacb 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -195,6 +195,7 @@ test/js/bun/spawn/spawn-stdin-readable-stream-sync.test.ts test/js/bun/spawn/spawn-stdin-readable-stream.test.ts test/js/bun/spawn/spawn-stream-serve.test.ts test/js/bun/spawn/spawn-streaming-stdout.test.ts +test/js/node/module/sourcemap.test.js test/js/bun/spawn/spawn-stress.test.ts test/js/bun/spawn/spawn.ipc.bun-node.test.ts test/js/bun/spawn/spawn.ipc.node-bun.test.ts From a63f09784e3eb3c77295e26460fb336b1c7b35f8 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 8 Jul 2025 15:45:24 -0800 Subject: [PATCH 137/147] .vscode/launch.json: delete unused configurations (#20901) --- .vscode/launch.json | 1005 ------------------------------------------- 1 file changed, 1005 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e3b39ae920..9cc2d04820 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,28 +33,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] --only", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "--only", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "1", - "BUN_DEBUG_jest": "1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, { "type": "lldb", "name": "Attach", @@ -69,150 +47,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "0", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "0", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] --watch", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "--watch", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] --hot", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "--hot", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?wait=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [file] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?break=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // bun run [file] { "type": "lldb", @@ -236,150 +70,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "0", - "BUN_DEBUG_IncrementalGraph": "1", - "BUN_DEBUG_Bake": "1", - "BUN_DEBUG_reload_file_list": "1", - "GOMAXPROCS": "1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "0", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --watch", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "--watch", "${file}"], - "cwd": "${fileDirname}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --hot", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "--hot", "${file}"], - "cwd": "${fileDirname}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "0", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?wait=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "0", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?break=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // bun test [...] { "type": "lldb", @@ -403,150 +93,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "0", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] --watch", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "--watch", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] --hot", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "--hot", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?wait=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [...] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_DEBUG_jest": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/?break=1", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // bun exec [...] { "type": "lldb", @@ -591,54 +137,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [*] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "0", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [*] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug", - "args": ["test"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - "BUN_INSPECT": "ws://localhost:0/", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, { "type": "lldb", "request": "launch", @@ -660,27 +158,6 @@ "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", }, }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [*] (ci)", - "program": "node", - "args": ["test/runner.node.mjs"], - "cwd": "${workspaceFolder}", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", - }, - "console": "internalConsole", - "sourceMap": { - // macOS - "/Users/runner/work/_temp/webkit-release/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/Users/runner/work/_temp/webkit-release/WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - // linux - "/webkitbuild/vendor/WebKit": "${workspaceFolder}/vendor/WebKit", - "/webkitbuild/.WTF/Headers": "${workspaceFolder}/vendor/WebKit/Source/WTF", - }, - }, // Windows: bun test [file] { "type": "cppvsdbg", @@ -707,149 +184,6 @@ }, ], }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test --only [file]", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "--only", "${file}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [file] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "0", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [file] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "0", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [file] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?wait=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [file] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${file}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?break=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // Windows: bun run [file] { "type": "cppvsdbg", @@ -897,91 +231,6 @@ }, ], }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun run [file] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["run", "${fileBasename}"], - "cwd": "${fileDirname}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_SYS", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun run [file] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["run", "${fileBasename}"], - "cwd": "${fileDirname}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?wait=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun run [file] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["run", "${fileBasename}"], - "cwd": "${fileDirname}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?break=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // Windows: bun test [...] { "type": "cppvsdbg", @@ -1008,174 +257,6 @@ }, ], }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "0", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] (verbose)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "0", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] --watch", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "--watch", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] --hot", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "--hot", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?wait=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [...] --inspect-brk", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test", "--timeout=3600000", "${input:testName}"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/?break=1", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, // Windows: bun exec [...] { "type": "cppvsdbg", @@ -1220,92 +301,6 @@ }, ], }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [*] (fast)", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "0", - }, - ], - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [*] --inspect", - "program": "${workspaceFolder}/build/debug/bun-debug.exe", - "args": ["test"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - { - "name": "BUN_INSPECT", - "value": "ws://localhost:0/", - }, - ], - "serverReadyAction": { - "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", - "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", - "action": "openExternally", - }, - }, - { - "type": "cppvsdbg", - "sourceFileMap": { - "D:\\a\\WebKit\\WebKit\\Source": "${workspaceFolder}\\src\\bun.js\\WebKit\\Source", - }, - "request": "launch", - "name": "Windows: bun test [*] (ci)", - "program": "node", - "args": ["test/runner.node.mjs"], - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "BUN_DEBUG_QUIET_LOGS", - "value": "1", - }, - { - "name": "BUN_DEBUG_jest", - "value": "1", - }, - { - "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2", - }, - ], - "console": "internalConsole", - // Don't pause when the GC runs while the debugger is open. - }, { "type": "bun", "name": "[JS] bun test [file]", From 36bedb0bbc4e9525094f337f0b3390a386f19a44 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 8 Jul 2025 20:23:25 -0800 Subject: [PATCH 138/147] js: fix async macros on windows (#20903) --- src/ast/Macro.zig | 2 ++ src/bun.js/event_loop.zig | 7 ++++++- test/bundler/transpiler/macro-test.test.ts | 5 +++++ test/bundler/transpiler/macro.ts | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ast/Macro.zig b/src/ast/Macro.zig index 1a7f06c363..546af4b768 100644 --- a/src/ast/Macro.zig +++ b/src/ast/Macro.zig @@ -118,6 +118,7 @@ pub const MacroContext = struct { } macro.vm.enableMacroMode(); defer macro.vm.disableMacroMode(); + macro.vm.eventLoop().ensureWaker(); const Wrapper = struct { args: std.meta.ArgsTuple(@TypeOf(Macro.Runner.run)), @@ -193,6 +194,7 @@ pub fn init( }; vm.enableMacroMode(); + vm.eventLoop().ensureWaker(); const loaded_result = try vm.loadMacroEntryPoint(input_specifier, function_name, specifier, hash); diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index e84d5b2934..d928e59b2f 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -314,7 +314,7 @@ pub fn tickConcurrentWithCount(this: *EventLoop) usize { return this.tasks.count - start_count; } -pub inline fn usocketsLoop(this: *const EventLoop) *uws.Loop { +pub fn usocketsLoop(this: *const EventLoop) *uws.Loop { if (comptime Environment.isWindows) { return this.uws_loop.?; } @@ -553,6 +553,11 @@ pub fn ensureWaker(this: *EventLoop) void { // _ = actual.addPostHandler(*JSC.EventLoop, this, JSC.EventLoop.afterUSocketsTick); // _ = actual.addPreHandler(*JSC.VM, this.virtual_machine.jsc, JSC.VM.drainMicrotasks); } + if (comptime Environment.isWindows) { + if (this.uws_loop == null) { + this.uws_loop = bun.uws.Loop.get(); + } + } bun.uws.Loop.get().internal_loop_data.setParentEventLoop(bun.JSC.EventLoopHandle.init(this)); } diff --git a/test/bundler/transpiler/macro-test.test.ts b/test/bundler/transpiler/macro-test.test.ts index 5eaa5d747c..bc5ea871f3 100644 --- a/test/bundler/transpiler/macro-test.test.ts +++ b/test/bundler/transpiler/macro-test.test.ts @@ -8,6 +8,7 @@ import defaultMacro, { identity, identity as identity1, identity as identity2, + ireturnapromise, } from "./macro.ts" assert { type: "macro" }; import * as macros from "./macro.ts" assert { type: "macro" }; @@ -124,3 +125,7 @@ test("namespace import", () => { // test("template string latin1", () => { // expect(identity(`©${""}`)).toBe("©"); // }); + +test("ireturnapromise", async () => { + expect(await ireturnapromise()).toEqual("aaa"); +}); diff --git a/test/bundler/transpiler/macro.ts b/test/bundler/transpiler/macro.ts index 430fab84ee..9da8d72c5a 100644 --- a/test/bundler/transpiler/macro.ts +++ b/test/bundler/transpiler/macro.ts @@ -17,3 +17,9 @@ export function addStringsUTF16(arg: string) { export default function() { return "defaultdefaultdefault"; } + +export async function ireturnapromise() { + const { promise, resolve } = Promise.withResolvers(); + setTimeout(() => resolve("aaa"), 100); + return promise; +} From f24e8cb98a5db32467a22b829373118fed77b119 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 9 Jul 2025 00:19:57 -0700 Subject: [PATCH 139/147] implement `"nodeLinker": "isolated"` in `bun install` (#20440) Co-authored-by: Jarred Sumner --- cmake/sources/ZigSources.txt | 9 + package.json | 2 +- src/Watcher.zig | 4 +- src/analytics/analytics_thread.zig | 2 + src/bake/DevServer.zig | 20 +- src/bun.js/api/BunObject.zig | 4 +- src/bun.js/api/bun/process.zig | 2 +- src/bun.js/api/server.zig | 8 +- src/bun.js/node/dir_iterator.zig | 51 +- src/bun.js/node/node_fs.zig | 43 +- src/bun.js/node/node_fs_stat_watcher.zig | 4 +- src/bun.js/node/node_fs_watcher.zig | 10 +- src/bun.js/node/types.zig | 8 +- src/bun.zig | 102 +- src/bundler/bundle_v2.zig | 4 +- src/cli/bunx_command.zig | 3 +- src/cli/create_command.zig | 6 +- src/cli/init_command.zig | 10 +- src/cli/link_command.zig | 20 +- src/cli/pack_command.zig | 12 +- src/cli/pm_trusted_command.zig | 50 +- src/cli/publish_command.zig | 2 +- src/cli/run_command.zig | 34 +- src/cli/unlink_command.zig | 19 +- src/fd.zig | 30 + src/feature_flags.zig | 3 +- src/fs.zig | 14 +- src/glob/GlobWalker.zig | 2 +- src/hive_array.zig | 3 +- src/identity_context.zig | 6 +- src/install/NetworkTask.zig | 4 +- src/install/PackageInstall.zig | 62 +- src/install/PackageInstaller.zig | 179 +-- src/install/PackageManager.zig | 222 +++- .../PackageManagerDirectories.zig | 135 +- .../PackageManager/PackageManagerEnqueue.zig | 18 +- .../PackageManagerLifecycle.zig | 126 +- .../PackageManager/install_with_manager.zig | 38 +- src/install/PackageManager/patchPackage.zig | 11 +- .../PackageManager/processDependencyList.zig | 4 +- src/install/PackageManager/runTasks.zig | 135 +- src/install/PackageManagerTask.zig | 96 +- src/install/bin.zig | 19 +- src/install/dependency.zig | 14 + src/install/hoisted_install.zig | 59 +- src/install/install.zig | 27 + src/install/isolated_install.zig | 985 ++++++++++++++ src/install/isolated_install/Hardlinker.zig | 128 ++ src/install/isolated_install/Installer.zig | 1139 +++++++++++++++++ src/install/isolated_install/Store.zig | 548 ++++++++ src/install/isolated_install/Symlinker.zig | 115 ++ src/install/lifecycle_script_runner.zig | 59 +- src/install/lockfile.zig | 73 +- src/install/lockfile/Package.zig | 19 + src/install/lockfile/Package/Scripts.zig | 147 +-- src/install/lockfile/Tree.zig | 233 ++-- src/install/lockfile/bun.lock.zig | 32 +- src/install/lockfile/bun.lockb.zig | 21 + src/install/patch_install.zig | 4 +- src/install/repository.zig | 53 +- src/install/resolution.zig | 35 +- src/macho.zig | 2 +- src/multi_array_list.zig | 4 +- src/paths.zig | 24 + src/paths/EnvPath.zig | 90 ++ src/paths/Path.zig | 808 ++++++++++++ src/paths/path_buffer_pool.zig | 34 + src/resolver/resolver.zig | 37 +- src/semver/SemverString.zig | 26 +- src/shell/builtin/ls.zig | 2 +- src/shell/builtin/rm.zig | 2 +- src/shell/builtin/which.zig | 8 +- src/shell/interpreter.zig | 18 +- src/shell/states/Cmd.zig | 4 +- src/sourcemap/sourcemap.zig | 4 +- src/string/paths.zig | 90 +- src/string_immutable.zig | 2 +- src/sys.zig | 260 ++-- src/walker_skippable.zig | 53 +- src/which.zig | 8 +- test/cli/install/bun-pack.test.ts | 2 +- test/cli/install/isolated-install.test.ts | 433 +++++++ .../packages/a-dep-b/a-dep-b-1.0.0.tgz | Bin 0 -> 165 bytes .../registry/packages/a-dep-b/package.json | 44 + .../packages/b-dep-a/b-dep-a-1.0.0.tgz | Bin 0 -> 165 bytes .../registry/packages/b-dep-a/package.json | 44 + .../diff-peer-1/diff-peer-1-1.0.0.tgz | Bin 0 -> 178 bytes .../packages/diff-peer-1/package.json | 42 + .../diff-peer-2/diff-peer-2-1.0.0.tgz | Bin 0 -> 180 bytes .../packages/diff-peer-2/package.json | 42 + .../packages/has-peer/has-peer-1.0.0.tgz | Bin 0 -> 173 bytes .../registry/packages/has-peer/package.json | 41 + .../packages/peer-no-deps/package.json | 74 ++ .../peer-no-deps/peer-no-deps-1.0.0.tgz | Bin 0 -> 145 bytes .../peer-no-deps/peer-no-deps-1.0.1.tgz | Bin 0 -> 145 bytes .../peer-no-deps/peer-no-deps-2.0.0.tgz | Bin 0 -> 145 bytes test/harness.ts | 6 +- test/internal/ban-words.test.ts | 8 +- test/no-validate-exceptions.txt | 1 + 99 files changed, 6255 insertions(+), 1185 deletions(-) create mode 100644 src/install/isolated_install.zig create mode 100644 src/install/isolated_install/Hardlinker.zig create mode 100644 src/install/isolated_install/Installer.zig create mode 100644 src/install/isolated_install/Store.zig create mode 100644 src/install/isolated_install/Symlinker.zig create mode 100644 src/paths.zig create mode 100644 src/paths/EnvPath.zig create mode 100644 src/paths/Path.zig create mode 100644 src/paths/path_buffer_pool.zig create mode 100644 test/cli/install/isolated-install.test.ts create mode 100644 test/cli/install/registry/packages/a-dep-b/a-dep-b-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/a-dep-b/package.json create mode 100644 test/cli/install/registry/packages/b-dep-a/b-dep-a-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/b-dep-a/package.json create mode 100644 test/cli/install/registry/packages/diff-peer-1/diff-peer-1-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/diff-peer-1/package.json create mode 100644 test/cli/install/registry/packages/diff-peer-2/diff-peer-2-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/diff-peer-2/package.json create mode 100644 test/cli/install/registry/packages/has-peer/has-peer-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/has-peer/package.json create mode 100644 test/cli/install/registry/packages/peer-no-deps/package.json create mode 100644 test/cli/install/registry/packages/peer-no-deps/peer-no-deps-1.0.0.tgz create mode 100644 test/cli/install/registry/packages/peer-no-deps/peer-no-deps-1.0.1.tgz create mode 100644 test/cli/install/registry/packages/peer-no-deps/peer-no-deps-2.0.0.tgz diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 1060e7fac6..2261399b49 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -562,6 +562,11 @@ src/install/hoisted_install.zig src/install/install_binding.zig src/install/install.zig src/install/integrity.zig +src/install/isolated_install.zig +src/install/isolated_install/Hardlinker.zig +src/install/isolated_install/Installer.zig +src/install/isolated_install/Store.zig +src/install/isolated_install/Symlinker.zig src/install/lifecycle_script_runner.zig src/install/lockfile.zig src/install/lockfile/Buffers.zig @@ -644,6 +649,10 @@ src/options.zig src/output.zig src/OutputFile.zig src/patch.zig +src/paths.zig +src/paths/EnvPath.zig +src/paths/path_buffer_pool.zig +src/paths/Path.zig src/perf.zig src/pool.zig src/Progress.zig diff --git a/package.json b/package.json index b6eb9de202..c5bcfc55a8 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "clang-tidy:diff": "bun run analysis --target clang-tidy-diff", "zig-format": "bun run analysis:no-llvm --target zig-format", "zig-format:check": "bun run analysis:no-llvm --target zig-format-check", - "prettier": "bunx prettier@latest --plugin=prettier-plugin-organize-imports --config .prettierrc --write scripts packages src docs 'test/**/*.{test,spec}.{ts,tsx,js,jsx,mts,mjs,cjs,cts}' '!test/**/*fixture*.*'", + "prettier": "bunx --bun prettier@latest --plugin=prettier-plugin-organize-imports --config .prettierrc --write scripts packages src docs 'test/**/*.{test,spec}.{ts,tsx,js,jsx,mts,mjs,cjs,cts}' '!test/**/*fixture*.*'", "node:test": "node ./scripts/runner.node.mjs --quiet --exec-path=$npm_execpath --node-tests ", "node:test:cp": "bun ./scripts/fetch-node-test.ts ", "clean:zig": "rm -rf build/debug/cache/zig build/debug/CMakeCache.txt 'build/debug/*.o' .zig-cache zig-out || true", diff --git a/src/Watcher.zig b/src/Watcher.zig index c810af324a..6ebd50faf7 100644 --- a/src/Watcher.zig +++ b/src/Watcher.zig @@ -463,9 +463,9 @@ fn appendDirectoryAssumeCapacity( null, ); } else if (Environment.isLinux) { - const buf = bun.PathBufferPool.get(); + const buf = bun.path_buffer_pool.get(); defer { - bun.PathBufferPool.put(buf); + bun.path_buffer_pool.put(buf); } const path: [:0]const u8 = if (clone_file_path and file_path_.len > 0 and file_path_[file_path_.len - 1] == 0) file_path_[0 .. file_path_.len - 1 :0] diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index c6a4e58b94..2a70bea13c 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -93,6 +93,8 @@ pub const Features = struct { pub var loaders: usize = 0; pub var lockfile_migration_from_package_lock: usize = 0; pub var text_lockfile: usize = 0; + pub var isolated_bun_install: usize = 0; + pub var hoisted_bun_install: usize = 0; pub var macros: usize = 0; pub var no_avx2: usize = 0; pub var no_avx: usize = 0; diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 9bdc249913..71acfebac7 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -642,8 +642,8 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { errdefer types.deinit(allocator); for (options.framework.file_system_router_types, 0..) |fsr, i| { - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const joined_root = bun.path.joinAbsStringBuf(dev.root, buf, &.{fsr.root}, .auto); const entry = dev.server_transpiler.resolver.readDirInfoIgnoreError(joined_root) orelse continue; @@ -5180,8 +5180,8 @@ pub fn IncrementalGraph(side: bake.Side) type { dev.relative_path_buf_lock.lock(); defer dev.relative_path_buf_lock.unlock(); - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len); errdefer file_paths.deinit(gpa); @@ -5464,8 +5464,8 @@ const DirectoryWatchStore = struct { => bun.debugAssert(false), } - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const joined = bun.path.joinAbsStringBuf(bun.path.dirname(import_source, .auto), buf, &.{specifier}, .auto); const dir = bun.path.dirname(joined, .auto); @@ -5894,8 +5894,8 @@ pub const SerializedFailure = struct { // For debugging, it is helpful to be able to see bundles. fn dumpBundle(dump_dir: std.fs.Dir, graph: bake.Graph, rel_path: []const u8, chunk: []const u8, wrap: bool) !void { - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const name = bun.path.joinAbsStringBuf("/", buf, &.{ @tagName(graph), rel_path, @@ -7648,8 +7648,8 @@ pub const SourceMapStore = struct { dev.relative_path_buf_lock.lock(); defer dev.relative_path_buf_lock.unlock(); - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); for (paths) |native_file_path| { try source_map_strings.appendSlice(","); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a21108959e..6f1d6e5897 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -271,8 +271,8 @@ pub fn braces(global: *JSC.JSGlobalObject, brace_str: bun.String, opts: gen.Brac pub fn which(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments_ = callframe.arguments_old(2); - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); var arguments = JSC.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); defer arguments.deinit(); const path_arg = arguments.nextEat() orelse { diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index 8a96256605..7812fc120f 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -132,7 +132,7 @@ pub const PidFDType = if (Environment.isLinux) fd_t else u0; pub const Process = struct { const Self = @This(); - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); + const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{}); pub const ref = RefCount.ref; pub const deref = RefCount.deref; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 60f6644d6f..5e3d78dbb5 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2462,8 +2462,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d // So we first use a hash of the main field: const first_hash_segment: [8]u8 = brk: { - const buffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buffer); + const buffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buffer); const main = JSC.VirtualMachine.get().main; const len = @min(main.len, buffer.len); break :brk @bitCast(bun.hash(bun.strings.copyLowercase(main[0..len], buffer[0..len]))); @@ -2471,8 +2471,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d // And then we use a hash of their project root directory: const second_hash_segment: [8]u8 = brk: { - const buffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buffer); + const buffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buffer); const root = this.dev_server.?.root; const len = @min(root.len, buffer.len); break :brk @bitCast(bun.hash(bun.strings.copyLowercase(root[0..len], buffer[0..len]))); diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index c9ba26706b..d6a23f3fe1 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -18,6 +18,7 @@ const IteratorError = error{ AccessDenied, SystemResources } || posix.Unexpected const mem = std.mem; const strings = bun.strings; const Maybe = JSC.Maybe; +const FD = bun.FD; pub const IteratorResult = struct { name: PathString, @@ -50,7 +51,7 @@ pub const IteratorW = NewIterator(true); pub fn NewIterator(comptime use_windows_ospath: bool) type { return switch (builtin.os.tag) { .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris => struct { - dir: Dir, + dir: FD, seek: i64, buf: [8192]u8 align(@alignOf(std.posix.system.dirent)), index: usize, @@ -61,10 +62,6 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { pub const Error = IteratorError; - fn fd(self: *Self) posix.fd_t { - return self.dir.fd; - } - /// Memory such as file names referenced in this returned entry becomes invalid /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. pub const next = switch (builtin.os.tag) { @@ -94,7 +91,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { self.buf[self.buf.len - 4 ..][0..4].* = .{ 0, 0, 0, 0 }; const rc = posix.system.__getdirentries64( - self.dir.fd, + self.dir.cast(), &self.buf, self.buf.len, &self.seek, @@ -146,7 +143,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } }, .linux => struct { - dir: Dir, + dir: FD, // The if guard is solely there to prevent compile errors from missing `linux.dirent64` // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. buf: [8192]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), @@ -158,16 +155,12 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { pub const Error = IteratorError; - fn fd(self: *Self) posix.fd_t { - return self.dir.fd; - } - /// Memory such as file names referenced in this returned entry becomes invalid /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. pub fn next(self: *Self) Result { start_over: while (true) { if (self.index >= self.end_index) { - const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); + const rc = linux.getdents64(self.dir.cast(), &self.buf, self.buf.len); if (Result.errnoSys(rc, .getdents64)) |err| return err; if (rc == 0) return .{ .result = null }; self.index = 0; @@ -208,7 +201,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) const FILE_DIRECTORY_INFORMATION_PTR = *align(2) FILE_DIRECTORY_INFORMATION; - dir: Dir, + dir: FD, // This structure must be aligned on a LONGLONG (8-byte) boundary. // If a buffer contains two or more of these structures, the @@ -227,10 +220,6 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { const ResultT = if (use_windows_ospath) ResultW else Result; - fn fd(self: *Self) posix.fd_t { - return self.dir.fd; - } - /// Memory such as file names referenced in this returned entry becomes invalid /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. pub fn next(self: *Self) ResultT { @@ -244,7 +233,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } const rc = w.ntdll.NtQueryDirectoryFile( - self.dir.fd, + self.dir.cast(), null, null, null, @@ -259,14 +248,14 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { self.first = false; if (io.Information == 0) { - bun.sys.syslog("NtQueryDirectoryFile({}) = 0", .{bun.FD.fromStdDir(self.dir)}); + bun.sys.syslog("NtQueryDirectoryFile({}) = 0", .{self.dir}); return .{ .result = null }; } self.index = 0; self.end_index = io.Information; // If the handle is not a directory, we'll get STATUS_INVALID_PARAMETER. if (rc == .INVALID_PARAMETER) { - bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.FD.fromStdDir(self.dir), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ self.dir, @tagName(rc) }); return .{ .err = .{ .errno = @intFromEnum(bun.sys.SystemErrno.ENOTDIR), @@ -276,13 +265,13 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } if (rc == .NO_MORE_FILES) { - bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.FD.fromStdDir(self.dir), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ self.dir, @tagName(rc) }); self.end_index = self.index; return .{ .result = null }; } if (rc != .SUCCESS) { - bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.FD.fromStdDir(self.dir), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ self.dir, @tagName(rc) }); if ((bun.windows.Win32Error.fromNTStatus(rc).toSystemErrno())) |errno| { return .{ @@ -301,7 +290,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { }; } - bun.sys.syslog("NtQueryDirectoryFile({}) = {d}", .{ bun.FD.fromStdDir(self.dir), self.end_index }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {d}", .{ self.dir, self.end_index }); } const dir_info: FILE_DIRECTORY_INFORMATION_PTR = @ptrCast(@alignCast(&self.buf[self.index])); @@ -356,7 +345,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } }, .wasi => struct { - dir: Dir, + dir: FD, buf: [8192]u8, // TODO align(@alignOf(os.wasi.dirent_t)), cookie: u64, index: usize, @@ -366,10 +355,6 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { pub const Error = IteratorError; - fn fd(self: *Self) posix.fd_t { - return self.dir.fd; - } - /// Memory such as file names referenced in this returned entry becomes invalid /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. pub fn next(self: *Self) Result { @@ -380,7 +365,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { start_over: while (true) { if (self.index >= self.end_index) { var bufused: usize = undefined; - switch (w.fd_readdir(self.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { + switch (w.fd_readdir(self.dir.cast(), &self.buf, self.buf.len, self.cookie, &bufused)) { .SUCCESS => {}, .BADF => unreachable, // Dir is invalid or was opened without iteration ability .FAULT => unreachable, @@ -440,13 +425,9 @@ pub fn NewWrappedIterator(comptime path_type: PathType) type { return self.iter.next(); } - pub inline fn fd(self: *Self) posix.fd_t { - return self.iter.fd(); - } - pub const Error = IteratorError; - pub fn init(dir: Dir) Self { + pub fn init(dir: FD) Self { return Self{ .iter = switch (builtin.os.tag) { .macos, @@ -494,6 +475,6 @@ pub fn NewWrappedIterator(comptime path_type: PathType) type { pub const WrappedIterator = NewWrappedIterator(.u8); pub const WrappedIteratorW = NewWrappedIterator(.u16); -pub fn iterate(self: Dir, comptime path_type: PathType) NewWrappedIterator(path_type) { +pub fn iterate(self: FD, comptime path_type: PathType) NewWrappedIterator(path_type) { return NewWrappedIterator(path_type).init(self); } diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index bb06f08248..324a45e3de 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -269,7 +269,7 @@ pub const Async = struct { this.result = @field(NodeFS, "uv_" ++ @tagName(FunctionEnum))(&node_fs, this.args, @intFromEnum(req.result)); if (this.result == .err) { - this.result.err = this.result.err.clone(bun.default_allocator) catch bun.outOfMemory(); + this.result.err = this.result.err.clone(bun.default_allocator); std.mem.doNotOptimizeAway(&node_fs); } @@ -283,7 +283,7 @@ pub const Async = struct { this.result = @field(NodeFS, "uv_" ++ @tagName(FunctionEnum))(&node_fs, this.args, req, @intFromEnum(req.result)); if (this.result == .err) { - this.result.err = this.result.err.clone(bun.default_allocator) catch bun.outOfMemory(); + this.result.err = this.result.err.clone(bun.default_allocator); std.mem.doNotOptimizeAway(&node_fs); } @@ -382,7 +382,7 @@ pub const Async = struct { this.result = function(&node_fs, this.args, .@"async"); if (this.result == .err) { - this.result.err = this.result.err.clone(bun.default_allocator) catch bun.outOfMemory(); + this.result.err = this.result.err.clone(bun.default_allocator); std.mem.doNotOptimizeAway(&node_fs); } @@ -642,7 +642,7 @@ pub fn NewAsyncCpTask(comptime is_shell: bool) type { this.result = result; if (this.result == .err) { - this.result.err = this.result.err.clone(bun.default_allocator) catch bun.outOfMemory(); + this.result.err = this.result.err.clone(bun.default_allocator); } if (this.evtloop == .js) { @@ -859,8 +859,7 @@ pub fn NewAsyncCpTask(comptime is_shell: bool) type { }, } - const dir = fd.stdDir(); - var iterator = DirIterator.iterate(dir, if (Environment.isWindows) .u16 else .u8); + var iterator = DirIterator.iterate(fd, if (Environment.isWindows) .u16 else .u8); var entry = iterator.next(); while (switch (entry) { .err => |err| { @@ -3722,8 +3721,8 @@ pub const NodeFS = struct { } if (comptime Environment.isWindows) { - const dest_buf = bun.OSPathBufferPool.get(); - defer bun.OSPathBufferPool.put(dest_buf); + const dest_buf = bun.os_path_buffer_pool.get(); + defer bun.os_path_buffer_pool.put(dest_buf); const src = bun.strings.toKernel32Path(bun.reinterpretSlice(u16, &fs.sync_error_buf), args.src.slice()); const dest = bun.strings.toKernel32Path(dest_buf, args.dest.slice()); @@ -3913,8 +3912,8 @@ pub const NodeFS = struct { } pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const path = args.path.osPathKernel32(buf); return switch (args.always_return_none) { @@ -4425,7 +4424,6 @@ pub const NodeFS = struct { comptime ExpectedType: type, entries: *std.ArrayList(ExpectedType), ) Maybe(void) { - const dir = fd.stdDir(); const is_u16 = comptime Environment.isWindows and (ExpectedType == bun.String or ExpectedType == bun.JSC.Node.Dirent); var dirent_path: bun.String = bun.String.dead; @@ -4433,15 +4431,15 @@ pub const NodeFS = struct { dirent_path.deref(); } - var iterator = DirIterator.iterate(dir, comptime if (is_u16) .u16 else .u8); + var iterator = DirIterator.iterate(fd, comptime if (is_u16) .u16 else .u8); var entry = iterator.next(); const re_encoding_buffer: ?*bun.PathBuffer = if (is_u16 and args.encoding != .utf8) - bun.PathBufferPool.get() + bun.path_buffer_pool.get() else null; defer if (is_u16 and args.encoding != .utf8) - bun.PathBufferPool.put(re_encoding_buffer.?); + bun.path_buffer_pool.put(re_encoding_buffer.?); while (switch (entry) { .err => |err| { @@ -4573,7 +4571,7 @@ pub const NodeFS = struct { } } - var iterator = DirIterator.iterate(fd.stdDir(), .u8); + var iterator = DirIterator.iterate(fd, .u8); var entry = iterator.next(); var dirent_path_prev: bun.String = bun.String.empty; defer { @@ -4727,7 +4725,7 @@ pub const NodeFS = struct { } } - var iterator = DirIterator.iterate(fd.stdDir(), .u8); + var iterator = DirIterator.iterate(fd, .u8); var entry = iterator.next(); var dirent_path_prev: bun.String = bun.String.dead; defer { @@ -5973,8 +5971,8 @@ pub const NodeFS = struct { pub fn osPathIntoSyncErrorBufOverlap(this: *NodeFS, slice: anytype) []const u8 { if (Environment.isWindows) { - const tmp = bun.OSPathBufferPool.get(); - defer bun.OSPathBufferPool.put(tmp); + const tmp = bun.os_path_buffer_pool.get(); + defer bun.os_path_buffer_pool.put(tmp); @memcpy(tmp[0..slice.len], slice); return bun.strings.fromWPath(&this.sync_error_buf, tmp[0..slice.len]); } @@ -6088,10 +6086,7 @@ pub const NodeFS = struct { .result => {}, } - var iterator = iterator: { - const dir = fd.stdDir(); - break :iterator DirIterator.iterate(dir, if (Environment.isWindows) .u16 else .u8); - }; + var iterator = DirIterator.iterate(fd, if (Environment.isWindows) .u16 else .u8); var entry = iterator.next(); while (switch (entry) { .err => |err| { @@ -6483,8 +6478,8 @@ pub const NodeFS = struct { .err => |err| return .{ .err = err }, .result => |src_fd| src_fd, }; - const wbuf = bun.OSPathBufferPool.get(); - defer bun.OSPathBufferPool.put(wbuf); + const wbuf = bun.os_path_buffer_pool.get(); + defer bun.os_path_buffer_pool.put(wbuf); const len = bun.windows.GetFinalPathNameByHandleW(handle.cast(), wbuf, wbuf.len, 0); if (len == 0) { return ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse dst_enoent_maybe; diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index 281a11b964..6052c180be 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -465,8 +465,8 @@ pub const StatWatcher = struct { pub fn init(args: Arguments) !*StatWatcher { log("init", .{}); - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); var slice = args.path.slice(); if (bun.strings.startsWith(slice, "file://")) { slice = slice[6..]; diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index f886f8ea56..80f2e87820 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -167,7 +167,7 @@ pub const FSWatcher = struct { pub fn dupe(event: Event) !Event { return switch (event) { inline .rename, .change => |path, t| @unionInit(Event, @tagName(t), try bun.default_allocator.dupe(u8, path)), - .@"error" => |err| .{ .@"error" = try err.clone(bun.default_allocator) }, + .@"error" => |err| .{ .@"error" = err.clone(bun.default_allocator) }, inline else => |value, t| @unionInit(Event, @tagName(t), value), }; } @@ -643,11 +643,11 @@ pub const FSWatcher = struct { } pub fn init(args: Arguments) bun.JSC.Maybe(*FSWatcher) { - const joined_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(joined_buf); + const joined_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(joined_buf); const file_path: [:0]const u8 = brk: { - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); var slice = args.path.slice(); if (bun.strings.startsWith(slice, "file://")) { slice = slice[6..]; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index f884d5c411..4ff5a6c332 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -569,8 +569,8 @@ pub const PathLike = union(enum) { if (std.fs.path.isAbsolute(sliced)) { if (sliced.len > 2 and bun.path.isDriveLetter(sliced[0]) and sliced[1] == ':' and bun.path.isSepAny(sliced[2])) { // Add the long path syntax. This affects most of node:fs - const drive_resolve_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(drive_resolve_buf); + const drive_resolve_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(drive_resolve_buf); const rest = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(drive_resolve_buf, sliced) catch @panic("Error while resolving path."); buf[0..4].* = bun.windows.long_path_prefix_u8; // When long path syntax is used, the entire string should be normalized @@ -619,8 +619,8 @@ pub const PathLike = union(enum) { pub fn osPathKernel32(this: PathLike, buf: *bun.PathBuffer) callconv(bun.callconv_inline) bun.OSPathSliceZ { if (comptime Environment.isWindows) { const s = this.slice(); - const b = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(b); + const b = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(b); if (s.len > 0 and bun.path.isSepAny(s[0])) { const resolve = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBuf(buf, s) catch @panic("Error while resolving path."); const normal = path_handler.normalizeBuf(resolve, b, .windows); diff --git a/src/bun.zig b/src/bun.zig index b875783824..a39e0cb9b9 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -254,13 +254,22 @@ pub const stringZ = StringTypes.stringZ; pub const string = StringTypes.string; pub const CodePoint = StringTypes.CodePoint; -pub const MAX_PATH_BYTES: usize = if (Environment.isWasm) 1024 else std.fs.max_path_bytes; -pub const PathBuffer = [MAX_PATH_BYTES]u8; -pub const WPathBuffer = [std.os.windows.PATH_MAX_WIDE]u16; -pub const OSPathChar = if (Environment.isWindows) u16 else u8; -pub const OSPathSliceZ = [:0]const OSPathChar; -pub const OSPathSlice = []const OSPathChar; -pub const OSPathBuffer = if (Environment.isWindows) WPathBuffer else PathBuffer; +pub const paths = @import("./paths.zig"); +pub const MAX_PATH_BYTES = paths.MAX_PATH_BYTES; +pub const PathBuffer = paths.PathBuffer; +pub const PATH_MAX_WIDE = paths.PATH_MAX_WIDE; +pub const WPathBuffer = paths.WPathBuffer; +pub const OSPathChar = paths.OSPathChar; +pub const OSPathSliceZ = paths.OSPathSliceZ; +pub const OSPathSlice = paths.OSPathSlice; +pub const OSPathBuffer = paths.OSPathBuffer; +pub const Path = paths.Path; +pub const AbsPath = paths.AbsPath; +pub const RelPath = paths.RelPath; +pub const EnvPath = paths.EnvPath; +pub const path_buffer_pool = paths.path_buffer_pool; +pub const w_path_buffer_pool = paths.w_path_buffer_pool; +pub const os_path_buffer_pool = paths.os_path_buffer_pool; pub inline fn cast(comptime To: type, value: anytype) To { if (@typeInfo(@TypeOf(value)) == .int) { @@ -752,14 +761,18 @@ pub fn openDirA(dir: std.fs.Dir, path_: []const u8) !std.fs.Dir { } } -pub fn openDirForIteration(dir: std.fs.Dir, path_: []const u8) !std.fs.Dir { +pub fn openDirForIteration(dir: FD, path_: []const u8) sys.Maybe(FD) { if (comptime Environment.isWindows) { - const res = try sys.openDirAtWindowsA(.fromStdDir(dir), path_, .{ .iterable = true, .can_rename_or_delete = false, .read_only = true }).unwrap(); - return res.stdDir(); - } else { - const fd = try sys.openatA(.fromStdDir(dir), path_, O.DIRECTORY | O.CLOEXEC | O.RDONLY, 0).unwrap(); - return fd.stdDir(); + return sys.openDirAtWindowsA(dir, path_, .{ .iterable = true, .can_rename_or_delete = false, .read_only = true }); } + return sys.openatA(dir, path_, O.DIRECTORY | O.CLOEXEC | O.RDONLY, 0); +} + +pub fn openDirForIterationOSPath(dir: FD, path_: []const OSPathChar) sys.Maybe(FD) { + if (comptime Environment.isWindows) { + return sys.openDirAtWindows(dir, path_, .{ .iterable = true, .can_rename_or_delete = false, .read_only = true }); + } + return sys.openatA(dir, path_, O.DIRECTORY | O.CLOEXEC | O.RDONLY, 0); } pub fn openDirAbsolute(path_: []const u8) !std.fs.Dir { @@ -2712,8 +2725,8 @@ pub fn exitThread() noreturn { pub fn deleteAllPoolsForThreadExit() void { const pools_to_delete = .{ JSC.WebCore.ByteListPool, - bun.WPathBufferPool, - bun.PathBufferPool, + bun.w_path_buffer_pool, + bun.path_buffer_pool, bun.JSC.ConsoleObject.Formatter.Visited.Pool, bun.js_parser.StringVoidMap.Pool, }; @@ -2766,7 +2779,7 @@ pub fn errnoToZigErr(err: anytype) anyerror { pub const brotli = @import("./brotli.zig"); -pub fn iterateDir(dir: std.fs.Dir) DirIterator.Iterator { +pub fn iterateDir(dir: FD) DirIterator.Iterator { return DirIterator.iterate(dir, .u8).iter; } @@ -3642,6 +3655,15 @@ pub inline fn clear(val: anytype, allocator: std.mem.Allocator) void { } } +pub inline fn move(val: anytype) switch (@typeInfo(@TypeOf(val))) { + .pointer => |p| p.child, + else => @compileError("unexpected move type"), +} { + const tmp = val.*; + @constCast(val).* = undefined; + return tmp; +} + pub inline fn wrappingNegation(val: anytype) @TypeOf(val) { return 0 -% val; } @@ -3712,37 +3734,6 @@ pub noinline fn throwStackOverflow() StackOverflow!void { } const StackOverflow = error{StackOverflow}; -// This pool exists because on Windows, each path buffer costs 64 KB. -// This makes the stack memory usage very unpredictable, which means we can't really know how much stack space we have left. -// This pool is a workaround to make the stack memory usage more predictable. -// We keep up to 4 path buffers alive per thread at a time. -pub fn PathBufferPoolT(comptime T: type) type { - return struct { - const Pool = ObjectPool(T, null, true, 4); - - pub fn get() *T { - // use a threadlocal allocator so mimalloc deletes it on thread deinit. - return &Pool.get(bun.threadlocalAllocator()).data; - } - - pub fn put(buffer: *T) void { - var node: *Pool.Node = @alignCast(@fieldParentPtr("data", buffer)); - node.release(); - } - - pub fn deleteAll() void { - Pool.deleteAll(); - } - }; -} - -pub const PathBufferPool = PathBufferPoolT(bun.PathBuffer); -pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPathBuffer) else struct { - // So it can be used in code that deletes all the pools. - pub fn deleteAll() void {} -}; -pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; - pub const S3 = @import("./s3/client.zig"); pub const ptr = @import("ptr.zig"); @@ -3764,13 +3755,12 @@ pub const highway = @import("./highway.zig"); pub const MemoryReportingAllocator = @import("allocators/MemoryReportingAllocator.zig"); -pub fn move(dest: []u8, src: []const u8) void { - if (comptime Environment.allow_assert) { - if (src.len != dest.len) { - bun.Output.panic("Move: src.len != dest.len, {d} != {d}", .{ src.len, dest.len }); - } - } - _ = bun.c.memmove(dest.ptr, src.ptr, src.len); -} - pub const mach_port = if (Environment.isMac) std.c.mach_port_t else u32; + +pub fn contains(item: anytype, list: *const std.ArrayListUnmanaged(@TypeOf(item))) bool { + const T = @TypeOf(item); + return switch (T) { + u8 => strings.containsChar(list.items, item), + else => std.mem.indexOfScalar(T, list.items, item) != null, + }; +} diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index dd4d7fd766..8844b3207d 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2953,8 +2953,8 @@ pub const BundleV2 = struct { ) catch bun.outOfMemory(); } } else { - const buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const specifier_to_use = if (loader == .html and bun.strings.hasPrefix(import_record.path.text, bun.fs.FileSystem.instance.top_level_dir)) brk: { const specifier_to_use = import_record.path.text[bun.fs.FileSystem.instance.top_level_dir.len..]; if (Environment.isWindows) { diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 08faf854f9..ab8aa095f5 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -196,8 +196,7 @@ pub const BunxCommand = struct { if (bin_prop.expr.asString(transpiler.allocator)) |dir_name| { const bin_dir = try bun.sys.openatA(dir_fd, dir_name, bun.O.RDONLY | bun.O.DIRECTORY, 0).unwrap(); defer bin_dir.close(); - const dir = std.fs.Dir{ .fd = bin_dir.cast() }; - var iterator = bun.DirIterator.iterate(dir, .u8); + var iterator = bun.DirIterator.iterate(bin_dir, .u8); var entry = iterator.next(); while (true) : (entry = iterator.next()) { const current = switch (entry) { diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 119fa520b2..e9064147b4 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -484,7 +484,7 @@ pub const CreateCommand = struct { const destination_dir = destination_dir__; const Walker = @import("../walker_skippable.zig"); - var walker_ = try Walker.walk(template_dir, ctx.allocator, skip_files, skip_dirs); + var walker_ = try Walker.walk(.fromStdDir(template_dir), ctx.allocator, skip_files, skip_dirs); defer walker_.deinit(); const FileCopier = struct { @@ -498,7 +498,7 @@ pub const CreateCommand = struct { src_base_len: if (Environment.isWindows) usize else void, src_buf: if (Environment.isWindows) *bun.WPathBuffer else void, ) !void { - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { if (comptime Environment.isWindows) { if (entry.kind != .file and entry.kind != .directory) continue; @@ -561,7 +561,7 @@ pub const CreateCommand = struct { defer outfile.close(); defer node_.completeOne(); - const infile = bun.FD.fromStdFile(try entry.dir.openFile(entry.basename, .{ .mode = .read_only })); + const infile = try entry.dir.openat(entry.basename, bun.O.RDONLY, 0).unwrap(); defer infile.close(); // Assumption: you only really care about making sure something that was executable is still executable diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 8ba5a7fc50..51cb2b146d 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -531,7 +531,7 @@ pub const InitCommand = struct { // Find any source file var dir = std.fs.cwd().openDir(".", .{ .iterate = true }) catch break :infer; defer dir.close(); - var it = bun.DirIterator.iterate(dir, .u8); + var it = bun.DirIterator.iterate(.fromStdDir(dir), .u8); while (try it.next().unwrap()) |file| { if (file.kind != .file) continue; const loader = bun.options.Loader.fromString(std.fs.path.extension(file.name.slice())) orelse @@ -1021,8 +1021,8 @@ const Template = enum { return false; } - const pathbuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(pathbuffer); + const pathbuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(pathbuffer); return bun.which(pathbuffer, bun.getenvZ("PATH") orelse return false, bun.fs.FileSystem.instance.top_level_dir, "claude") != null; } @@ -1097,8 +1097,8 @@ const Template = enum { if (Environment.isWindows) { if (bun.getenvZAnyCase("USER")) |user| { - const pathbuf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(pathbuf); + const pathbuf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(pathbuf); const path = std.fmt.bufPrintZ(pathbuf, "C:\\Users\\{s}\\AppData\\Local\\Programs\\Cursor\\Cursor.exe", .{user}) catch { return false; }; diff --git a/src/cli/link_command.zig b/src/cli/link_command.zig index 0c57ba7329..c66462721b 100644 --- a/src/cli/link_command.zig +++ b/src/cli/link_command.zig @@ -100,7 +100,7 @@ fn link(ctx: Command.Context) !void { ); link_path_buf[top_level.len] = 0; const link_path = link_path_buf[0..top_level.len :0]; - const global_path = try manager.globalLinkDirPath(); + const global_path = manager.globalLinkDirPath(); const dest_path = Path.joinAbsStringZ(global_path, &.{name}, .windows); switch (bun.sys.sys_uv.symlinkUV( link_path, @@ -128,16 +128,18 @@ fn link(ctx: Command.Context) !void { var link_target_buf: bun.PathBuffer = undefined; var link_dest_buf: bun.PathBuffer = undefined; var link_rel_buf: bun.PathBuffer = undefined; - var node_modules_path_buf: bun.PathBuffer = undefined; + + var node_modules_path = bun.AbsPath(.{}).initFdPath(.fromStdDir(node_modules)) catch |err| { + if (manager.options.log_level != .silent) { + Output.err(err, "failed to link binary", .{}); + } + Global.crash(); + }; + defer node_modules_path.deinit(); + var bin_linker = Bin.Linker{ .bin = package.bin, - .node_modules = .fromStdDir(node_modules), - .node_modules_path = bun.getFdPath(.fromStdDir(node_modules), &node_modules_path_buf) catch |err| { - if (manager.options.log_level != .silent) { - Output.err(err, "failed to link binary", .{}); - } - Global.crash(); - }, + .node_modules_path = &node_modules_path, .global_bin_path = manager.options.bin_path, // .destination_dir_subpath = destination_dir_subpath, diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index 5fb3cd3255..056c4a2a2b 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -297,7 +297,7 @@ pub const PackCommand = struct { } } - var dir_iter = DirIterator.iterate(dir, .u8); + var dir_iter = DirIterator.iterate(.fromStdDir(dir), .u8); while (dir_iter.next().unwrap() catch null) |entry| { if (entry.kind != .file and entry.kind != .directory) continue; @@ -451,7 +451,7 @@ pub const PackCommand = struct { } } - var iter = DirIterator.iterate(dir, .u8); + var iter = DirIterator.iterate(.fromStdDir(dir), .u8); while (iter.next().unwrap() catch null) |entry| { if (entry.kind != .file and entry.kind != .directory) continue; @@ -565,7 +565,7 @@ pub const PackCommand = struct { var additional_bundled_deps: std.ArrayListUnmanaged(DirInfo) = .{}; defer additional_bundled_deps.deinit(ctx.allocator); - var iter = DirIterator.iterate(dir, .u8); + var iter = DirIterator.iterate(.fromStdDir(dir), .u8); while (iter.next().unwrap() catch null) |entry| { if (entry.kind != .directory) continue; @@ -579,7 +579,7 @@ pub const PackCommand = struct { }; defer scoped_dir.close(); - var scoped_iter = DirIterator.iterate(scoped_dir, .u8); + var scoped_iter = DirIterator.iterate(.fromStdDir(scoped_dir), .u8); while (scoped_iter.next().unwrap() catch null) |sub_entry| { const entry_name = try entrySubpath(ctx.allocator, _entry_name, sub_entry.name.slice()); @@ -689,7 +689,7 @@ pub const PackCommand = struct { var dir, const dir_subpath, const dir_depth = dir_info; defer dir.close(); - var iter = DirIterator.iterate(dir, .u8); + var iter = DirIterator.iterate(.fromStdDir(dir), .u8); while (iter.next().unwrap() catch null) |entry| { if (entry.kind != .file and entry.kind != .directory) continue; @@ -849,7 +849,7 @@ pub const PackCommand = struct { } } - var dir_iter = DirIterator.iterate(dir, .u8); + var dir_iter = DirIterator.iterate(.fromStdDir(dir), .u8); while (dir_iter.next().unwrap() catch null) |entry| { if (entry.kind != .file and entry.kind != .directory) continue; diff --git a/src/cli/pm_trusted_command.zig b/src/cli/pm_trusted_command.zig index 6edfc0fdec..21f5ca301f 100644 --- a/src/cli/pm_trusted_command.zig +++ b/src/cli/pm_trusted_command.zig @@ -11,7 +11,6 @@ const String = bun.Semver.String; const PackageManager = Install.PackageManager; const PackageManagerCommand = @import("./package_manager_command.zig").PackageManagerCommand; const Lockfile = Install.Lockfile; -const Fs = @import("../fs.zig"); const Global = bun.Global; const DependencyID = Install.DependencyID; const ArrayIdentityContext = bun.ArrayIdentityContext; @@ -73,22 +72,14 @@ pub const UntrustedCommand = struct { var tree_iterator = Lockfile.Tree.Iterator(.node_modules).init(pm.lockfile); - const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); - var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; - defer abs_node_modules_path.deinit(ctx.allocator); - try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); - try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); + var node_modules_path: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer node_modules_path.deinit(); while (tree_iterator.next(null)) |node_modules| { - // + 1 because we want to keep the path separator - abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; - try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); + const node_modules_path_save = node_modules_path.save(); + defer node_modules_path_save.restore(); - var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { - if (err == error.ENOENT) continue; - return err; - }; - defer node_modules_dir.close(); + node_modules_path.append(node_modules.relative_path); for (node_modules.dependencies) |dep_id| { if (untrusted_dep_ids.contains(dep_id)) { @@ -97,12 +88,15 @@ pub const UntrustedCommand = struct { const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; const resolution = &resolutions[package_id]; var package_scripts = scripts[package_id]; - var not_lazy: PackageManager.PackageInstaller.LazyPackageDestinationDir = .{ .dir = node_modules_dir }; + + const folder_name_save = node_modules_path.save(); + defer folder_name_save.restore(); + node_modules_path.append(alias); + const maybe_scripts_list = package_scripts.getList( pm.log, pm.lockfile, - ¬_lazy, - abs_node_modules_path.items, + &node_modules_path, alias, resolution, ) catch |err| { @@ -227,11 +221,8 @@ pub const TrustCommand = struct { // in the correct order as they would during a normal install var tree_iter = Lockfile.Tree.Iterator(.node_modules).init(pm.lockfile); - const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); - var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; - defer abs_node_modules_path.deinit(ctx.allocator); - try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); - try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); + var node_modules_path: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer node_modules_path.deinit(); var package_names_to_add: bun.StringArrayHashMapUnmanaged(void) = .{}; var scripts_at_depth: std.AutoArrayHashMapUnmanaged(usize, std.ArrayListUnmanaged(struct { @@ -243,8 +234,9 @@ pub const TrustCommand = struct { var scripts_count: usize = 0; while (tree_iter.next(null)) |node_modules| { - abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; - try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); + const node_modules_path_save = node_modules_path.save(); + defer node_modules_path_save.restore(); + node_modules_path.append(node_modules.relative_path); var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { if (err == error.ENOENT) continue; @@ -262,12 +254,15 @@ pub const TrustCommand = struct { } const resolution = &resolutions[package_id]; var package_scripts = scripts[package_id]; - var not_lazy = PackageManager.PackageInstaller.LazyPackageDestinationDir{ .dir = node_modules_dir }; + + var folder_save = node_modules_path.save(); + defer folder_save.restore(); + node_modules_path.append(alias); + const maybe_scripts_list = package_scripts.getList( pm.log, pm.lockfile, - ¬_lazy, - abs_node_modules_path.items, + &node_modules_path, alias, resolution, ) catch |err| { @@ -344,6 +339,7 @@ pub const TrustCommand = struct { info.scripts_list, optional, output_in_foreground, + null, ); if (pm.options.log_level.showProgress()) { diff --git a/src/cli/publish_command.zig b/src/cli/publish_command.zig index ae9cd7df01..c4257c5226 100644 --- a/src/cli/publish_command.zig +++ b/src/cli/publish_command.zig @@ -1149,7 +1149,7 @@ pub const PublishCommand = struct { var dir, const dir_subpath, const close_dir = dir_info; defer if (close_dir) dir.close(); - var iter = bun.DirIterator.iterate(dir, .u8); + var iter = bun.DirIterator.iterate(.fromStdDir(dir), .u8); while (iter.next().unwrap() catch null) |entry| { const name, const subpath = name_and_subpath: { const name = entry.name.slice(); diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 4ff919dbe3..51872fb067 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -84,26 +84,24 @@ pub const RunCommand = struct { /// Find the "best" shell to use /// Cached to only run once pub fn findShell(PATH: string, cwd: string) ?stringZ { - const bufs = struct { - pub var shell_buf_once: bun.PathBuffer = undefined; - pub var found_shell: [:0]const u8 = ""; + const Once = struct { + var shell_buf: bun.PathBuffer = undefined; + pub var once = bun.once(struct { + pub fn run(PATH_: string, cwd_: string) ?stringZ { + if (findShellImpl(PATH_, cwd_)) |found| { + if (found.len < shell_buf.len) { + @memcpy(shell_buf[0..found.len], found); + shell_buf[found.len] = 0; + return shell_buf[0..found.len :0]; + } + } + + return null; + } + }.run); }; - if (bufs.found_shell.len > 0) { - return bufs.found_shell; - } - if (findShellImpl(PATH, cwd)) |found| { - if (found.len < bufs.shell_buf_once.len) { - @memcpy(bufs.shell_buf_once[0..found.len], found); - bufs.shell_buf_once[found.len] = 0; - bufs.found_shell = bufs.shell_buf_once[0..found.len :0]; - return bufs.found_shell; - } - - return found; - } - - return null; + return Once.once.call(.{ PATH, cwd }); } const BUN_BIN_NAME = if (Environment.isDebug) "bun-debug" else "bun"; diff --git a/src/cli/unlink_command.zig b/src/cli/unlink_command.zig index 85c9a9328e..a06648e331 100644 --- a/src/cli/unlink_command.zig +++ b/src/cli/unlink_command.zig @@ -55,7 +55,7 @@ fn unlink(ctx: Command.Context) !void { } } - switch (Syscall.lstat(Path.joinAbsStringZ(try manager.globalLinkDirPath(), &.{name}, .auto))) { + switch (Syscall.lstat(Path.joinAbsStringZ(manager.globalLinkDirPath(), &.{name}, .auto))) { .result => |stat| { if (!bun.S.ISLNK(@intCast(stat.mode))) { Output.prettyErrorln("success: package \"{s}\" is not globally linked, so there's nothing to do.", .{name}); @@ -91,17 +91,18 @@ fn unlink(ctx: Command.Context) !void { var link_target_buf: bun.PathBuffer = undefined; var link_dest_buf: bun.PathBuffer = undefined; var link_rel_buf: bun.PathBuffer = undefined; - var node_modules_path_buf: bun.PathBuffer = undefined; + + var node_modules_path = bun.AbsPath(.{}).initFdPath(.fromStdDir(node_modules)) catch |err| { + if (manager.options.log_level != .silent) { + Output.err(err, "failed to link binary", .{}); + } + Global.crash(); + }; + defer node_modules_path.deinit(); var bin_linker = Bin.Linker{ .bin = package.bin, - .node_modules = .fromStdDir(node_modules), - .node_modules_path = bun.getFdPath(.fromStdDir(node_modules), &node_modules_path_buf) catch |err| { - if (manager.options.log_level != .silent) { - Output.err(err, "failed to link binary", .{}); - } - Global.crash(); - }, + .node_modules_path = &node_modules_path, .global_bin_path = manager.options.bin_path, .package_name = strings.StringOrTinyString.init(name), .string_buf = lockfile.buffers.string_bytes.items, diff --git a/src/fd.zig b/src/fd.zig index afdf886a2e..d2a9f8383c 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -538,6 +538,36 @@ pub const FD = packed struct(backing_int) { return @enumFromInt(@as(backing_int, @bitCast(fd))); } + pub fn makePath(dir: FD, comptime T: type, subpath: []const T) !void { + return switch (T) { + u8 => bun.makePath(dir.stdDir(), subpath), + u16 => bun.makePathW(dir.stdDir(), subpath), + else => @compileError("unexpected type"), + }; + } + + pub fn makeOpenPath(dir: FD, comptime T: type, subpath: []const T) !FD { + return switch (T) { + u8 => { + if (comptime Environment.isWindows) { + return bun.sys.openDirAtWindowsA(dir, subpath, .{ .can_rename_or_delete = false, .create = true, .read_only = false }).unwrap(); + } + + return FD.fromStdDir(try dir.stdDir().makeOpenPath(subpath, .{ .iterate = true, .access_sub_paths = true })); + }, + u16 => { + if (comptime !Environment.isWindows) @compileError("unexpected type"); + return bun.sys.openDirAtWindows(dir, subpath, .{ .can_rename_or_delete = false, .create = true, .read_only = false }).unwrap(); + }, + else => @compileError("unexpected type"), + }; + } + + // TODO: make our own version of deleteTree + pub fn deleteTree(dir: FD, subpath: []const u8) !void { + try dir.stdDir().deleteTree(subpath); + } + // The following functions are from bun.sys but with the 'f' prefix dropped // where it is relevant. These functions all take FD as the first argument, // so that makes them Zig methods, even when declared in a separate file. diff --git a/src/feature_flags.zig b/src/feature_flags.zig index cb01ba3fac..a18d3c4add 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -15,8 +15,8 @@ pub const RuntimeFeatureFlag = enum { BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS, BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG, BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, - BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, + BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX, BUN_FEATURE_FLAG_DISABLE_IO_POOL, BUN_FEATURE_FLAG_DISABLE_IPV4, @@ -28,6 +28,7 @@ pub const RuntimeFeatureFlag = enum { BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE, BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE, BUN_FEATURE_FLAG_FORCE_IO_POOL, + BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS, BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304, BUN_FEATURE_FLAG_NO_LIBDEFLATE, BUN_INSTRUMENTS, diff --git a/src/fs.zig b/src/fs.zig index c3a8533a24..a4d5b822ca 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -624,7 +624,7 @@ pub const FileSystem = struct { var existing = this.entries.atIndex(index) orelse return null; if (existing.* == .entries) { if (existing.entries.generation < generation) { - var handle = bun.openDirForIteration(std.fs.cwd(), existing.entries.dir) catch |err| { + var handle = bun.openDirForIteration(FD.cwd(), existing.entries.dir).unwrap() catch |err| { existing.entries.data.clearAndFree(bun.fs_allocator); return this.readDirectoryError(existing.entries.dir, err) catch unreachable; @@ -636,7 +636,7 @@ pub const FileSystem = struct { &existing.entries.data, existing.entries.dir, generation, - handle, + handle.stdDir(), void, void{}, @@ -982,7 +982,7 @@ pub const FileSystem = struct { ) !DirEntry { _ = fs; - var iter = bun.iterateDir(handle); + var iter = bun.iterateDir(.fromStdDir(handle)); var dir = DirEntry.init(_dir, generation); const allocator = bun.fs_allocator; errdefer dir.deinit(allocator); @@ -1382,10 +1382,10 @@ pub const FileSystem = struct { if (comptime bun.Environment.isWindows) { var file = bun.sys.getFileAttributes(absolute_path_c) orelse return error.FileNotFound; var depth: usize = 0; - const buf2: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf2); - const buf3: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf3); + const buf2: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf2); + const buf3: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf3); var current_buf: *bun.PathBuffer = buf2; var other_buf: *bun.PathBuffer = &outpath; diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index 2fb0b13f2d..b69fe5aabe 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -153,7 +153,7 @@ pub const SyscallAccessor = struct { } pub inline fn iterate(dir: Handle) DirIter { - return .{ .value = DirIterator.WrappedIterator.init(dir.value.stdDir()) }; + return .{ .value = DirIterator.WrappedIterator.init(dir.value) }; } }; diff --git a/src/hive_array.zig b/src/hive_array.zig index 042c4d2387..c1881a5c4b 100644 --- a/src/hive_array.zig +++ b/src/hive_array.zig @@ -3,6 +3,7 @@ const bun = @import("bun"); const assert = bun.assert; const mem = std.mem; const testing = std.testing; +const OOM = bun.OOM; /// An array that efficiently tracks which elements are in use. /// The pointers are intended to be stable @@ -114,7 +115,7 @@ pub fn HiveArray(comptime T: type, comptime capacity: u16) type { return self.allocator.create(T) catch bun.outOfMemory(); } - pub fn tryGet(self: *This) !*T { + pub fn tryGet(self: *This) OOM!*T { if (comptime capacity > 0) { if (self.hive.get()) |value| { return value; diff --git a/src/identity_context.zig b/src/identity_context.zig index 5e0cfe987e..171c253540 100644 --- a/src/identity_context.zig +++ b/src/identity_context.zig @@ -1,7 +1,11 @@ pub fn IdentityContext(comptime Key: type) type { return struct { pub fn hash(_: @This(), key: Key) u64 { - return key; + return switch (comptime @typeInfo(Key)) { + .@"enum" => @intFromEnum(key), + .int => key, + else => @compileError("unexpected identity context type"), + }; } pub fn eql(_: @This(), a: Key, b: Key) bool { diff --git a/src/install/NetworkTask.zig b/src/install/NetworkTask.zig index 8469ba5bd2..8f0e711f06 100644 --- a/src/install/NetworkTask.zig +++ b/src/install/NetworkTask.zig @@ -1,6 +1,6 @@ unsafe_http_client: AsyncHTTP = undefined, response: bun.http.HTTPClientResult = .{}, -task_id: u64, +task_id: Task.Id, url_buf: []const u8 = &[_]u8{}, retried: u16 = 0, allocator: std.mem.Allocator, @@ -24,7 +24,7 @@ next: ?*NetworkTask = null, pub const DedupeMapEntry = struct { is_required: bool, }; -pub const DedupeMap = std.HashMap(u64, DedupeMapEntry, IdentityContext(u64), 80); +pub const DedupeMap = std.HashMap(Task.Id, DedupeMapEntry, IdentityContext(Task.Id), 80); pub fn notify(this: *NetworkTask, async_http: *AsyncHTTP, result: bun.http.HTTPClientResult) void { defer this.package_manager.wake(); diff --git a/src/install/PackageInstall.zig b/src/install/PackageInstall.zig index 9736414a83..c65dfc098d 100644 --- a/src/install/PackageInstall.zig +++ b/src/install/PackageInstall.zig @@ -43,7 +43,7 @@ pub const PackageInstall = struct { package_name: String, package_version: string, - patch: Patch, + patch: ?Patch, // TODO: this is never read file_count: u32 = 0, @@ -53,15 +53,8 @@ pub const PackageInstall = struct { const ThisPackageInstall = @This(); pub const Patch = struct { - root_project_dir: ?[]const u8 = null, - patch_path: string = undefined, - patch_contents_hash: u64 = 0, - - pub const NULL = Patch{}; - - pub fn isNull(this: Patch) bool { - return this.root_project_dir == null; - } + path: string, + contents_hash: u64, }; const debug = Output.scoped(.install, true); @@ -140,12 +133,11 @@ pub const PackageInstall = struct { /// fn verifyPatchHash( this: *@This(), + patch: *const Patch, root_node_modules_dir: std.fs.Dir, ) bool { - bun.debugAssert(!this.patch.isNull()); - // hash from the .patch file, to be checked against bun tag - const patchfile_contents_hash = this.patch.patch_contents_hash; + const patchfile_contents_hash = patch.contents_hash; var buf: BuntagHashBuf = undefined; const bunhashtag = buntaghashbuf_make(&buf, patchfile_contents_hash); @@ -211,9 +203,12 @@ pub const PackageInstall = struct { this.verifyTransitiveSymlinkedFolder(root_node_modules_dir), else => this.verifyPackageJSONNameAndVersion(root_node_modules_dir, resolution.tag), }; - if (this.patch.isNull()) return verified; - if (!verified) return false; - return this.verifyPatchHash(root_node_modules_dir); + + if (this.patch) |*patch| { + if (!verified) return false; + return this.verifyPatchHash(patch, root_node_modules_dir); + } + return verified; } // Only check for destination directory in node_modules. We can't use package.json because @@ -415,7 +410,7 @@ pub const PackageInstall = struct { var cached_package_dir = bun.openDir(this.cache_dir, this.cache_dir_subpath) catch |err| return Result.fail(err, .opening_cache_dir, @errorReturnTrace()); defer cached_package_dir.close(); var walker_ = Walker.walk( - cached_package_dir, + .fromStdDir(cached_package_dir), this.allocator, &[_]bun.OSPathSlice{}, &[_]bun.OSPathSlice{}, @@ -429,7 +424,7 @@ pub const PackageInstall = struct { ) !u32 { var real_file_count: u32 = 0; var stackpath: [bun.MAX_PATH_BYTES]u8 = undefined; - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { switch (entry.kind) { .directory => { _ = bun.sys.mkdirat(.fromStdDir(destination_dir_), entry.path, 0o755); @@ -440,7 +435,7 @@ pub const PackageInstall = struct { const path: [:0]u8 = stackpath[0..entry.path.len :0]; const basename: [:0]u8 = stackpath[entry.path.len - entry.basename.len .. entry.path.len :0]; switch (bun.c.clonefileat( - entry.dir.fd, + entry.dir.cast(), basename, destination_dir_.fd, path, @@ -549,7 +544,7 @@ pub const PackageInstall = struct { return Result.fail(err, .opening_cache_dir, @errorReturnTrace()); state.walker = Walker.walk( - state.cached_package_dir, + .fromStdDir(state.cached_package_dir), this.allocator, &[_]bun.OSPathSlice{}, if (method == .symlink and this.cache_dir_subpath.len == 1 and this.cache_dir_subpath[0] == '.') @@ -635,7 +630,7 @@ pub const PackageInstall = struct { var copy_file_state: bun.CopyFileState = .{}; - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { if (comptime Environment.isWindows) { switch (entry.kind) { .directory, .file => {}, @@ -688,10 +683,9 @@ pub const PackageInstall = struct { } else { if (entry.kind != .file) continue; real_file_count += 1; - const openFile = std.fs.Dir.openFile; const createFile = std.fs.Dir.createFile; - var in_file = try openFile(entry.dir, entry.basename, .{ .mode = .read_only }); + var in_file = try entry.dir.openat(entry.basename, bun.O.RDONLY, 0).unwrap(); defer in_file.close(); debug("createFile {} {s}\n", .{ destination_dir_.fd, entry.path }); @@ -712,11 +706,11 @@ pub const PackageInstall = struct { defer outfile.close(); if (comptime Environment.isPosix) { - const stat = in_file.stat() catch continue; + const stat = in_file.stat().unwrap() catch continue; _ = bun.c.fchmod(outfile.handle, @intCast(stat.mode)); } - bun.copyFileWithState(.fromStdFile(in_file), .fromStdFile(outfile), ©_file_state).unwrap() catch |err| { + bun.copyFileWithState(in_file, .fromStdFile(outfile), ©_file_state).unwrap() catch |err| { if (progress_) |progress| { progress.root.end(); progress.refresh(); @@ -910,20 +904,20 @@ pub const PackageInstall = struct { var real_file_count: u32 = 0; var queue = if (Environment.isWindows) HardLinkWindowsInstallTask.getQueue(); - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { if (comptime Environment.isPosix) { switch (entry.kind) { .directory => { bun.MakePath.makePath(std.meta.Elem(@TypeOf(entry.path)), destination_dir, entry.path) catch {}; }, .file => { - std.posix.linkat(entry.dir.fd, entry.basename, destination_dir.fd, entry.path, 0) catch |err| { + std.posix.linkatZ(entry.dir.cast(), entry.basename, destination_dir.fd, entry.path, 0) catch |err| { if (err != error.PathAlreadyExists) { return err; } - std.posix.unlinkat(destination_dir.fd, entry.path, 0) catch {}; - try std.posix.linkat(entry.dir.fd, entry.basename, destination_dir.fd, entry.path, 0); + std.posix.unlinkatZ(destination_dir.fd, entry.path, 0) catch {}; + try std.posix.linkatZ(entry.dir.cast(), entry.basename, destination_dir.fd, entry.path, 0); }; real_file_count += 1; @@ -1019,7 +1013,7 @@ pub const PackageInstall = struct { head2: []if (Environment.isWindows) u16 else u8, ) !u32 { var real_file_count: u32 = 0; - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { if (comptime Environment.isPosix) { switch (entry.kind) { .directory => { @@ -1181,7 +1175,7 @@ pub const PackageInstall = struct { var unintall_task: *@This() = @fieldParentPtr("task", task); var debug_timer = bun.Output.DebugTimer.start(); defer { - _ = PackageManager.get().decrementPendingTasks(); + PackageManager.get().decrementPendingTasks(); PackageManager.get().wake(); } @@ -1334,12 +1328,12 @@ pub const PackageInstall = struct { // https://github.com/npm/cli/blob/162c82e845d410ede643466f9f8af78a312296cc/workspaces/arborist/lib/arborist/reify.js#L738 // https://github.com/npm/cli/commit/0e58e6f6b8f0cd62294642a502c17561aaf46553 - switch (bun.sys.symlinkOrJunction(dest_z, target_z)) { + switch (bun.sys.symlinkOrJunction(dest_z, target_z, null)) { .err => |err_| brk: { var err = err_; if (err.getErrno() == .EXIST) { _ = bun.sys.rmdirat(.fromStdDir(destination_dir), this.destination_dir_subpath); - switch (bun.sys.symlinkOrJunction(dest_z, target_z)) { + switch (bun.sys.symlinkOrJunction(dest_z, target_z, null)) { .err => |e| err = e, .result => break :brk, } @@ -1380,7 +1374,7 @@ pub const PackageInstall = struct { return switch (state) { .done => false, else => brk: { - if (this.patch.isNull()) { + if (this.patch == null) { const exists = switch (resolution_tag) { .npm => package_json_exists: { var buf = &PackageManager.cached_package_folder_name_buf; diff --git a/src/install/PackageInstaller.zig b/src/install/PackageInstaller.zig index 89f610522a..5f654a58cf 100644 --- a/src/install/PackageInstaller.zig +++ b/src/install/PackageInstaller.zig @@ -242,7 +242,6 @@ pub const PackageInstaller = struct { pub fn incrementTreeInstallCount( this: *PackageInstaller, tree_id: Lockfile.Tree.Id, - maybe_destination_dir: ?*LazyPackageDestinationDir, comptime should_install_packages: bool, log_level: Options.LogLevel, ) void { @@ -269,19 +268,13 @@ pub const PackageInstaller = struct { this.completed_trees.set(tree_id); - // Avoid opening this directory if we don't need to. if (tree.binaries.count() > 0) { - // Don't close this directory in here. It will be closed by the caller. - if (maybe_destination_dir) |maybe| { - if (maybe.getDir() catch null) |destination_dir| { - this.seen_bin_links.clearRetainingCapacity(); + this.seen_bin_links.clearRetainingCapacity(); - var link_target_buf: bun.PathBuffer = undefined; - var link_dest_buf: bun.PathBuffer = undefined; - var link_rel_buf: bun.PathBuffer = undefined; - this.linkTreeBins(tree, tree_id, destination_dir, &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); - } - } + var link_target_buf: bun.PathBuffer = undefined; + var link_dest_buf: bun.PathBuffer = undefined; + var link_rel_buf: bun.PathBuffer = undefined; + this.linkTreeBins(tree, tree_id, &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); } if (comptime should_install_packages) { @@ -295,7 +288,6 @@ pub const PackageInstaller = struct { this: *PackageInstaller, tree: *TreeContext, tree_id: TreeContext.Id, - destination_dir: std.fs.Dir, link_target_buf: []u8, link_dest_buf: []u8, link_rel_buf: []u8, @@ -303,6 +295,9 @@ pub const PackageInstaller = struct { ) void { const lockfile = this.lockfile; const string_buf = lockfile.buffers.string_bytes.items; + var node_modules_path: bun.AbsPath(.{}) = .from(this.node_modules.path.items); + defer node_modules_path.deinit(); + while (tree.binaries.removeOrNull()) |dep_id| { bun.assertWithLocation(dep_id < lockfile.buffers.dependencies.items.len, @src()); const package_id = lockfile.buffers.resolutions.items[dep_id]; @@ -319,8 +314,7 @@ pub const PackageInstaller = struct { .string_buf = string_buf, .extern_string_buf = lockfile.buffers.extern_strings.items, .seen = &this.seen_bin_links, - .node_modules_path = this.node_modules.path.items, - .node_modules = .fromStdDir(destination_dir), + .node_modules_path = &node_modules_path, .abs_target_buf = link_target_buf, .abs_dest_buf = link_dest_buf, .rel_buf = link_rel_buf, @@ -385,18 +379,7 @@ pub const PackageInstaller = struct { this.node_modules.path.appendSlice(rel_path) catch bun.outOfMemory(); - var destination_dir = this.node_modules.openDir(this.root_node_modules_folder) catch |err| { - if (log_level != .silent) { - Output.err(err, "Failed to open node_modules folder at {s}", .{ - bun.fmt.fmtPath(u8, this.node_modules.path.items, .{}), - }); - } - - continue; - }; - defer destination_dir.close(); - - this.linkTreeBins(tree, @intCast(tree_id), destination_dir, &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); + this.linkTreeBins(tree, @intCast(tree_id), &link_target_buf, &link_dest_buf, &link_rel_buf, log_level); } } } @@ -417,6 +400,7 @@ pub const PackageInstaller = struct { entry.list, optional, output_in_foreground, + null, ) catch |err| { if (log_level != .silent) { const fmt = "\nerror: failed to spawn life-cycle scripts for {s}: {s}\n"; @@ -498,7 +482,7 @@ pub const PackageInstaller = struct { const optional = entry.optional; const output_in_foreground = false; - this.manager.spawnPackageLifecycleScripts(this.command_ctx, entry.list, optional, output_in_foreground) catch |err| { + this.manager.spawnPackageLifecycleScripts(this.command_ctx, entry.list, optional, output_in_foreground, null) catch |err| { if (log_level != .silent) { const fmt = "\nerror: failed to spawn life-cycle scripts for {s}: {s}\n"; const args = .{ package_name, @errorName(err) }; @@ -594,39 +578,24 @@ pub const PackageInstaller = struct { /// Install versions of a package which are waiting on a network request pub fn installEnqueuedPackagesAfterExtraction( this: *PackageInstaller, + task_id: Task.Id, dependency_id: DependencyID, data: *const ExtractData, log_level: Options.LogLevel, ) void { const package_id = this.lockfile.buffers.resolutions.items[dependency_id]; const name = this.names[package_id]; - const resolution = &this.resolutions[package_id]; - const task_id = switch (resolution.tag) { - .git => Task.Id.forGitCheckout(data.url, data.resolved), - .github => Task.Id.forTarball(data.url), - .local_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.local_tarball)), - .remote_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.remote_tarball)), - .npm => Task.Id.forNPMPackage(name.slice(this.lockfile.buffers.string_bytes.items), resolution.value.npm.version), - else => unreachable, - }; - if (!this.installEnqueuedPackagesImpl(name, task_id, log_level)) { - if (comptime Environment.allow_assert) { - Output.panic("Ran callback to install enqueued packages, but there was no task associated with it. {}:{} (dependency_id: {d})", .{ - bun.fmt.quote(name.slice(this.lockfile.buffers.string_bytes.items)), - bun.fmt.quote(data.url), - dependency_id, - }); - } - } - } + // const resolution = &this.resolutions[package_id]; + // const task_id = switch (resolution.tag) { + // .git => Task.Id.forGitCheckout(data.url, data.resolved), + // .github => Task.Id.forTarball(data.url), + // .local_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.local_tarball)), + // .remote_tarball => Task.Id.forTarball(this.lockfile.str(&resolution.value.remote_tarball)), + // .npm => Task.Id.forNPMPackage(name.slice(this.lockfile.buffers.string_bytes.items), resolution.value.npm.version), + // else => unreachable, + // }; - pub fn installEnqueuedPackagesImpl( - this: *PackageInstaller, - name: String, - task_id: Task.Id.Type, - log_level: Options.LogLevel, - ) bool { if (this.manager.task_queue.fetchRemove(task_id)) |removed| { var callbacks = removed.value; defer callbacks.deinit(this.manager.allocator); @@ -638,7 +607,7 @@ pub const PackageInstaller = struct { if (callbacks.items.len == 0) { debug("Unexpected state: no callbacks for async task.", .{}); - return true; + return; } for (callbacks.items) |*cb| { @@ -664,9 +633,16 @@ pub const PackageInstaller = struct { ); this.node_modules.deinit(); } - return true; + return; + } + + if (comptime Environment.allow_assert) { + Output.panic("Ran callback to install enqueued packages, but there was no task associated with it. {}:{} (dependency_id: {d})", .{ + bun.fmt.quote(name.slice(this.lockfile.buffers.string_bytes.items)), + bun.fmt.quote(data.url), + dependency_id, + }); } - return false; } fn getInstalledPackageScriptsCount( @@ -674,7 +650,7 @@ pub const PackageInstaller = struct { alias: string, package_id: PackageID, resolution_tag: Resolution.Tag, - node_modules_folder: *LazyPackageDestinationDir, + folder_path: *bun.AbsPath(.{ .sep = .auto }), log_level: Options.LogLevel, ) usize { if (comptime Environment.allow_assert) { @@ -696,8 +672,7 @@ pub const PackageInstaller = struct { this.lockfile.allocator, &string_builder, this.manager.log, - node_modules_folder, - alias, + folder_path, ) catch |err| { if (log_level != .silent) { Output.errGeneric("failed to fill lifecycle scripts for {s}: {s}", .{ @@ -835,11 +810,10 @@ pub const PackageInstaller = struct { .destination_dir_subpath_buf = &this.destination_dir_subpath_buf, .allocator = this.lockfile.allocator, .package_name = pkg_name, - .patch = if (patch_patch) |p| PackageInstall.Patch{ - .patch_contents_hash = patch_contents_hash.?, - .patch_path = p, - .root_project_dir = FileSystem.instance.top_level_dir, - } else PackageInstall.Patch.NULL, + .patch = if (patch_patch) |p| .{ + .contents_hash = patch_contents_hash.?, + .path = p, + } else null, .package_version = package_version, .node_modules = &this.node_modules, .lockfile = this.lockfile, @@ -848,7 +822,6 @@ pub const PackageInstaller = struct { pkg_name.slice(this.lockfile.buffers.string_bytes.items), resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix), }); - const pkg_has_patch = !installer.patch.isNull(); switch (resolution.tag) { .npm => { @@ -917,32 +890,7 @@ pub const PackageInstaller = struct { installer.cache_dir = std.fs.cwd(); }, .symlink => { - const directory = this.manager.globalLinkDir() catch |err| { - if (log_level != .silent) { - const fmt = "\nerror: unable to access global directory while installing {s}: {s}\n"; - const args = .{ pkg_name.slice(this.lockfile.buffers.string_bytes.items), @errorName(err) }; - - if (log_level.showProgress()) { - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - this.progress.log(comptime Output.prettyFmt(fmt, enable_ansi_colors), args); - }, - } - } else { - Output.prettyErrorln(fmt, args); - } - } - - if (this.manager.options.enable.fail_early) { - Global.exit(1); - } - - Output.flush(); - this.summary.fail += 1; - - if (!installer.patch.isNull()) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); - return; - }; + const directory = this.manager.globalLinkDir(); const folder = resolution.value.symlink.slice(this.lockfile.buffers.string_bytes.items); @@ -950,7 +898,7 @@ pub const PackageInstaller = struct { installer.cache_dir_subpath = "."; installer.cache_dir = std.fs.cwd(); } else { - const global_link_dir = this.manager.globalLinkDirPath() catch unreachable; + const global_link_dir = this.manager.globalLinkDirPath(); var ptr = &this.folder_path_buf; var remain: []u8 = this.folder_path_buf[0..]; @memcpy(ptr[0..global_link_dir.len], global_link_dir); @@ -971,7 +919,7 @@ pub const PackageInstaller = struct { if (comptime Environment.allow_assert) { @panic("Internal assertion failure: unexpected resolution tag"); } - if (!installer.patch.isNull()) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); return; }, } @@ -983,7 +931,7 @@ pub const PackageInstaller = struct { this.summary.skipped += @intFromBool(!needs_install); if (needs_install) { - if (!remove_patch and resolution.tag.canEnqueueInstallTask() and installer.packageMissingFromCache(this.manager, package_id, resolution.tag)) { + if (resolution.tag.canEnqueueInstallTask() and installer.packageMissingFromCache(this.manager, package_id, resolution.tag)) { if (comptime Environment.allow_assert) { bun.assertWithLocation(resolution.canEnqueueInstallTask(), @src()); } @@ -1017,7 +965,6 @@ pub const PackageInstaller = struct { ) catch |err| switch (err) { error.OutOfMemory => bun.outOfMemory(), error.InvalidURL => this.failWithInvalidUrl( - pkg_has_patch, is_pending_package_install, log_level, ), @@ -1041,7 +988,6 @@ pub const PackageInstaller = struct { ) catch |err| switch (err) { error.OutOfMemory => bun.outOfMemory(), error.InvalidURL => this.failWithInvalidUrl( - pkg_has_patch, is_pending_package_install, log_level, ), @@ -1070,7 +1016,6 @@ pub const PackageInstaller = struct { ) catch |err| switch (err) { error.OutOfMemory => bun.outOfMemory(), error.InvalidURL => this.failWithInvalidUrl( - pkg_has_patch, is_pending_package_install, log_level, ), @@ -1080,7 +1025,7 @@ pub const PackageInstaller = struct { if (comptime Environment.allow_assert) { @panic("unreachable, handled above"); } - if (!installer.patch.isNull()) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); this.summary.fail += 1; }, } @@ -1090,12 +1035,12 @@ pub const PackageInstaller = struct { // above checks if unpatched package is in cache, if not null apply patch in temp directory, copy // into cache, then install into node_modules - if (!installer.patch.isNull()) { + if (installer.patch) |patch| { if (installer.patchedPackageMissingFromCache(this.manager, package_id)) { const task = PatchTask.newApplyPatchHash( this.manager, package_id, - installer.patch.patch_contents_hash, + patch.contents_hash, patch_name_and_version_hash.?, ); task.callback.apply.install_context = .{ @@ -1126,7 +1071,7 @@ pub const PackageInstaller = struct { }); } this.summary.fail += 1; - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); return; }; @@ -1185,10 +1130,14 @@ pub const PackageInstaller = struct { }; if (resolution.tag != .root and (resolution.tag == .workspace or is_trusted)) { + var folder_path: bun.AbsPath(.{ .sep = .auto }) = .from(this.node_modules.path.items); + defer folder_path.deinit(); + folder_path.append(alias.slice(this.lockfile.buffers.string_bytes.items)); + if (this.enqueueLifecycleScripts( alias.slice(this.lockfile.buffers.string_bytes.items), log_level, - &lazy_package_dir, + &folder_path, package_id, dep.behavior.optional, resolution, @@ -1212,11 +1161,15 @@ pub const PackageInstaller = struct { else => if (!is_trusted and this.metas[package_id].hasInstallScript()) { // Check if the package actually has scripts. `hasInstallScript` can be false positive if a package is published with // an auto binding.gyp rebuild script but binding.gyp is excluded from the published files. + var folder_path: bun.AbsPath(.{ .sep = .auto }) = .from(this.node_modules.path.items); + defer folder_path.deinit(); + folder_path.append(alias.slice(this.lockfile.buffers.string_bytes.items)); + const count = this.getInstalledPackageScriptsCount( alias.slice(this.lockfile.buffers.string_bytes.items), package_id, resolution.tag, - &lazy_package_dir, + &folder_path, log_level, ); if (count > 0) { @@ -1234,7 +1187,7 @@ pub const PackageInstaller = struct { }, } - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &lazy_package_dir, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); }, .failure => |cause| { if (comptime Environment.allow_assert) { @@ -1243,7 +1196,7 @@ pub const PackageInstaller = struct { // even if the package failed to install, we still need to increment the install // counter for this tree - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &lazy_package_dir, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); if (cause.err == error.DanglingSymlink) { Output.prettyErrorln( @@ -1333,7 +1286,7 @@ pub const PackageInstaller = struct { destination_dir.close(); } - defer if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, &destination_dir, !is_pending_package_install, log_level); + defer this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); const dep = this.lockfile.buffers.dependencies.items[dependency_id]; const truncated_dep_name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); @@ -1349,10 +1302,14 @@ pub const PackageInstaller = struct { }; if (resolution.tag != .root and is_trusted) { + var folder_path: bun.AbsPath(.{ .sep = .auto }) = .from(this.node_modules.path.items); + defer folder_path.deinit(); + folder_path.append(alias.slice(this.lockfile.buffers.string_bytes.items)); + if (this.enqueueLifecycleScripts( alias.slice(this.lockfile.buffers.string_bytes.items), log_level, - &destination_dir, + &folder_path, package_id, dep.behavior.optional, resolution, @@ -1375,12 +1332,11 @@ pub const PackageInstaller = struct { fn failWithInvalidUrl( this: *PackageInstaller, - pkg_has_patch: bool, comptime is_pending_package_install: bool, log_level: Options.LogLevel, ) void { this.summary.fail += 1; - if (!pkg_has_patch) this.incrementTreeInstallCount(this.current_tree_id, null, !is_pending_package_install, log_level); + this.incrementTreeInstallCount(this.current_tree_id, !is_pending_package_install, log_level); } // returns true if scripts are enqueued @@ -1388,7 +1344,7 @@ pub const PackageInstaller = struct { this: *PackageInstaller, folder_name: string, log_level: Options.LogLevel, - node_modules_folder: *LazyPackageDestinationDir, + package_path: *bun.AbsPath(.{ .sep = .auto }), package_id: PackageID, optional: bool, resolution: *const Resolution, @@ -1397,8 +1353,7 @@ pub const PackageInstaller = struct { const scripts_list = scripts.getList( this.manager.log, this.lockfile, - node_modules_folder, - this.node_modules.path.items, + package_path, folder_name, resolution, ) catch |err| { diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index d6af0e00ac..d4425eeaed 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -1,6 +1,5 @@ cache_directory_: ?std.fs.Dir = null, -// TODO(dylan-conway): remove this field when we move away from `std.ChildProcess` in repository.zig cache_directory_path: stringZ = "", temp_dir_: ?std.fs.Dir = null, temp_dir_path: stringZ = "", @@ -96,7 +95,6 @@ preinstall_state: std.ArrayListUnmanaged(PreinstallState) = .{}, global_link_dir: ?std.fs.Dir = null, global_dir: ?std.fs.Dir = null, global_link_dir_path: string = "", -wait_count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), onWake: WakeHandler = .{}, ci_mode: bun.LazyBool(computeIsContinuousIntegration, @This(), "ci_mode") = .{}, @@ -307,60 +305,69 @@ pub fn hasEnoughTimePassedBetweenWaitingMessages() bool { return false; } -pub fn configureEnvForScripts(this: *PackageManager, ctx: Command.Context, log_level: Options.LogLevel) !*transpiler.Transpiler { - if (this.env_configure) |*env_configure| { - return &env_configure.transpiler; - } - - // We need to figure out the PATH and other environment variables - // to do that, we re-use the code from bun run - // this is expensive, it traverses the entire directory tree going up to the root - // so we really only want to do it when strictly necessary - this.env_configure = .{ - .root_dir_info = undefined, - .transpiler = undefined, - }; - const this_transpiler: *transpiler.Transpiler = &this.env_configure.?.transpiler; - - const root_dir_info = try RunCommand.configureEnvForRun( - ctx, - this_transpiler, - this.env, - log_level != .silent, - false, - ); - - const init_cwd_entry = try this.env.map.getOrPutWithoutValue("INIT_CWD"); - if (!init_cwd_entry.found_existing) { - init_cwd_entry.key_ptr.* = try ctx.allocator.dupe(u8, init_cwd_entry.key_ptr.*); - init_cwd_entry.value_ptr.* = .{ - .value = try ctx.allocator.dupe(u8, strings.withoutTrailingSlash(FileSystem.instance.top_level_dir)), - .conditional = false, - }; - } - - this.env.loadCCachePath(this_transpiler.fs); - - { - var node_path: bun.PathBuffer = undefined; - if (this.env.getNodePath(this_transpiler.fs, &node_path)) |node_pathZ| { - _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, node_pathZ) catch bun.outOfMemory()); - } else brk: { - const current_path = this.env.get("PATH") orelse ""; - var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, current_path.len); - try PATH.appendSlice(current_path); - var bun_path: string = ""; - RunCommand.createFakeTemporaryNodeExecutable(&PATH, &bun_path) catch break :brk; - try this.env.map.put("PATH", PATH.items); - _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, bun_path) catch bun.outOfMemory()); - } - } - - this.env_configure.?.root_dir_info = root_dir_info; - - return this_transpiler; +pub fn configureEnvForScripts(this: *PackageManager, ctx: Command.Context, log_level: Options.LogLevel) !transpiler.Transpiler { + return configureEnvForScriptsOnce.call(.{ this, ctx, log_level }); } +pub var configureEnvForScriptsOnce = bun.once(struct { + pub fn run(this: *PackageManager, ctx: Command.Context, log_level: Options.LogLevel) !transpiler.Transpiler { + + // We need to figure out the PATH and other environment variables + // to do that, we re-use the code from bun run + // this is expensive, it traverses the entire directory tree going up to the root + // so we really only want to do it when strictly necessary + var this_transpiler: transpiler.Transpiler = undefined; + _ = try RunCommand.configureEnvForRun( + ctx, + &this_transpiler, + this.env, + log_level != .silent, + false, + ); + + const init_cwd_entry = try this.env.map.getOrPutWithoutValue("INIT_CWD"); + if (!init_cwd_entry.found_existing) { + init_cwd_entry.key_ptr.* = try ctx.allocator.dupe(u8, init_cwd_entry.key_ptr.*); + init_cwd_entry.value_ptr.* = .{ + .value = try ctx.allocator.dupe(u8, strings.withoutTrailingSlash(FileSystem.instance.top_level_dir)), + .conditional = false, + }; + } + + this.env.loadCCachePath(this_transpiler.fs); + + { + // Run node-gyp jobs in parallel. + // https://github.com/nodejs/node-gyp/blob/7d883b5cf4c26e76065201f85b0be36d5ebdcc0e/lib/build.js#L150-L184 + const thread_count = bun.getThreadCount(); + if (thread_count > 2) { + if (!this_transpiler.env.has("JOBS")) { + var int_buf: [10]u8 = undefined; + const jobs_str = std.fmt.bufPrint(&int_buf, "{d}", .{thread_count}) catch unreachable; + this_transpiler.env.map.putAllocValue(bun.default_allocator, "JOBS", jobs_str) catch unreachable; + } + } + } + + { + var node_path: bun.PathBuffer = undefined; + if (this.env.getNodePath(this_transpiler.fs, &node_path)) |node_pathZ| { + _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, node_pathZ) catch bun.outOfMemory()); + } else brk: { + const current_path = this.env.get("PATH") orelse ""; + var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, current_path.len); + try PATH.appendSlice(current_path); + var bun_path: string = ""; + RunCommand.createFakeTemporaryNodeExecutable(&PATH, &bun_path) catch break :brk; + try this.env.map.put("PATH", PATH.items); + _ = try this.env.loadNodeJSConfig(this_transpiler.fs, bun.default_allocator.dupe(u8, bun_path) catch bun.outOfMemory()); + } + } + + return this_transpiler; + } +}.run); + pub fn httpProxy(this: *PackageManager, url: URL) ?URL { return this.env.getHttpProxyFor(url); } @@ -409,7 +416,6 @@ pub fn wake(this: *PackageManager) void { this.onWake.getHandler()(ctx, this); } - _ = this.wait_count.fetchAdd(1, .monotonic); this.event_loop.wakeup(); } @@ -418,7 +424,7 @@ pub fn sleepUntil(this: *PackageManager, closure: anytype, comptime isDoneFn: an this.event_loop.tick(closure, isDoneFn); } -pub var cached_package_folder_name_buf: bun.PathBuffer = undefined; +pub threadlocal var cached_package_folder_name_buf: bun.PathBuffer = undefined; const Holder = struct { pub var ptr: *PackageManager = undefined; @@ -437,6 +443,96 @@ pub const FailFn = *const fn (*PackageManager, *const Dependency, PackageID, any pub const debug = Output.scoped(.PackageManager, true); +pub fn ensureTempNodeGypScript(this: *PackageManager) !void { + return ensureTempNodeGypScriptOnce.call(.{this}); +} + +var ensureTempNodeGypScriptOnce = bun.once(struct { + pub fn run(manager: *PackageManager) !void { + if (manager.node_gyp_tempdir_name.len > 0) return; + + const tempdir = manager.getTemporaryDirectory(); + var path_buf: bun.PathBuffer = undefined; + const node_gyp_tempdir_name = bun.span(try Fs.FileSystem.instance.tmpname("node-gyp", &path_buf, 12345)); + + // used later for adding to path for scripts + manager.node_gyp_tempdir_name = try manager.allocator.dupe(u8, node_gyp_tempdir_name); + + var node_gyp_tempdir = tempdir.makeOpenPath(manager.node_gyp_tempdir_name, .{}) catch |err| { + if (err == error.EEXIST) { + // it should not exist + Output.prettyErrorln("error: node-gyp tempdir already exists", .{}); + Global.crash(); + } + Output.prettyErrorln("error: {s} creating node-gyp tempdir", .{@errorName(err)}); + Global.crash(); + }; + defer node_gyp_tempdir.close(); + + const file_name = switch (Environment.os) { + else => "node-gyp", + .windows => "node-gyp.cmd", + }; + const mode = switch (Environment.os) { + else => 0o755, + .windows => 0, // windows does not have an executable bit + }; + + var node_gyp_file = node_gyp_tempdir.createFile(file_name, .{ .mode = mode }) catch |err| { + Output.prettyErrorln("error: {s} creating node-gyp tempdir", .{@errorName(err)}); + Global.crash(); + }; + defer node_gyp_file.close(); + + const content = switch (Environment.os) { + .windows => + \\if not defined npm_config_node_gyp ( + \\ bun x --silent node-gyp %* + \\) else ( + \\ node "%npm_config_node_gyp%" %* + \\) + \\ + , + else => + \\#!/bin/sh + \\if [ "x$npm_config_node_gyp" = "x" ]; then + \\ bun x --silent node-gyp $@ + \\else + \\ "$npm_config_node_gyp" $@ + \\fi + \\ + , + }; + + node_gyp_file.writeAll(content) catch |err| { + Output.prettyErrorln("error: {s} writing to " ++ file_name ++ " file", .{@errorName(err)}); + Global.crash(); + }; + + // Add our node-gyp tempdir to the path + const existing_path = manager.env.get("PATH") orelse ""; + var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, existing_path.len + 1 + manager.temp_dir_name.len + 1 + manager.node_gyp_tempdir_name.len); + try PATH.appendSlice(existing_path); + if (existing_path.len > 0 and existing_path[existing_path.len - 1] != std.fs.path.delimiter) + try PATH.append(std.fs.path.delimiter); + try PATH.appendSlice(strings.withoutTrailingSlash(manager.temp_dir_name)); + try PATH.append(std.fs.path.sep); + try PATH.appendSlice(manager.node_gyp_tempdir_name); + try manager.env.map.put("PATH", PATH.items); + + const npm_config_node_gyp = try std.fmt.bufPrint(&path_buf, "{s}{s}{s}{s}{s}", .{ + strings.withoutTrailingSlash(manager.temp_dir_name), + std.fs.path.sep_str, + strings.withoutTrailingSlash(manager.node_gyp_tempdir_name), + std.fs.path.sep_str, + file_name, + }); + + const node_gyp_abs_dir = std.fs.path.dirname(npm_config_node_gyp).?; + try manager.env.map.putAllocKeyAndValue(manager.allocator, "BUN_WHICH_IGNORE_CWD", node_gyp_abs_dir); + } +}.run); + fn httpThreadOnInitError(err: HTTP.InitError, opts: HTTP.HTTPThread.InitOpts) noreturn { switch (err) { error.LoadCAFile => { @@ -728,6 +824,10 @@ pub fn init( bun.spawn.process.WaiterThread.setShouldUseWaiterThread(); } + if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS)) { + bun.sys.WindowsSymlinkOptions.has_failed_to_create_symlink = true; + } + if (PackageManager.verbose_install) { Output.prettyErrorln("Cache Dir: {s}", .{options.cache_directory}); Output.flush(); @@ -1016,18 +1116,20 @@ const default_max_simultaneous_requests_for_bun_install = 64; const default_max_simultaneous_requests_for_bun_install_for_proxies = 64; pub const TaskCallbackList = std.ArrayListUnmanaged(TaskCallbackContext); -const TaskDependencyQueue = std.HashMapUnmanaged(u64, TaskCallbackList, IdentityContext(u64), 80); +const TaskDependencyQueue = std.HashMapUnmanaged(Task.Id, TaskCallbackList, IdentityContext(Task.Id), 80); const PreallocatedTaskStore = bun.HiveArray(Task, 64).Fallback; const PreallocatedNetworkTasks = bun.HiveArray(NetworkTask, 128).Fallback; const ResolveTaskQueue = bun.UnboundedQueue(Task, .next); -const RepositoryMap = std.HashMapUnmanaged(u64, bun.FileDescriptor, IdentityContext(u64), 80); +const RepositoryMap = std.HashMapUnmanaged(Task.Id, bun.FileDescriptor, IdentityContext(Task.Id), 80); const NpmAliasMap = std.HashMapUnmanaged(PackageNameHash, Dependency.Version, IdentityContext(u64), 80); const NetworkQueue = std.fifo.LinearFifo(*NetworkTask, .{ .Static = 32 }); const PatchTaskFifo = std.fifo.LinearFifo(*PatchTask, .{ .Static = 32 }); +// pub const ensureTempNodeGypScript = directories.ensureTempNodeGypScript; + // @sortImports pub const CommandLineArguments = @import("./PackageManager/CommandLineArguments.zig"); @@ -1055,11 +1157,12 @@ pub const cachedNPMPackageFolderPrintBasename = directories.cachedNPMPackageFold pub const cachedTarballFolderName = directories.cachedTarballFolderName; pub const cachedTarballFolderNamePrint = directories.cachedTarballFolderNamePrint; pub const computeCacheDirAndSubpath = directories.computeCacheDirAndSubpath; -pub const ensureTempNodeGypScript = directories.ensureTempNodeGypScript; pub const fetchCacheDirectoryPath = directories.fetchCacheDirectoryPath; pub const getCacheDirectory = directories.getCacheDirectory; +pub const getCacheDirectoryAndAbsPath = directories.getCacheDirectoryAndAbsPath; pub const getTemporaryDirectory = directories.getTemporaryDirectory; pub const globalLinkDir = directories.globalLinkDir; +pub const globalLinkDirAndPath = directories.globalLinkDirAndPath; pub const globalLinkDirPath = directories.globalLinkDirPath; pub const isFolderInCache = directories.isFolderInCache; pub const pathForCachedNPMPath = directories.pathForCachedNPMPath; @@ -1089,6 +1192,7 @@ const lifecycle = @import("PackageManager/PackageManagerLifecycle.zig"); const LifecycleScriptTimeLog = lifecycle.LifecycleScriptTimeLog; pub const determinePreinstallState = lifecycle.determinePreinstallState; pub const ensurePreinstallStateListCapacity = lifecycle.ensurePreinstallStateListCapacity; +pub const findTrustedDependenciesFromUpdateRequests = lifecycle.findTrustedDependenciesFromUpdateRequests; pub const getPreinstallState = lifecycle.getPreinstallState; pub const hasNoMorePendingLifecycleScripts = lifecycle.hasNoMorePendingLifecycleScripts; pub const loadRootLifecycleScripts = lifecycle.loadRootLifecycleScripts; diff --git a/src/install/PackageManager/PackageManagerDirectories.zig b/src/install/PackageManager/PackageManagerDirectories.zig index 7b5ba804b5..98aa1f9cfa 100644 --- a/src/install/PackageManager/PackageManagerDirectories.zig +++ b/src/install/PackageManager/PackageManagerDirectories.zig @@ -5,6 +5,11 @@ pub inline fn getCacheDirectory(this: *PackageManager) std.fs.Dir { }; } +pub inline fn getCacheDirectoryAndAbsPath(this: *PackageManager) struct { FD, bun.AbsPath(.{}) } { + const cache_dir = this.getCacheDirectory(); + return .{ .fromStdDir(cache_dir), .from(this.cache_directory_path) }; +} + pub inline fn getTemporaryDirectory(this: *PackageManager) std.fs.Dir { return this.temp_dir_ orelse brk: { this.temp_dir_ = ensureTemporaryDirectory(this); @@ -356,23 +361,43 @@ pub fn setupGlobalDir(manager: *PackageManager, ctx: Command.Context) !void { manager.options.bin_path = path.ptr[0..path.len :0]; } -pub fn globalLinkDir(this: *PackageManager) !std.fs.Dir { +pub fn globalLinkDir(this: *PackageManager) std.fs.Dir { return this.global_link_dir orelse brk: { - var global_dir = try Options.openGlobalDir(this.options.explicit_global_directory); + var global_dir = Options.openGlobalDir(this.options.explicit_global_directory) catch |err| switch (err) { + error.@"No global directory found" => { + Output.errGeneric("failed to find a global directory for package caching and global link directories", .{}); + Global.exit(1); + }, + else => { + Output.err(err, "failed to open the global directory", .{}); + Global.exit(1); + }, + }; this.global_dir = global_dir; - this.global_link_dir = try global_dir.makeOpenPath("node_modules", .{}); + this.global_link_dir = global_dir.makeOpenPath("node_modules", .{}) catch |err| { + Output.err(err, "failed to open global link dir node_modules at '{}'", .{FD.fromStdDir(global_dir)}); + Global.exit(1); + }; var buf: bun.PathBuffer = undefined; - const _path = try bun.getFdPath(.fromStdDir(this.global_link_dir.?), &buf); - this.global_link_dir_path = try Fs.FileSystem.DirnameStore.instance.append([]const u8, _path); + const _path = bun.getFdPath(.fromStdDir(this.global_link_dir.?), &buf) catch |err| { + Output.err(err, "failed to get the full path of the global directory", .{}); + Global.exit(1); + }; + this.global_link_dir_path = Fs.FileSystem.DirnameStore.instance.append([]const u8, _path) catch bun.outOfMemory(); break :brk this.global_link_dir.?; }; } -pub fn globalLinkDirPath(this: *PackageManager) ![]const u8 { - _ = try this.globalLinkDir(); +pub fn globalLinkDirPath(this: *PackageManager) []const u8 { + _ = this.globalLinkDir(); return this.global_link_dir_path; } +pub fn globalLinkDirAndPath(this: *PackageManager) struct { std.fs.Dir, []const u8 } { + const dir = this.globalLinkDir(); + return .{ dir, this.global_link_dir_path }; +} + pub fn pathForCachedNPMPath( this: *PackageManager, buf: *bun.PathBuffer, @@ -492,14 +517,7 @@ pub fn computeCacheDirAndSubpath( cache_dir = std.fs.cwd(); }, .symlink => { - const directory = manager.globalLinkDir() catch |err| { - const fmt = "\nerror: unable to access global directory while installing {s}: {s}\n"; - const args = .{ name, @errorName(err) }; - - Output.prettyErrorln(fmt, args); - - Global.exit(1); - }; + const directory = manager.globalLinkDir(); const folder = resolution.value.symlink.slice(buf); @@ -507,7 +525,7 @@ pub fn computeCacheDirAndSubpath( cache_dir_subpath = "."; cache_dir = std.fs.cwd(); } else { - const global_link_dir = manager.globalLinkDirPath() catch unreachable; + const global_link_dir = manager.globalLinkDirPath(); var ptr = folder_path_buf; var remain: []u8 = folder_path_buf[0..]; @memcpy(ptr[0..global_link_dir.len], global_link_dir); @@ -719,90 +737,6 @@ const PatchHashFmt = struct { } }; -pub fn ensureTempNodeGypScript(this: *PackageManager) !void { - if (this.node_gyp_tempdir_name.len > 0) return; - - const tempdir = this.getTemporaryDirectory(); - var path_buf: bun.PathBuffer = undefined; - const node_gyp_tempdir_name = bun.span(try Fs.FileSystem.instance.tmpname("node-gyp", &path_buf, 12345)); - - // used later for adding to path for scripts - this.node_gyp_tempdir_name = try this.allocator.dupe(u8, node_gyp_tempdir_name); - - var node_gyp_tempdir = tempdir.makeOpenPath(this.node_gyp_tempdir_name, .{}) catch |err| { - if (err == error.EEXIST) { - // it should not exist - Output.prettyErrorln("error: node-gyp tempdir already exists", .{}); - Global.crash(); - } - Output.prettyErrorln("error: {s} creating node-gyp tempdir", .{@errorName(err)}); - Global.crash(); - }; - defer node_gyp_tempdir.close(); - - const file_name = switch (Environment.os) { - else => "node-gyp", - .windows => "node-gyp.cmd", - }; - const mode = switch (Environment.os) { - else => 0o755, - .windows => 0, // windows does not have an executable bit - }; - - var node_gyp_file = node_gyp_tempdir.createFile(file_name, .{ .mode = mode }) catch |err| { - Output.prettyErrorln("error: {s} creating node-gyp tempdir", .{@errorName(err)}); - Global.crash(); - }; - defer node_gyp_file.close(); - - const content = switch (Environment.os) { - .windows => - \\if not defined npm_config_node_gyp ( - \\ bun x --silent node-gyp %* - \\) else ( - \\ node "%npm_config_node_gyp%" %* - \\) - \\ - , - else => - \\#!/bin/sh - \\if [ "x$npm_config_node_gyp" = "x" ]; then - \\ bun x --silent node-gyp $@ - \\else - \\ "$npm_config_node_gyp" $@ - \\fi - \\ - , - }; - - node_gyp_file.writeAll(content) catch |err| { - Output.prettyErrorln("error: {s} writing to " ++ file_name ++ " file", .{@errorName(err)}); - Global.crash(); - }; - - // Add our node-gyp tempdir to the path - const existing_path = this.env.get("PATH") orelse ""; - var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, existing_path.len + 1 + this.temp_dir_name.len + 1 + this.node_gyp_tempdir_name.len); - try PATH.appendSlice(existing_path); - if (existing_path.len > 0 and existing_path[existing_path.len - 1] != std.fs.path.delimiter) - try PATH.append(std.fs.path.delimiter); - try PATH.appendSlice(strings.withoutTrailingSlash(this.temp_dir_name)); - try PATH.append(std.fs.path.sep); - try PATH.appendSlice(this.node_gyp_tempdir_name); - try this.env.map.put("PATH", PATH.items); - - const npm_config_node_gyp = try std.fmt.bufPrint(&path_buf, "{s}{s}{s}{s}{s}", .{ - strings.withoutTrailingSlash(this.temp_dir_name), - std.fs.path.sep_str, - strings.withoutTrailingSlash(this.node_gyp_tempdir_name), - std.fs.path.sep_str, - file_name, - }); - - const node_gyp_abs_dir = std.fs.path.dirname(npm_config_node_gyp).?; - try this.env.map.putAllocKeyAndValue(this.allocator, "BUN_WHICH_IGNORE_CWD", node_gyp_abs_dir); -} - var using_fallback_temp_dir: bool = false; // @sortImports @@ -821,7 +755,6 @@ const Progress = bun.Progress; const default_allocator = bun.default_allocator; const string = bun.string; const stringZ = bun.stringZ; -const strings = bun.strings; const Command = bun.CLI.Command; const File = bun.sys.File; diff --git a/src/install/PackageManager/PackageManagerEnqueue.zig b/src/install/PackageManager/PackageManagerEnqueue.zig index 739582561e..ae89ab3ed0 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.zig +++ b/src/install/PackageManager/PackageManagerEnqueue.zig @@ -209,7 +209,7 @@ pub fn enqueueGitForCheckout( pub fn enqueueParseNPMPackage( this: *PackageManager, - task_id: u64, + task_id: Task.Id, name: strings.StringOrTinyString, network_task: *NetworkTask, ) *ThreadPool.Task { @@ -652,7 +652,7 @@ pub fn enqueueDependencyWithMainAndSuccessFn( const name_str = this.lockfile.str(&name); const task_id = Task.Id.forManifest(name_str); - if (comptime Environment.allow_assert) bun.assert(task_id != 0); + if (comptime Environment.allow_assert) bun.assert(task_id.get() != 0); if (comptime Environment.allow_assert) debug( @@ -1132,7 +1132,7 @@ pub fn enqueueExtractNPMPackage( fn enqueueGitClone( this: *PackageManager, - task_id: u64, + task_id: Task.Id, name: string, repository: *const Repository, dep_id: DependencyID, @@ -1182,7 +1182,7 @@ fn enqueueGitClone( pub fn enqueueGitCheckout( this: *PackageManager, - task_id: u64, + task_id: Task.Id, dir: bun.FileDescriptor, dependency_id: DependencyID, name: string, @@ -1238,7 +1238,7 @@ pub fn enqueueGitCheckout( fn enqueueLocalTarball( this: *PackageManager, - task_id: u64, + task_id: Task.Id, dependency_id: DependencyID, name: string, path: string, @@ -1641,6 +1641,12 @@ fn getOrPutResolvedPackage( // .auto, // ); }; + + // if (strings.eqlLong(strings.withoutTrailingSlash(folder_path_abs), strings.withoutTrailingSlash(FileSystem.instance.top_level_dir), true)) { + // successFn(this, dependency_id, 0); + // return .{ .package = this.lockfile.packages.get(0) }; + // } + break :res FolderResolution.getOrPut(.{ .relative = .folder }, version, folder_path_abs, this); } @@ -1720,7 +1726,7 @@ fn getOrPutResolvedPackage( } }, .symlink => { - const res = FolderResolution.getOrPut(.{ .global = try this.globalLinkDirPath() }, version, this.lockfile.str(&version.value.symlink), this); + const res = FolderResolution.getOrPut(.{ .global = this.globalLinkDirPath() }, version, this.lockfile.str(&version.value.symlink), this); switch (res) { .err => |err| return err, diff --git a/src/install/PackageManager/PackageManagerLifecycle.zig b/src/install/PackageManager/PackageManagerLifecycle.zig index dff3c0ae80..1ba1aec35d 100644 --- a/src/install/PackageManager/PackageManagerLifecycle.zig +++ b/src/install/PackageManager/PackageManagerLifecycle.zig @@ -59,7 +59,7 @@ pub fn ensurePreinstallStateListCapacity(this: *PackageManager, count: usize) vo @memset(this.preinstall_state.items[offset..], PreinstallState.unknown); } -pub fn setPreinstallState(this: *PackageManager, package_id: PackageID, lockfile: *Lockfile, value: PreinstallState) void { +pub fn setPreinstallState(this: *PackageManager, package_id: PackageID, lockfile: *const Lockfile, value: PreinstallState) void { this.ensurePreinstallStateListCapacity(lockfile.packages.len); this.preinstall_state.items[package_id] = value; } @@ -218,7 +218,9 @@ pub fn loadRootLifecycleScripts(this: *PackageManager, root_package: Package) vo const buf = this.lockfile.buffers.string_bytes.items; // need to clone because this is a copy before Lockfile.cleanWithLogger const name = root_package.name.slice(buf); - const top_level_dir_without_trailing_slash = strings.withoutTrailingSlash(FileSystem.instance.top_level_dir); + + var top_level_dir: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer top_level_dir.deinit(); if (root_package.scripts.hasAny()) { const add_node_gyp_rebuild_script = root_package.scripts.install.isEmpty() and root_package.scripts.preinstall.isEmpty() and Syscall.exists(binding_dot_gyp_path); @@ -226,7 +228,7 @@ pub fn loadRootLifecycleScripts(this: *PackageManager, root_package: Package) vo this.root_lifecycle_scripts = root_package.scripts.createList( this.lockfile, buf, - top_level_dir_without_trailing_slash, + &top_level_dir, name, .root, add_node_gyp_rebuild_script, @@ -237,7 +239,7 @@ pub fn loadRootLifecycleScripts(this: *PackageManager, root_package: Package) vo this.root_lifecycle_scripts = root_package.scripts.createList( this.lockfile, buf, - top_level_dir_without_trailing_slash, + &top_level_dir, name, .root, true, @@ -252,6 +254,7 @@ pub fn spawnPackageLifecycleScripts( list: Lockfile.Package.Scripts.List, optional: bool, foreground: bool, + install_ctx: ?LifecycleScriptSubprocess.InstallCtx, ) !void { const log_level = this.options.log_level; var any_scripts = false; @@ -268,55 +271,99 @@ pub fn spawnPackageLifecycleScripts( try this.ensureTempNodeGypScript(); const cwd = list.cwd; - const this_transpiler = try this.configureEnvForScripts(ctx, log_level); - const original_path = this_transpiler.env.get("PATH") orelse ""; + var this_transpiler = try this.configureEnvForScripts(ctx, log_level); - var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, original_path.len + 1 + "node_modules/.bin".len + cwd.len + 1); - var current_dir: ?*DirInfo = this_transpiler.resolver.readDirInfo(cwd) catch null; - bun.assert(current_dir != null); - while (current_dir) |dir| { - if (PATH.items.len > 0 and PATH.items[PATH.items.len - 1] != std.fs.path.delimiter) { - try PATH.append(std.fs.path.delimiter); - } - try PATH.appendSlice(strings.withoutTrailingSlash(dir.abs_path)); - if (!(dir.abs_path.len == 1 and dir.abs_path[0] == std.fs.path.sep)) { - try PATH.append(std.fs.path.sep); - } - try PATH.appendSlice(this.options.bin_path); - current_dir = dir.getParent(); + var script_env = try this_transpiler.env.map.cloneWithAllocator(bun.default_allocator); + defer script_env.map.deinit(); + + const original_path = script_env.get("PATH") orelse ""; + + var PATH: bun.EnvPath(.{}) = try .initCapacity(bun.default_allocator, original_path.len + 1 + "node_modules/.bin".len + cwd.len + 1); + defer PATH.deinit(); + + var parent: ?string = cwd; + + while (parent) |dir| { + var builder = PATH.pathComponentBuilder(); + builder.append(dir); + builder.append("node_modules/.bin"); + try builder.apply(); + + parent = std.fs.path.dirname(dir); } - if (original_path.len > 0) { - if (PATH.items.len > 0 and PATH.items[PATH.items.len - 1] != std.fs.path.delimiter) { - try PATH.append(std.fs.path.delimiter); + try PATH.append(original_path); + try script_env.put("PATH", PATH.slice()); + + const envp = try script_env.createNullDelimitedEnvMap(this.allocator); + + const shell_bin = shell_bin: { + if (comptime Environment.isWindows) { + break :shell_bin null; } - try PATH.appendSlice(original_path); - } + if (this.env.get("PATH")) |env_path| { + break :shell_bin bun.CLI.RunCommand.findShell(env_path, cwd); + } - this_transpiler.env.map.put("PATH", PATH.items) catch unreachable; + break :shell_bin null; + }; - // Run node-gyp jobs in parallel. - // https://github.com/nodejs/node-gyp/blob/7d883b5cf4c26e76065201f85b0be36d5ebdcc0e/lib/build.js#L150-L184 - const thread_count = bun.getThreadCount(); - if (thread_count > 2) { - if (!this_transpiler.env.has("JOBS")) { - var int_buf: [10]u8 = undefined; - const jobs_str = std.fmt.bufPrint(&int_buf, "{d}", .{thread_count}) catch unreachable; - this_transpiler.env.map.putAllocValue(bun.default_allocator, "JOBS", jobs_str) catch unreachable; + try LifecycleScriptSubprocess.spawnPackageScripts(this, list, envp, shell_bin, optional, log_level, foreground, install_ctx); +} + +pub fn findTrustedDependenciesFromUpdateRequests(this: *PackageManager) std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void) { + const parts = this.lockfile.packages.slice(); + // find all deps originating from --trust packages from cli + var set: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void) = .{}; + if (this.options.do.trust_dependencies_from_args and this.lockfile.packages.len > 0) { + const root_deps = parts.items(.dependencies)[this.root_package_id.get(this.lockfile, this.workspace_name_hash)]; + var dep_id = root_deps.off; + const end = dep_id +| root_deps.len; + while (dep_id < end) : (dep_id += 1) { + const root_dep = this.lockfile.buffers.dependencies.items[dep_id]; + for (this.update_requests) |request| { + if (request.matches(root_dep, this.lockfile.buffers.string_bytes.items)) { + const package_id = this.lockfile.buffers.resolutions.items[dep_id]; + if (package_id == invalid_package_id) continue; + + const entry = set.getOrPut(this.lockfile.allocator, @truncate(root_dep.name_hash)) catch bun.outOfMemory(); + if (!entry.found_existing) { + const dependency_slice = parts.items(.dependencies)[package_id]; + addDependenciesToSet(&set, this.lockfile, dependency_slice); + } + break; + } + } } } - const envp = try this_transpiler.env.map.createNullDelimitedEnvMap(this.allocator); - try this_transpiler.env.map.put("PATH", original_path); - PATH.deinit(); + return set; +} - try LifecycleScriptSubprocess.spawnPackageScripts(this, list, envp, optional, log_level, foreground); +fn addDependenciesToSet( + names: *std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void), + lockfile: *Lockfile, + dependencies_slice: Lockfile.DependencySlice, +) void { + const begin = dependencies_slice.off; + const end = begin +| dependencies_slice.len; + var dep_id = begin; + while (dep_id < end) : (dep_id += 1) { + const package_id = lockfile.buffers.resolutions.items[dep_id]; + if (package_id == invalid_package_id) continue; + + const dep = lockfile.buffers.dependencies.items[dep_id]; + const entry = names.getOrPut(lockfile.allocator, @truncate(dep.name_hash)) catch bun.outOfMemory(); + if (!entry.found_existing) { + const dependency_slice = lockfile.packages.items(.dependencies)[package_id]; + addDependenciesToSet(names, lockfile, dependency_slice); + } + } } // @sortImports -const DirInfo = @import("../../resolver/dir_info.zig"); const std = @import("std"); const bun = @import("bun"); @@ -326,7 +373,6 @@ const Path = bun.path; const Syscall = bun.sys; const default_allocator = bun.default_allocator; const string = bun.string; -const strings = bun.strings; const Command = bun.CLI.Command; const Semver = bun.Semver; @@ -339,6 +385,8 @@ const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; const PackageID = bun.install.PackageID; const PackageManager = bun.install.PackageManager; const PreinstallState = bun.install.PreinstallState; +const TruncatedPackageNameHash = bun.install.TruncatedPackageNameHash; +const invalid_package_id = bun.install.invalid_package_id; const Lockfile = bun.install.Lockfile; const Package = Lockfile.Package; diff --git a/src/install/PackageManager/install_with_manager.zig b/src/install/PackageManager/install_with_manager.zig index 34e1826504..c68a904239 100644 --- a/src/install/PackageManager/install_with_manager.zig +++ b/src/install/PackageManager/install_with_manager.zig @@ -199,6 +199,8 @@ pub fn installWithManager( lockfile.catalogs.count(&lockfile, builder); maybe_root.scripts.count(lockfile.buffers.string_bytes.items, *Lockfile.StringBuilder, builder); + manager.lockfile.node_linker = lockfile.node_linker; + const off = @as(u32, @truncate(manager.lockfile.buffers.dependencies.items.len)); const len = @as(u32, @truncate(new_dependencies.len)); var packages = manager.lockfile.packages.slice(); @@ -468,7 +470,6 @@ pub fn installWithManager( this, .{ .onExtract = {}, - .onPatch = {}, .onResolve = {}, .onPackageManifestError = {}, .onPackageDownloadError = {}, @@ -735,16 +736,33 @@ pub fn installWithManager( } } - var install_summary = PackageInstall.Summary{}; - if (manager.options.do.install_packages) { - install_summary = try @import("../hoisted_install.zig").installHoistedPackages( + const install_summary: PackageInstall.Summary = install_summary: { + if (!manager.options.do.install_packages) { + break :install_summary .{}; + } + + if (manager.lockfile.node_linker == .hoisted or + // TODO + manager.lockfile.node_linker == .auto) + { + break :install_summary try installHoistedPackages( + manager, + ctx, + workspace_filters.items, + install_root_dependencies, + log_level, + ); + } + + break :install_summary installIsolatedPackages( manager, ctx, - workspace_filters.items, install_root_dependencies, - log_level, - ); - } + workspace_filters.items, + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; + }; if (log_level != .silent) { try manager.log.print(Output.errorWriter()); @@ -821,7 +839,7 @@ pub fn installWithManager( // have finished, and lockfiles have been saved const optional = false; const output_in_foreground = true; - try manager.spawnPackageLifecycleScripts(ctx, scripts, optional, output_in_foreground); + try manager.spawnPackageLifecycleScripts(ctx, scripts, optional, output_in_foreground, null); while (manager.pending_lifecycle_script_tasks.load(.monotonic) > 0) { manager.reportSlowLifecycleScripts(); @@ -964,6 +982,8 @@ fn printBlockedPackagesInfo(summary: *const PackageInstall.Summary, global: bool // @sortImports const std = @import("std"); +const installHoistedPackages = @import("../hoisted_install.zig").installHoistedPackages; +const installIsolatedPackages = @import("../isolated_install.zig").installIsolatedPackages; const bun = @import("bun"); const Environment = bun.Environment; diff --git a/src/install/PackageManager/patchPackage.zig b/src/install/PackageManager/patchPackage.zig index cb78280b21..f443ea0a85 100644 --- a/src/install/PackageManager/patchPackage.zig +++ b/src/install/PackageManager/patchPackage.zig @@ -760,10 +760,9 @@ fn overwritePackageInNodeModulesFolder( var pathbuf2: bun.PathBuffer = undefined; // _ = pathbuf; // autofix - while (try walker.next()) |entry| { + while (try walker.next().unwrap()) |entry| { if (entry.kind != .file) continue; real_file_count += 1; - const openFile = std.fs.Dir.openFile; const createFile = std.fs.Dir.createFile; // 1. rename original file in node_modules to tmp_dir_in_node_modules @@ -807,7 +806,7 @@ fn overwritePackageInNodeModulesFolder( Global.crash(); }; } else if (comptime Environment.isPosix) { - var in_file = try openFile(entry.dir, entry.basename, .{ .mode = .read_only }); + var in_file = try entry.dir.openat(entry.basename, bun.O.RDONLY, 0).unwrap(); defer in_file.close(); @memcpy(pathbuf[0..entry.path.len], entry.path); @@ -824,10 +823,10 @@ fn overwritePackageInNodeModulesFolder( var outfile = try createFile(destination_dir_, entry.path, .{}); defer outfile.close(); - const stat = in_file.stat() catch continue; + const stat = in_file.stat().unwrap() catch continue; _ = bun.c.fchmod(outfile.handle, @intCast(stat.mode)); - bun.copyFileWithState(.fromStdFile(in_file), .fromStdFile(outfile), ©_file_state).unwrap() catch |err| { + bun.copyFileWithState(in_file, .fromStdFile(outfile), ©_file_state).unwrap() catch |err| { Output.prettyError("{s}: copying file {}", .{ @errorName(err), bun.fmt.fmtOSPath(entry.path, .{}) }); Global.crash(); }; @@ -840,7 +839,7 @@ fn overwritePackageInNodeModulesFolder( var pkg_in_cache_dir = try cache_dir.openDir(cache_dir_subpath, .{ .iterate = true }); defer pkg_in_cache_dir.close(); - var walker = Walker.walk(pkg_in_cache_dir, manager.allocator, &.{}, IGNORED_PATHS) catch bun.outOfMemory(); + var walker = Walker.walk(.fromStdDir(pkg_in_cache_dir), manager.allocator, &.{}, IGNORED_PATHS) catch bun.outOfMemory(); defer walker.deinit(); var buf1: if (bun.Environment.isWindows) bun.WPathBuffer else void = undefined; diff --git a/src/install/PackageManager/processDependencyList.zig b/src/install/PackageManager/processDependencyList.zig index 663305d6f5..f2ce90170b 100644 --- a/src/install/PackageManager/processDependencyList.zig +++ b/src/install/PackageManager/processDependencyList.zig @@ -291,8 +291,8 @@ pub fn processPeerDependencyList( pub fn processDependencyList( this: *PackageManager, dep_list: TaskCallbackList, - comptime Context: type, - ctx: Context, + comptime Ctx: type, + ctx: Ctx, comptime callbacks: anytype, install_peer: bool, ) !void { diff --git a/src/install/PackageManager/runTasks.zig b/src/install/PackageManager/runTasks.zig index 9f6ebcff78..000f3d8907 100644 --- a/src/install/PackageManager/runTasks.zig +++ b/src/install/PackageManager/runTasks.zig @@ -1,7 +1,7 @@ pub fn runTasks( manager: *PackageManager, - comptime ExtractCompletionContext: type, - extract_ctx: ExtractCompletionContext, + comptime Ctx: type, + extract_ctx: Ctx, comptime callbacks: anytype, install_peer: bool, log_level: Options.LogLevel, @@ -33,7 +33,7 @@ pub fn runTasks( var patch_tasks_iter = patch_tasks_batch.iterator(); while (patch_tasks_iter.next()) |ptask| { if (comptime Environment.allow_assert) bun.assert(manager.pendingTaskCount() > 0); - _ = manager.decrementPendingTasks(); + manager.decrementPendingTasks(); defer ptask.deinit(); try ptask.runFromMainThread(manager, log_level); if (ptask.callback == .apply) { @@ -42,7 +42,7 @@ pub fn runTasks( if (ptask.callback.apply.task_id) |task_id| { _ = task_id; // autofix - } else if (ExtractCompletionContext == *PackageInstaller) { + } else if (Ctx == *PackageInstaller) { if (ptask.callback.apply.install_context) |*ctx| { var installer: *PackageInstaller = extract_ctx; const path = ctx.path; @@ -68,11 +68,41 @@ pub fn runTasks( } } + if (Ctx == *Store.Installer) { + const installer: *Store.Installer = extract_ctx; + const batch = installer.tasks.popBatch(); + var iter = batch.iterator(); + while (iter.next()) |task| { + defer installer.preallocated_tasks.put(task); + switch (task.result) { + .none => { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(false, @src()); + } + installer.onTaskComplete(task.entry_id, .success); + }, + .err => |err| { + installer.onTaskFail(task.entry_id, err); + }, + .blocked => { + installer.onTaskBlocked(task.entry_id); + }, + .done => { + if (comptime Environment.ci_assert) { + const step = installer.store.entries.items(.step)[task.entry_id.get()].load(.monotonic); + bun.assertWithLocation(step == .done, @src()); + } + installer.onTaskComplete(task.entry_id, .success); + }, + } + } + } + var network_tasks_batch = manager.async_network_task_queue.popBatch(); var network_tasks_iter = network_tasks_batch.iterator(); while (network_tasks_iter.next()) |task| { if (comptime Environment.allow_assert) bun.assert(manager.pendingTaskCount() > 0); - _ = manager.decrementPendingTasks(); + manager.decrementPendingTasks(); // We cannot free the network task at the end of this scope. // It may continue to be referenced in a future task. @@ -256,7 +286,7 @@ pub fn runTasks( try manager.processDependencyList( dependency_list, - ExtractCompletionContext, + Ctx, extract_ctx, callbacks, install_peer, @@ -449,7 +479,7 @@ pub fn runTasks( while (resolve_tasks_iter.next()) |task| { if (comptime Environment.allow_assert) bun.assert(manager.pendingTaskCount() > 0); defer manager.preallocated_resolve_tasks.put(task); - _ = manager.decrementPendingTasks(); + manager.decrementPendingTasks(); if (task.log.msgs.items.len > 0) { try task.log.print(Output.errorWriter()); @@ -500,7 +530,7 @@ pub fn runTasks( const dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; - try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer); + try manager.processDependencyList(dependency_list, Ctx, extract_ctx, callbacks, install_peer); if (log_level.showProgress()) { if (!has_updated_this_run) { @@ -561,14 +591,26 @@ pub fn runTasks( manager.extracted_count += 1; bun.Analytics.Features.extracted_packages += 1; - if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { - extract_ctx.fixCachedLockfilePackageSlices(); - callbacks.onExtract( - extract_ctx, - dependency_id, - &task.data.extract, - log_level, - ); + if (comptime @TypeOf(callbacks.onExtract) != void) { + switch (Ctx) { + *PackageInstaller => { + extract_ctx.fixCachedLockfilePackageSlices(); + callbacks.onExtract( + extract_ctx, + task.id, + dependency_id, + &task.data.extract, + log_level, + ); + }, + *Store.Installer => { + callbacks.onExtract( + extract_ctx, + task.id, + ); + }, + else => @compileError("unexpected context type"), + } } else if (manager.processExtractedTarballPackage(&package_id, dependency_id, resolution, &task.data.extract, log_level)) |pkg| handle_pkg: { // In the middle of an install, you could end up needing to downlaod the github tarball for a dependency // We need to make sure we resolve the dependencies first before calling the onExtract callback @@ -626,11 +668,6 @@ pub fn runTasks( manager.setPreinstallState(package_id, manager.lockfile, .done); - if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext != *PackageInstaller) { - // handled *PackageInstaller above - callbacks.onExtract(extract_ctx, dependency_id, &task.data.extract, log_level); - } - if (log_level.showProgress()) { if (!has_updated_this_run) { manager.setNodeName(manager.downloads_node.?, alias, ProgressStrings.extract_emoji, true); @@ -671,7 +708,7 @@ pub fn runTasks( continue; } - if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { + if (comptime @TypeOf(callbacks.onExtract) != void and Ctx == *PackageInstaller) { // Installing! // this dependency might be something other than a git dependency! only need the name and // behavior, use the resolution from the task. @@ -712,7 +749,7 @@ pub fn runTasks( const dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; - try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer); + try manager.processDependencyList(dependency_list, Ctx, extract_ctx, callbacks, install_peer); } if (log_level.showProgress()) { @@ -745,20 +782,32 @@ pub fn runTasks( continue; } - if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) { + if (comptime @TypeOf(callbacks.onExtract) != void) { // We've populated the cache, package already exists in memory. Call the package installer callback // and don't enqueue dependencies + switch (Ctx) { + *PackageInstaller => { - // TODO(dylan-conway) most likely don't need to call this now that the package isn't appended, but - // keeping just in case for now - extract_ctx.fixCachedLockfilePackageSlices(); + // TODO(dylan-conway) most likely don't need to call this now that the package isn't appended, but + // keeping just in case for now + extract_ctx.fixCachedLockfilePackageSlices(); - callbacks.onExtract( - extract_ctx, - git_checkout.dependency_id, - &task.data.git_checkout, - log_level, - ); + callbacks.onExtract( + extract_ctx, + task.id, + git_checkout.dependency_id, + &task.data.git_checkout, + log_level, + ); + }, + *Store.Installer => { + callbacks.onExtract( + extract_ctx, + task.id, + ); + }, + else => @compileError("unexpected context type"), + } } else if (manager.processExtractedTarballPackage( &package_id, git_checkout.dependency_id, @@ -795,13 +844,8 @@ pub fn runTasks( } } - if (comptime @TypeOf(callbacks.onExtract) != void) { - callbacks.onExtract( - extract_ctx, - git_checkout.dependency_id, - &task.data.git_checkout, - log_level, - ); + if (@TypeOf(callbacks.onExtract) != void) { + @compileError("ctx should be void"); } } @@ -825,8 +869,8 @@ pub inline fn incrementPendingTasks(manager: *PackageManager, count: u32) u32 { return manager.pending_tasks.fetchAdd(count, .monotonic); } -pub inline fn decrementPendingTasks(manager: *PackageManager) u32 { - return manager.pending_tasks.fetchSub(1, .monotonic); +pub inline fn decrementPendingTasks(manager: *PackageManager) void { + _ = manager.pending_tasks.fetchSub(1, .monotonic); } pub fn flushNetworkQueue(this: *PackageManager) void { @@ -934,7 +978,7 @@ pub fn allocGitHubURL(this: *const PackageManager, repository: *const Repository ) catch unreachable; } -pub fn hasCreatedNetworkTask(this: *PackageManager, task_id: u64, is_required: bool) bool { +pub fn hasCreatedNetworkTask(this: *PackageManager, task_id: Task.Id, is_required: bool) bool { const gpe = this.network_dedupe_map.getOrPut(task_id) catch bun.outOfMemory(); // if there's an existing network task that is optional, we want to make it non-optional if this one would be required @@ -946,13 +990,13 @@ pub fn hasCreatedNetworkTask(this: *PackageManager, task_id: u64, is_required: b return gpe.found_existing; } -pub fn isNetworkTaskRequired(this: *const PackageManager, task_id: u64) bool { +pub fn isNetworkTaskRequired(this: *const PackageManager, task_id: Task.Id) bool { return (this.network_dedupe_map.get(task_id) orelse return true).is_required; } pub fn generateNetworkTaskForTarball( this: *PackageManager, - task_id: u64, + task_id: Task.Id, url: string, is_required: bool, dependency_id: DependencyID, @@ -1035,6 +1079,7 @@ const PackageID = bun.install.PackageID; const PackageManifestError = bun.install.PackageManifestError; const PatchTask = bun.install.PatchTask; const Repository = bun.install.Repository; +const Store = bun.install.Store; const Task = bun.install.Task; const invalid_package_id = bun.install.invalid_package_id; diff --git a/src/install/PackageManagerTask.zig b/src/install/PackageManagerTask.zig index 7f3ce838b1..c0d17a0d06 100644 --- a/src/install/PackageManagerTask.zig +++ b/src/install/PackageManagerTask.zig @@ -7,64 +7,66 @@ data: Data, status: Status = Status.waiting, threadpool_task: ThreadPool.Task = ThreadPool.Task{ .callback = &callback }, log: logger.Log, -id: u64, +id: Id, err: ?anyerror = null, package_manager: *PackageManager, apply_patch_task: ?*PatchTask = null, next: ?*Task = null, /// An ID that lets us register a callback without keeping the same pointer around -pub fn NewID(comptime Hasher: type, comptime IDType: type) type { - return struct { - pub const Type = IDType; - pub fn forNPMPackage(package_name: string, package_version: Semver.Version) IDType { - var hasher = Hasher.init(0); - hasher.update("npm-package:"); - hasher.update(package_name); - hasher.update("@"); - hasher.update(std.mem.asBytes(&package_version)); - return hasher.final(); - } +pub const Id = enum(u64) { + _, - pub fn forBinLink(package_id: PackageID) IDType { - var hasher = Hasher.init(0); - hasher.update("bin-link:"); - hasher.update(std.mem.asBytes(&package_id)); - return hasher.final(); - } + pub fn get(this: @This()) u64 { + return @intFromEnum(this); + } - pub fn forManifest(name: string) IDType { - var hasher = Hasher.init(0); - hasher.update("manifest:"); - hasher.update(name); - return hasher.final(); - } + pub fn forNPMPackage(package_name: string, package_version: Semver.Version) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update("npm-package:"); + hasher.update(package_name); + hasher.update("@"); + hasher.update(std.mem.asBytes(&package_version)); + return @enumFromInt(hasher.final()); + } - pub fn forTarball(url: string) IDType { - var hasher = Hasher.init(0); - hasher.update("tarball:"); - hasher.update(url); - return hasher.final(); - } + pub fn forBinLink(package_id: PackageID) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update("bin-link:"); + hasher.update(std.mem.asBytes(&package_id)); + return @enumFromInt(hasher.final()); + } - // These cannot change: - // We persist them to the filesystem. - pub fn forGitClone(url: string) IDType { - var hasher = Hasher.init(0); - hasher.update(url); - return @as(u64, 4 << 61) | @as(u64, @as(u61, @truncate(hasher.final()))); - } + pub fn forManifest(name: string) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update("manifest:"); + hasher.update(name); + return @enumFromInt(hasher.final()); + } - pub fn forGitCheckout(url: string, resolved: string) IDType { - var hasher = Hasher.init(0); - hasher.update(url); - hasher.update("@"); - hasher.update(resolved); - return @as(u64, 5 << 61) | @as(u64, @as(u61, @truncate(hasher.final()))); - } - }; -} -pub const Id = NewID(bun.Wyhash11, u64); + pub fn forTarball(url: string) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update("tarball:"); + hasher.update(url); + return @enumFromInt(hasher.final()); + } + + // These cannot change: + // We persist them to the filesystem. + pub fn forGitClone(url: string) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update(url); + return @enumFromInt(@as(u64, 4 << 61) | @as(u64, @as(u61, @truncate(hasher.final())))); + } + + pub fn forGitCheckout(url: string, resolved: string) Id { + var hasher = bun.Wyhash11.init(0); + hasher.update(url); + hasher.update("@"); + hasher.update(resolved); + return @enumFromInt(@as(u64, 5 << 61) | @as(u64, @as(u61, @truncate(hasher.final())))); + } +}; pub fn callback(task: *ThreadPool.Task) void { Output.Source.configureThread(); diff --git a/src/install/bin.zig b/src/install/bin.zig index 34d8f4ccb0..225c2eb075 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -563,8 +563,7 @@ pub const Bin = extern struct { // linking each tree. seen: ?*bun.StringHashMap(void), - node_modules: bun.FileDescriptor, - node_modules_path: []const u8, + node_modules_path: *bun.AbsPath(.{}), /// Used for generating relative paths package_name: strings.StringOrTinyString, @@ -692,7 +691,11 @@ pub const Bin = extern struct { return; } - bun.makePath(this.node_modules.stdDir(), ".bin") catch {}; + const node_modules_path_save = this.node_modules_path.save(); + this.node_modules_path.append(".bin"); + bun.makePath(std.fs.cwd(), this.node_modules_path.slice()) catch {}; + node_modules_path_save.restore(); + break :bunx_file bun.sys.File.openatOSPath(bun.invalid_fd, abs_bunx_file, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664).unwrap() catch |real_err| { this.err = real_err; return; @@ -785,7 +788,11 @@ pub const Bin = extern struct { return; } - bun.makePath(this.node_modules.stdDir(), ".bin") catch {}; + const node_modules_path_save = this.node_modules_path.save(); + this.node_modules_path.append(".bin"); + bun.makePath(std.fs.cwd(), this.node_modules_path.slice()) catch {}; + node_modules_path_save.restore(); + switch (bun.sys.symlink(rel_target, abs_dest)) { .err => |real_error| { // It was just created, no need to delete destination and symlink again @@ -815,7 +822,7 @@ pub const Bin = extern struct { /// uses `this.abs_target_buf` pub fn buildTargetPackageDir(this: *const Linker) []const u8 { - const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path); + const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path.slice()); var remain = this.abs_target_buf; @@ -834,7 +841,7 @@ pub const Bin = extern struct { } pub fn buildDestinationDir(this: *const Linker, global: bool) []u8 { - const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path); + const dest_dir_without_trailing_slash = strings.withoutTrailingSlash(this.node_modules_path.slice()); var remain = this.abs_dest_buf; if (global) { diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 217160edb7..4ea40ac399 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -165,6 +165,12 @@ pub fn toExternal(this: Dependency) External { return bytes; } +// Needed when a dependency uses workspace: protocol and isn't +// marked with workspace behavior. +pub fn isWorkspaceDep(this: *const Dependency) bool { + return this.behavior.isWorkspace() or this.version.tag == .workspace; +} + pub inline fn isSCPLikePath(dependency: string) bool { // Shortest valid expression: h:p if (dependency.len < 3) return false; @@ -1399,6 +1405,14 @@ pub const Behavior = packed struct(u8) { return .eq; } + if (lhs.isWorkspaceOnly() != rhs.isWorkspaceOnly()) { + // ensure isWorkspaceOnly deps are placed at the beginning + return if (lhs.isWorkspaceOnly()) + .lt + else + .gt; + } + if (lhs.isProd() != rhs.isProd()) { return if (lhs.isProd()) .gt diff --git a/src/install/hoisted_install.zig b/src/install/hoisted_install.zig index 79cce4ef2e..8cca648f7c 100644 --- a/src/install/hoisted_install.zig +++ b/src/install/hoisted_install.zig @@ -16,32 +16,9 @@ const ProgressStrings = PackageManager.ProgressStrings; const Bin = install.Bin; const PackageInstaller = PackageManager.PackageInstaller; const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const TruncatedPackageNameHash = install.TruncatedPackageNameHash; const PackageID = install.PackageID; -const invalid_package_id = install.invalid_package_id; const TreeContext = PackageInstaller.TreeContext; -fn addDependenciesToSet( - names: *std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void), - lockfile: *Lockfile, - dependencies_slice: Lockfile.DependencySlice, -) void { - const begin = dependencies_slice.off; - const end = begin +| dependencies_slice.len; - var dep_id = begin; - while (dep_id < end) : (dep_id += 1) { - const package_id = lockfile.buffers.resolutions.items[dep_id]; - if (package_id == invalid_package_id) continue; - - const dep = lockfile.buffers.dependencies.items[dep_id]; - const entry = names.getOrPut(lockfile.allocator, @truncate(dep.name_hash)) catch bun.outOfMemory(); - if (!entry.found_existing) { - const dependency_slice = lockfile.packages.items(.dependencies)[package_id]; - addDependenciesToSet(names, lockfile, dependency_slice); - } - } -} - pub fn installHoistedPackages( this: *PackageManager, ctx: Command.Context, @@ -49,6 +26,8 @@ pub fn installHoistedPackages( install_root_dependencies: bool, log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { + bun.Analytics.Features.hoisted_bun_install += 1; + const original_trees = this.lockfile.buffers.trees; const original_tree_dep_ids = this.lockfile.buffers.hoisted_dependencies; @@ -182,35 +161,6 @@ pub fn installHoistedPackages( // to make mistakes harder var parts = this.lockfile.packages.slice(); - const trusted_dependencies_from_update_requests: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void) = trusted_deps: { - - // find all deps originating from --trust packages from cli - var set: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void) = .{}; - if (this.options.do.trust_dependencies_from_args and this.lockfile.packages.len > 0) { - const root_deps = parts.items(.dependencies)[this.root_package_id.get(this.lockfile, this.workspace_name_hash)]; - var dep_id = root_deps.off; - const end = dep_id +| root_deps.len; - while (dep_id < end) : (dep_id += 1) { - const root_dep = this.lockfile.buffers.dependencies.items[dep_id]; - for (this.update_requests) |request| { - if (request.matches(root_dep, this.lockfile.buffers.string_bytes.items)) { - const package_id = this.lockfile.buffers.resolutions.items[dep_id]; - if (package_id == invalid_package_id) continue; - - const entry = set.getOrPut(this.lockfile.allocator, @truncate(root_dep.name_hash)) catch bun.outOfMemory(); - if (!entry.found_existing) { - const dependency_slice = parts.items(.dependencies)[package_id]; - addDependenciesToSet(&set, this.lockfile, dependency_slice); - } - break; - } - } - } - } - - break :trusted_deps set; - }; - break :brk PackageInstaller{ .manager = this, .options = &this.options, @@ -258,7 +208,7 @@ pub fn installHoistedPackages( } break :trees trees; }, - .trusted_dependencies_from_update_requests = trusted_dependencies_from_update_requests, + .trusted_dependencies_from_update_requests = this.findTrustedDependenciesFromUpdateRequests(), .seen_bin_links = bun.StringHashMap(void).init(this.allocator), }; }; @@ -298,7 +248,6 @@ pub fn installHoistedPackages( &installer, .{ .onExtract = PackageInstaller.installEnqueuedPackagesAfterExtraction, - .onPatch = PackageInstaller.installEnqueuedPackagesImpl, .onResolve = {}, .onPackageManifestError = {}, .onPackageDownloadError = {}, @@ -321,7 +270,6 @@ pub fn installHoistedPackages( &installer, .{ .onExtract = PackageInstaller.installEnqueuedPackagesAfterExtraction, - .onPatch = PackageInstaller.installEnqueuedPackagesImpl, .onResolve = {}, .onPackageManifestError = {}, .onPackageDownloadError = {}, @@ -348,7 +296,6 @@ pub fn installHoistedPackages( closure.installer, .{ .onExtract = PackageInstaller.installEnqueuedPackagesAfterExtraction, - .onPatch = PackageInstaller.installEnqueuedPackagesImpl, .onResolve = {}, .onPackageManifestError = {}, .onPackageDownloadError = {}, diff --git a/src/install/install.zig b/src/install/install.zig index e71aae1ad4..32464ef26f 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -16,6 +16,31 @@ pub fn buntaghashbuf_make(buf: *BuntagHashBuf, patch_hash: u64) [:0]u8 { return bunhashtag; } +pub const StorePathFormatter = struct { + str: string, + + pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { + // if (!this.opts.replace_slashes) { + // try writer.writeAll(this.str); + // return; + // } + + for (this.str) |c| { + switch (c) { + '/' => try writer.writeByte('+'), + '\\' => try writer.writeByte('+'), + else => try writer.writeByte(c), + } + } + } +}; + +pub fn fmtStorePath(str: string) StorePathFormatter { + return .{ + .str = str, + }; +} + // these bytes are skipped // so we just make it repeat bun bun bun bun bun bun bun bun bun // because why not @@ -192,6 +217,7 @@ pub const DependencyInstallContext = struct { pub const TaskCallbackContext = union(enum) { dependency: DependencyID, dependency_install_context: DependencyInstallContext, + isolated_package_install_context: Store.Entry.Id, root_dependency: DependencyID, root_request_id: PackageID, }; @@ -227,6 +253,7 @@ pub const LifecycleScriptSubprocess = @import("./lifecycle_script_runner.zig").L pub const PackageInstall = @import("./PackageInstall.zig").PackageInstall; pub const Repository = @import("./repository.zig").Repository; pub const Resolution = @import("./resolution.zig").Resolution; +pub const Store = @import("./isolated_install/Store.zig").Store; pub const ArrayIdentityContext = @import("../identity_context.zig").ArrayIdentityContext; pub const IdentityContext = @import("../identity_context.zig").IdentityContext; diff --git a/src/install/isolated_install.zig b/src/install/isolated_install.zig new file mode 100644 index 0000000000..5052fa7a50 --- /dev/null +++ b/src/install/isolated_install.zig @@ -0,0 +1,985 @@ +const log = Output.scoped(.IsolatedInstall, false); + +pub fn installIsolatedPackages( + manager: *PackageManager, + command_ctx: Command.Context, + install_root_dependencies: bool, + workspace_filters: []const WorkspaceFilter, +) OOM!PackageInstall.Summary { + bun.Analytics.Features.isolated_bun_install += 1; + + const lockfile = manager.lockfile; + + const store: Store = store: { + var timer = std.time.Timer.start() catch unreachable; + const pkgs = lockfile.packages.slice(); + const pkg_dependency_slices = pkgs.items(.dependencies); + const pkg_resolutions = pkgs.items(.resolution); + const pkg_names = pkgs.items(.name); + + const resolutions = lockfile.buffers.resolutions.items; + const dependencies = lockfile.buffers.dependencies.items; + const string_buf = lockfile.buffers.string_bytes.items; + + var nodes: Store.Node.List = .empty; + + const QueuedNode = struct { + parent_id: Store.Node.Id, + dep_id: DependencyID, + pkg_id: PackageID, + }; + + var node_queue: std.fifo.LinearFifo(QueuedNode, .Dynamic) = .init(lockfile.allocator); + defer node_queue.deinit(); + + try node_queue.writeItem(.{ + .parent_id = .invalid, + .dep_id = invalid_dependency_id, + .pkg_id = 0, + }); + + var dep_ids_sort_buf: std.ArrayListUnmanaged(DependencyID) = .empty; + defer dep_ids_sort_buf.deinit(lockfile.allocator); + + // Used by leaves and linked dependencies. They can be deduplicated early + // because peers won't change them. + // + // In the pnpm repo without this map: 772,471 nodes + // and with this map: 314,022 nodes + var early_dedupe: std.AutoHashMapUnmanaged(PackageID, Store.Node.Id) = .empty; + defer early_dedupe.deinit(lockfile.allocator); + + var peer_dep_ids: std.ArrayListUnmanaged(DependencyID) = .empty; + defer peer_dep_ids.deinit(lockfile.allocator); + + var visited_parent_node_ids: std.ArrayListUnmanaged(Store.Node.Id) = .empty; + defer visited_parent_node_ids.deinit(lockfile.allocator); + + // First pass: create full dependency tree with resolved peers + next_node: while (node_queue.readItem()) |entry| { + { + // check for cycles + const nodes_slice = nodes.slice(); + const node_pkg_ids = nodes_slice.items(.pkg_id); + const node_parent_ids = nodes_slice.items(.parent_id); + const node_nodes = nodes_slice.items(.nodes); + + var curr_id = entry.parent_id; + while (curr_id != .invalid) { + if (node_pkg_ids[curr_id.get()] == entry.pkg_id) { + // skip the new node, and add the previously added node to parent so it appears in + // 'node_modules/.bun/parent@version/node_modules' + node_nodes[entry.parent_id.get()].appendAssumeCapacity(curr_id); + continue :next_node; + } + curr_id = node_parent_ids[curr_id.get()]; + } + } + + const node_id: Store.Node.Id = .from(@intCast(nodes.len)); + const pkg_deps = pkg_dependency_slices[entry.pkg_id]; + + var skip_dependencies_of_workspace_node = false; + if (entry.dep_id != invalid_dependency_id) { + const entry_dep = dependencies[entry.dep_id]; + if (pkg_deps.len == 0 or entry_dep.isWorkspaceDep()) dont_dedupe: { + const dedupe_entry = try early_dedupe.getOrPut(lockfile.allocator, entry.pkg_id); + if (dedupe_entry.found_existing) { + const dedupe_node_id = dedupe_entry.value_ptr.*; + + const nodes_slice = nodes.slice(); + const node_nodes = nodes_slice.items(.nodes); + const node_dep_ids = nodes_slice.items(.dep_id); + + const dedupe_dep_id = node_dep_ids[dedupe_node_id.get()]; + const dedupe_dep = dependencies[dedupe_dep_id]; + + if (dedupe_dep.name_hash != entry_dep.name_hash) { + break :dont_dedupe; + } + + if (dedupe_dep.isWorkspaceDep() and entry_dep.isWorkspaceDep()) { + if (dedupe_dep.behavior.isWorkspaceOnly() != entry_dep.behavior.isWorkspaceOnly()) { + // only attach the dependencies to one of the workspaces + skip_dependencies_of_workspace_node = true; + break :dont_dedupe; + } + } + + node_nodes[entry.parent_id.get()].appendAssumeCapacity(dedupe_node_id); + continue; + } + + dedupe_entry.value_ptr.* = node_id; + } + } + + try nodes.append(lockfile.allocator, .{ + .pkg_id = entry.pkg_id, + .dep_id = entry.dep_id, + .parent_id = entry.parent_id, + .nodes = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + .dependencies = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + }); + + const nodes_slice = nodes.slice(); + const node_parent_ids = nodes_slice.items(.parent_id); + const node_dependencies = nodes_slice.items(.dependencies); + const node_peers = nodes_slice.items(.peers); + const node_nodes = nodes_slice.items(.nodes); + + if (entry.parent_id.tryGet()) |parent_id| { + node_nodes[parent_id].appendAssumeCapacity(node_id); + } + + if (skip_dependencies_of_workspace_node) { + continue; + } + + dep_ids_sort_buf.clearRetainingCapacity(); + try dep_ids_sort_buf.ensureUnusedCapacity(lockfile.allocator, pkg_deps.len); + for (pkg_deps.begin()..pkg_deps.end()) |_dep_id| { + const dep_id: DependencyID = @intCast(_dep_id); + dep_ids_sort_buf.appendAssumeCapacity(dep_id); + } + + // TODO: make this sort in an order that allows peers to be resolved last + // and devDependency handling to match `hoistDependency` + std.sort.pdq( + DependencyID, + dep_ids_sort_buf.items, + Lockfile.DepSorter{ + .lockfile = lockfile, + }, + Lockfile.DepSorter.isLessThan, + ); + + peer_dep_ids.clearRetainingCapacity(); + for (dep_ids_sort_buf.items) |dep_id| { + if (Tree.isFilteredDependencyOrWorkspace( + dep_id, + entry.pkg_id, + workspace_filters, + install_root_dependencies, + manager, + lockfile, + )) { + continue; + } + + const pkg_id = resolutions[dep_id]; + const dep = dependencies[dep_id]; + + // TODO: handle duplicate dependencies. should be similar logic + // like we have for dev dependencies in `hoistDependency` + + if (!dep.behavior.isPeer()) { + // simple case: + // - add it as a dependency + // - queue it + node_dependencies[node_id.get()].appendAssumeCapacity(.{ .dep_id = dep_id, .pkg_id = pkg_id }); + try node_queue.writeItem(.{ + .parent_id = node_id, + .dep_id = dep_id, + .pkg_id = pkg_id, + }); + continue; + } + + try peer_dep_ids.append(lockfile.allocator, dep_id); + } + + next_peer: for (peer_dep_ids.items) |peer_dep_id| { + const resolved_pkg_id, const auto_installed = resolved_pkg_id: { + + // Go through the peers parents looking for a package with the same name. + // If none is found, use current best version. Parents visited must have + // the package id for the chosen peer marked as a transitive peer. Nodes + // are deduplicated only if their package id and their transitive peer package + // ids are equal. + const peer_dep = dependencies[peer_dep_id]; + + // TODO: double check this + // Start with the current package. A package + // can satisfy it's own peers. + var curr_id = node_id; + + visited_parent_node_ids.clearRetainingCapacity(); + while (curr_id != .invalid) { + for (node_dependencies[curr_id.get()].items) |ids| { + const dep = dependencies[ids.dep_id]; + + if (dep.name_hash != peer_dep.name_hash) { + continue; + } + + const res = pkg_resolutions[ids.pkg_id]; + + if (peer_dep.version.tag != .npm or res.tag != .npm) { + // TODO: print warning for this? we don't have a version + // to compare to say if this satisfies or not. + break :resolved_pkg_id .{ ids.pkg_id, false }; + } + + const peer_dep_version = peer_dep.version.value.npm.version; + const res_version = res.value.npm.version; + + if (!peer_dep_version.satisfies(res_version, string_buf, string_buf)) { + // TODO: add warning! + } + + break :resolved_pkg_id .{ ids.pkg_id, false }; + } + + const curr_peers = node_peers[curr_id.get()]; + for (curr_peers.list.items) |ids| { + const transitive_peer_dep = dependencies[ids.dep_id]; + + if (transitive_peer_dep.name_hash != peer_dep.name_hash) { + continue; + } + + // A transitive peer with the same name has already passed + // through this node + + if (!ids.auto_installed) { + // The resolution was found here or above. Choose the same + // peer resolution. No need to mark this node or above. + + // TODO: add warning if not satisfies()! + break :resolved_pkg_id .{ ids.pkg_id, false }; + } + + // It didn't find a matching name and auto installed + // from somewhere this peer can't reach. Choose best + // version. Only mark all parents if resolution is + // different from this transitive peer. + + if (peer_dep.behavior.isOptionalPeer()) { + // exclude it + continue :next_peer; + } + + const best_version = resolutions[peer_dep_id]; + + if (best_version == ids.pkg_id) { + break :resolved_pkg_id .{ ids.pkg_id, true }; + } + + // add the remaining parent ids + while (curr_id != .invalid) { + try visited_parent_node_ids.append(lockfile.allocator, curr_id); + curr_id = node_parent_ids[curr_id.get()]; + } + + break :resolved_pkg_id .{ best_version, true }; + } + + // TODO: prevent marking workspace and symlink deps with transitive peers + + // add to visited parents after searching for a peer resolution. + // if a node resolves a transitive peer, it can still be deduplicated + try visited_parent_node_ids.append(lockfile.allocator, curr_id); + curr_id = node_parent_ids[curr_id.get()]; + } + + if (peer_dep.behavior.isOptionalPeer()) { + // exclude it + continue; + } + + // choose the current best version + break :resolved_pkg_id .{ resolutions[peer_dep_id], true }; + }; + + bun.debugAssert(resolved_pkg_id != invalid_package_id); + + for (visited_parent_node_ids.items) |visited_parent_id| { + const ctx: Store.Node.TransitivePeer.OrderedArraySetCtx = .{ + .string_buf = string_buf, + .pkg_names = pkg_names, + }; + const peer: Store.Node.TransitivePeer = .{ + .dep_id = peer_dep_id, + .pkg_id = resolved_pkg_id, + .auto_installed = auto_installed, + }; + try node_peers[visited_parent_id.get()].insert(lockfile.allocator, peer, &ctx); + } + + if (visited_parent_node_ids.items.len != 0) { + // visited parents length == 0 means the node satisfied it's own + // peer. don't queue. + node_dependencies[node_id.get()].appendAssumeCapacity(.{ .dep_id = peer_dep_id, .pkg_id = resolved_pkg_id }); + try node_queue.writeItem(.{ + .parent_id = node_id, + .dep_id = peer_dep_id, + .pkg_id = resolved_pkg_id, + }); + } + } + } + + if (manager.options.log_level.isVerbose()) { + const full_tree_end = timer.read(); + timer.reset(); + Output.prettyErrorln("Resolved peers [{}]", .{bun.fmt.fmtDurationOneDecimal(full_tree_end)}); + } + + const DedupeInfo = struct { + entry_id: Store.Entry.Id, + dep_id: DependencyID, + peers: Store.OrderedArraySet(Store.Node.TransitivePeer, Store.Node.TransitivePeer.OrderedArraySetCtx), + }; + + var dedupe: std.AutoHashMapUnmanaged(PackageID, std.ArrayListUnmanaged(DedupeInfo)) = .empty; + defer dedupe.deinit(lockfile.allocator); + + var res_fmt_buf: std.ArrayList(u8) = .init(lockfile.allocator); + defer res_fmt_buf.deinit(); + + const nodes_slice = nodes.slice(); + const node_pkg_ids = nodes_slice.items(.pkg_id); + const node_dep_ids = nodes_slice.items(.dep_id); + const node_peers: []const Store.Node.Peers = nodes_slice.items(.peers); + const node_nodes = nodes_slice.items(.nodes); + + var store: Store.Entry.List = .empty; + + const QueuedEntry = struct { + node_id: Store.Node.Id, + entry_parent_id: Store.Entry.Id, + }; + var entry_queue: std.fifo.LinearFifo(QueuedEntry, .Dynamic) = .init(lockfile.allocator); + defer entry_queue.deinit(); + + try entry_queue.writeItem(.{ + .node_id = .from(0), + .entry_parent_id = .invalid, + }); + + // Second pass: Deduplicate nodes when the pkg_id and peer set match an existing entry. + next_entry: while (entry_queue.readItem()) |entry| { + const pkg_id = node_pkg_ids[entry.node_id.get()]; + + const dedupe_entry = try dedupe.getOrPut(lockfile.allocator, pkg_id); + if (!dedupe_entry.found_existing) { + dedupe_entry.value_ptr.* = .{}; + } else { + const curr_peers = node_peers[entry.node_id.get()]; + const curr_dep_id = node_dep_ids[entry.node_id.get()]; + + for (dedupe_entry.value_ptr.items) |info| { + if (info.dep_id != invalid_dependency_id and curr_dep_id != invalid_dependency_id) { + const curr_dep = dependencies[curr_dep_id]; + const existing_dep = dependencies[info.dep_id]; + + if (existing_dep.isWorkspaceDep() and curr_dep.isWorkspaceDep()) { + if (existing_dep.behavior.isWorkspaceOnly() != curr_dep.behavior.isWorkspaceOnly()) { + continue; + } + } + } + + const eql_ctx: Store.Node.TransitivePeer.OrderedArraySetCtx = .{ + .string_buf = string_buf, + .pkg_names = pkg_names, + }; + + if (info.peers.eql(&curr_peers, &eql_ctx)) { + // dedupe! depend on the already created entry + + const entries = store.slice(); + const entry_dependencies = entries.items(.dependencies); + const entry_parents = entries.items(.parents); + + var parents = &entry_parents[info.entry_id.get()]; + + if (curr_dep_id != invalid_dependency_id and dependencies[curr_dep_id].behavior.isWorkspaceOnly()) { + try parents.append(lockfile.allocator, entry.entry_parent_id); + continue :next_entry; + } + const ctx: Store.Entry.DependenciesOrderedArraySetCtx = .{ + .string_buf = string_buf, + .dependencies = dependencies, + }; + try entry_dependencies[entry.entry_parent_id.get()].insert( + lockfile.allocator, + .{ .entry_id = info.entry_id, .dep_id = curr_dep_id }, + &ctx, + ); + try parents.append(lockfile.allocator, entry.entry_parent_id); + continue :next_entry; + } + } + + // nothing matched - create a new entry + } + + const new_entry_peer_hash: Store.Entry.PeerHash = peer_hash: { + const peers = node_peers[entry.node_id.get()]; + if (peers.len() == 0) { + break :peer_hash .none; + } + var hasher = bun.Wyhash11.init(0); + for (peers.slice()) |peer_ids| { + const pkg_name = pkg_names[peer_ids.pkg_id]; + hasher.update(pkg_name.slice(string_buf)); + const pkg_res = pkg_resolutions[peer_ids.pkg_id]; + res_fmt_buf.clearRetainingCapacity(); + try res_fmt_buf.writer().print("{}", .{pkg_res.fmt(string_buf, .posix)}); + hasher.update(res_fmt_buf.items); + } + break :peer_hash .from(hasher.final()); + }; + + const new_entry_dep_id = node_dep_ids[entry.node_id.get()]; + + const new_entry_is_root = new_entry_dep_id == invalid_dependency_id; + const new_entry_is_workspace = !new_entry_is_root and dependencies[new_entry_dep_id].isWorkspaceDep(); + + const new_entry_dependencies: Store.Entry.Dependencies = if (dedupe_entry.found_existing and new_entry_is_workspace) + .empty + else + try .initCapacity(lockfile.allocator, node_nodes[entry.node_id.get()].items.len); + + var new_entry_parents: std.ArrayListUnmanaged(Store.Entry.Id) = try .initCapacity(lockfile.allocator, 1); + new_entry_parents.appendAssumeCapacity(entry.entry_parent_id); + + const new_entry: Store.Entry = .{ + .node_id = entry.node_id, + .dependencies = new_entry_dependencies, + .parents = new_entry_parents, + .peer_hash = new_entry_peer_hash, + }; + + const new_entry_id: Store.Entry.Id = .from(@intCast(store.len)); + try store.append(lockfile.allocator, new_entry); + + if (entry.entry_parent_id.tryGet()) |entry_parent_id| skip_adding_dependency: { + if (new_entry_dep_id != invalid_dependency_id and dependencies[new_entry_dep_id].behavior.isWorkspaceOnly()) { + // skip implicit workspace dependencies on the root. + break :skip_adding_dependency; + } + + const entries = store.slice(); + const entry_dependencies = entries.items(.dependencies); + const ctx: Store.Entry.DependenciesOrderedArraySetCtx = .{ + .string_buf = string_buf, + .dependencies = dependencies, + }; + try entry_dependencies[entry_parent_id].insert( + lockfile.allocator, + .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id }, + &ctx, + ); + } + + try dedupe_entry.value_ptr.append(lockfile.allocator, .{ + .entry_id = new_entry_id, + .dep_id = new_entry_dep_id, + .peers = node_peers[entry.node_id.get()], + }); + + for (node_nodes[entry.node_id.get()].items) |node_id| { + try entry_queue.writeItem(.{ + .node_id = node_id, + .entry_parent_id = new_entry_id, + }); + } + } + + if (manager.options.log_level.isVerbose()) { + const dedupe_end = timer.read(); + Output.prettyErrorln("Created store [{}]", .{bun.fmt.fmtDurationOneDecimal(dedupe_end)}); + } + + break :store .{ + .entries = store, + .nodes = nodes, + }; + }; + + const cwd = FD.cwd(); + + const root_node_modules_dir, const is_new_root_node_modules, const bun_modules_dir, const is_new_bun_modules = root_dirs: { + const node_modules_path = bun.OSPathLiteral("node_modules"); + const bun_modules_path = bun.OSPathLiteral("node_modules/" ++ Store.modules_dir_name); + const existing_root_node_modules_dir = sys.openatOSPath(cwd, node_modules_path, bun.O.DIRECTORY | bun.O.RDONLY, 0o755).unwrap() catch { + sys.mkdirat(cwd, node_modules_path, 0o755).unwrap() catch |err| { + Output.err(err, "failed to create the './node_modules' directory", .{}); + Global.exit(1); + }; + + sys.mkdirat(cwd, bun_modules_path, 0o755).unwrap() catch |err| { + Output.err(err, "failed to create the './node_modules/.bun' directory", .{}); + Global.exit(1); + }; + + const new_root_node_modules_dir = sys.openatOSPath(cwd, node_modules_path, bun.O.DIRECTORY | bun.O.RDONLY, 0o755).unwrap() catch |err| { + Output.err(err, "failed to open the './node_modules' directory", .{}); + Global.exit(1); + }; + + const new_bun_modules_dir = sys.openatOSPath(cwd, bun_modules_path, bun.O.DIRECTORY | bun.O.RDONLY, 0o755).unwrap() catch |err| { + Output.err(err, "failed to open the './node_modules/.bun' directory", .{}); + Global.exit(1); + }; + + break :root_dirs .{ + new_root_node_modules_dir, + true, + new_bun_modules_dir, + true, + }; + }; + + const existing_bun_modules_dir = sys.openatOSPath(cwd, bun_modules_path, bun.O.DIRECTORY | bun.O.RDONLY, 0o755).unwrap() catch { + sys.mkdirat(cwd, bun_modules_path, 0o755).unwrap() catch |err| { + Output.err(err, "failed to create the './node_modules/.bun' directory", .{}); + Global.exit(1); + }; + + const new_bun_modules_dir = sys.openatOSPath(cwd, bun_modules_path, bun.O.DIRECTORY | bun.O.RDONLY, 0o755).unwrap() catch |err| { + Output.err(err, "failed to open the './node_modules/.bun' directory", .{}); + Global.exit(1); + }; + + break :root_dirs .{ + existing_root_node_modules_dir, + false, + new_bun_modules_dir, + true, + }; + }; + + break :root_dirs .{ + existing_root_node_modules_dir, + false, + existing_bun_modules_dir, + false, + }; + }; + _ = root_node_modules_dir; + _ = is_new_root_node_modules; + _ = bun_modules_dir; + // _ = is_new_bun_modules; + + { + var root_node: *Progress.Node = undefined; + // var download_node: Progress.Node = undefined; + var install_node: Progress.Node = undefined; + var scripts_node: Progress.Node = undefined; + var progress = &manager.progress; + + if (manager.options.log_level.showProgress()) { + progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; + root_node = progress.start("", 0); + // download_node = root_node.start(ProgressStrings.download(), 0); + install_node = root_node.start(ProgressStrings.install(), store.entries.len); + scripts_node = root_node.start(ProgressStrings.script(), 0); + + manager.downloads_node = null; + manager.scripts_node = &scripts_node; + } + + const nodes_slice = store.nodes.slice(); + const node_pkg_ids = nodes_slice.items(.pkg_id); + const node_dep_ids = nodes_slice.items(.dep_id); + + const entries = store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + const entry_steps = entries.items(.step); + const entry_dependencies = entries.items(.dependencies); + + const string_buf = lockfile.buffers.string_bytes.items; + + const pkgs = lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_name_hashes = pkgs.items(.name_hash); + const pkg_resolutions = pkgs.items(.resolution); + + var seen_entry_ids: std.AutoHashMapUnmanaged(Store.Entry.Id, void) = .empty; + defer seen_entry_ids.deinit(lockfile.allocator); + try seen_entry_ids.ensureTotalCapacity(lockfile.allocator, @intCast(store.entries.len)); + + // TODO: delete + var seen_workspace_ids: std.AutoHashMapUnmanaged(PackageID, void) = .empty; + defer seen_workspace_ids.deinit(lockfile.allocator); + + var installer: Store.Installer = .{ + .lockfile = lockfile, + .manager = manager, + .command_ctx = command_ctx, + .installed = try .initEmpty(manager.allocator, lockfile.packages.len), + .install_node = if (manager.options.log_level.showProgress()) &install_node else null, + .scripts_node = if (manager.options.log_level.showProgress()) &scripts_node else null, + .store = &store, + .preallocated_tasks = .init(bun.default_allocator), + .trusted_dependencies_mutex = .{}, + .trusted_dependencies_from_update_requests = manager.findTrustedDependenciesFromUpdateRequests(), + }; + + // add the pending task count upfront + _ = manager.incrementPendingTasks(@intCast(store.entries.len)); + + for (0..store.entries.len) |_entry_id| { + const entry_id: Store.Entry.Id = .from(@intCast(_entry_id)); + + const node_id = entry_node_ids[entry_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + + const pkg_name = pkg_names[pkg_id]; + const pkg_name_hash = pkg_name_hashes[pkg_id]; + const pkg_res: Resolution = pkg_resolutions[pkg_id]; + + switch (pkg_res.tag) { + else => { + // this is `uninitialized` or `single_file_module`. + bun.debugAssert(false); + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .skipped); + continue; + }, + .root => { + if (entry_id == .root) { + entry_steps[entry_id.get()].store(.symlink_dependencies, .monotonic); + installer.startTask(entry_id); + continue; + } + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .skipped); + continue; + }, + .workspace => { + // if injected=true this might be false + if (!(try seen_workspace_ids.getOrPut(lockfile.allocator, pkg_id)).found_existing) { + entry_steps[entry_id.get()].store(.symlink_dependencies, .monotonic); + installer.startTask(entry_id); + continue; + } + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .success); + continue; + }, + .symlink => { + // no installation required, will only need to be linked to packages that depend on it. + bun.debugAssert(entry_dependencies[entry_id.get()].list.items.len == 0); + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .skipped); + continue; + }, + .folder => { + // folders are always hardlinked to keep them up-to-date + installer.startTask(entry_id); + continue; + }, + + inline .npm, + .git, + .github, + .local_tarball, + .remote_tarball, + => |pkg_res_tag| { + const patch_info = try installer.packagePatchInfo(pkg_name, pkg_name_hash, &pkg_res); + + const needs_install = + manager.options.enable.force_install or + is_new_bun_modules or + patch_info == .remove or + needs_install: { + var store_path: bun.AbsPath(.{}) = .initTopLevelDir(); + defer store_path.deinit(); + installer.appendStorePath(&store_path, entry_id); + const exists = sys.existsZ(store_path.sliceZ()); + + break :needs_install switch (patch_info) { + .none => !exists, + // checked above + .remove => unreachable, + .patch => |patch| { + var hash_buf: install.BuntagHashBuf = undefined; + const hash = install.buntaghashbuf_make(&hash_buf, patch.contents_hash); + var patch_tag_path: bun.AbsPath(.{}) = .initTopLevelDir(); + defer patch_tag_path.deinit(); + installer.appendStorePath(&patch_tag_path, entry_id); + patch_tag_path.append(hash); + break :needs_install !sys.existsZ(patch_tag_path.sliceZ()); + }, + }; + }; + + if (!needs_install) { + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .skipped); + continue; + } + + var pkg_cache_dir_subpath: bun.RelPath(.{ .sep = .auto }) = .from(switch (pkg_res_tag) { + .npm => manager.cachedNPMPackageFolderName(pkg_name.slice(string_buf), pkg_res.value.npm.version, patch_info.contentsHash()), + .git => manager.cachedGitFolderName(&pkg_res.value.git, patch_info.contentsHash()), + .github => manager.cachedGitHubFolderName(&pkg_res.value.github, patch_info.contentsHash()), + .local_tarball => manager.cachedTarballFolderName(pkg_res.value.local_tarball, patch_info.contentsHash()), + .remote_tarball => manager.cachedTarballFolderName(pkg_res.value.remote_tarball, patch_info.contentsHash()), + + else => comptime unreachable, + }); + defer pkg_cache_dir_subpath.deinit(); + + const cache_dir, const cache_dir_path = manager.getCacheDirectoryAndAbsPath(); + defer cache_dir_path.deinit(); + + const missing_from_cache = switch (manager.getPreinstallState(pkg_id)) { + .done => false, + else => missing_from_cache: { + if (patch_info == .none) { + const exists = switch (pkg_res_tag) { + .npm => exists: { + var cache_dir_path_save = pkg_cache_dir_subpath.save(); + defer cache_dir_path_save.restore(); + pkg_cache_dir_subpath.append("package.json"); + break :exists sys.existsAt(cache_dir, pkg_cache_dir_subpath.sliceZ()); + }, + else => sys.directoryExistsAt(cache_dir, pkg_cache_dir_subpath.sliceZ()).unwrapOr(false), + }; + if (exists) { + manager.setPreinstallState(pkg_id, installer.lockfile, .done); + } + break :missing_from_cache !exists; + } + + // TODO: why does this look like it will never work? + break :missing_from_cache true; + }, + }; + + if (!missing_from_cache) { + installer.startTask(entry_id); + continue; + } + + const ctx: install.TaskCallbackContext = .{ + .isolated_package_install_context = entry_id, + }; + + const dep_id = node_dep_ids[node_id.get()]; + const dep = lockfile.buffers.dependencies.items[dep_id]; + switch (pkg_res_tag) { + .npm => { + manager.enqueuePackageForDownload( + pkg_name.slice(string_buf), + dep_id, + pkg_id, + pkg_res.value.npm.version, + pkg_res.value.npm.url.slice(string_buf), + ctx, + patch_info.nameAndVersionHash(), + ) catch |err| switch (err) { + error.OutOfMemory => |oom| return oom, + error.InvalidURL => { + Output.err(err, "failed to enqueue package for download: {s}@{}", .{ + pkg_name.slice(string_buf), + pkg_res.fmt(string_buf, .auto), + }); + Output.flush(); + if (manager.options.enable.fail_early) { + Global.exit(1); + } + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .fail); + continue; + }, + }; + }, + .git => { + manager.enqueueGitForCheckout( + dep_id, + dep.name.slice(string_buf), + &pkg_res, + ctx, + patch_info.nameAndVersionHash(), + ); + }, + .github => { + const url = manager.allocGitHubURL(&pkg_res.value.git); + defer manager.allocator.free(url); + manager.enqueueTarballForDownload( + dep_id, + pkg_id, + url, + ctx, + patch_info.nameAndVersionHash(), + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + error.InvalidURL => { + Output.err(err, "failed to enqueue github package for download: {s}@{}", .{ + pkg_name.slice(string_buf), + pkg_res.fmt(string_buf, .auto), + }); + Output.flush(); + if (manager.options.enable.fail_early) { + Global.exit(1); + } + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .fail); + continue; + }, + }; + }, + .local_tarball => { + manager.enqueueTarballForReading( + dep_id, + dep.name.slice(string_buf), + &pkg_res, + ctx, + ); + }, + .remote_tarball => { + manager.enqueueTarballForDownload( + dep_id, + pkg_id, + pkg_res.value.remote_tarball.slice(string_buf), + ctx, + patch_info.nameAndVersionHash(), + ) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + error.InvalidURL => { + Output.err(err, "failed to enqueue tarball for download: {s}@{}", .{ + pkg_name.slice(string_buf), + pkg_res.fmt(string_buf, .auto), + }); + Output.flush(); + if (manager.options.enable.fail_early) { + Global.exit(1); + } + entry_steps[entry_id.get()].store(.done, .monotonic); + installer.onTaskComplete(entry_id, .fail); + continue; + }, + }; + }, + else => comptime unreachable, + } + }, + } + } + + if (manager.pendingTaskCount() > 0) { + const Wait = struct { + installer: *Store.Installer, + manager: *PackageManager, + err: ?anyerror = null, + + pub fn isDone(wait: *@This()) bool { + wait.manager.runTasks( + *Store.Installer, + wait.installer, + .{ + .onExtract = Store.Installer.onPackageExtracted, + .onResolve = {}, + .onPackageManifestError = {}, + .onPackageDownloadError = {}, + }, + true, + wait.manager.options.log_level, + ) catch |err| { + wait.err = err; + return true; + }; + + return wait.manager.pendingTaskCount() == 0; + } + }; + + var wait: Wait = .{ + .manager = manager, + .installer = &installer, + }; + + manager.sleepUntil(&wait, &Wait.isDone); + + if (wait.err) |err| { + Output.err(err, "failed to install packages", .{}); + Global.exit(1); + } + } + + if (manager.options.log_level.showProgress()) { + progress.root.end(); + progress.* = .{}; + } + + if (comptime Environment.ci_assert) { + var done = true; + next_entry: for (store.entries.items(.step), 0..) |entry_step, _entry_id| { + const entry_id: Store.Entry.Id = .from(@intCast(_entry_id)); + const step = entry_step.load(.monotonic); + + if (step == .done) { + continue; + } + + done = false; + + log("entry not done: {d}, {s}\n", .{ entry_id, @tagName(step) }); + + const deps = store.entries.items(.dependencies)[entry_id.get()]; + for (deps.slice()) |dep| { + const dep_step = entry_steps[dep.entry_id.get()].load(.monotonic); + if (dep_step != .done) { + log(", parents:\n - ", .{}); + const parent_ids = Store.Entry.debugGatherAllParents(entry_id, installer.store); + for (parent_ids) |parent_id| { + if (parent_id == .root) { + log("root ", .{}); + } else { + log("{d} ", .{parent_id.get()}); + } + } + + log("\n", .{}); + continue :next_entry; + } + } + + log(" and is able to run\n", .{}); + } + + bun.debugAssert(done); + } + + installer.summary.successfully_installed = installer.installed; + + return installer.summary; + } +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const Environment = bun.Environment; +const FD = bun.FD; +const Global = bun.Global; +const OOM = bun.OOM; +const Output = bun.Output; +const Progress = bun.Progress; +const sys = bun.sys; +const Command = bun.CLI.Command; + +const install = bun.install; +const DependencyID = install.DependencyID; +const PackageID = install.PackageID; +const PackageInstall = install.PackageInstall; +const Resolution = install.Resolution; +const Store = install.Store; +const invalid_dependency_id = install.invalid_dependency_id; +const invalid_package_id = install.invalid_package_id; + +const Lockfile = install.Lockfile; +const Tree = Lockfile.Tree; + +const PackageManager = install.PackageManager; +const ProgressStrings = PackageManager.ProgressStrings; +const WorkspaceFilter = PackageManager.WorkspaceFilter; diff --git a/src/install/isolated_install/Hardlinker.zig b/src/install/isolated_install/Hardlinker.zig new file mode 100644 index 0000000000..f3b2a968f9 --- /dev/null +++ b/src/install/isolated_install/Hardlinker.zig @@ -0,0 +1,128 @@ +pub const Hardlinker = struct { + src_dir: FD, + src: bun.AbsPath(.{ .sep = .auto, .unit = .os }), + dest: bun.RelPath(.{ .sep = .auto, .unit = .os }), + + pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.Maybe(void) { + var walker: Walker = try .walk( + this.src_dir, + bun.default_allocator, + &.{}, + skip_dirnames, + ); + defer walker.deinit(); + + if (comptime Environment.isWindows) { + while (switch (walker.next()) { + .result => |res| res, + .err => |err| return .initErr(err), + }) |entry| { + var src_save = this.src.save(); + defer src_save.restore(); + + this.src.append(entry.path); + + var dest_save = this.dest.save(); + defer dest_save.restore(); + + this.dest.append(entry.path); + + switch (entry.kind) { + .directory => { + FD.cwd().makePath(u16, this.dest.sliceZ()) catch {}; + }, + .file => { + switch (sys.link(u16, this.src.sliceZ(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err1| switch (link_err1.getErrno()) { + .UV_EEXIST, + .EXIST, + => { + _ = sys.unlinkW(this.dest.sliceZ()); + switch (sys.link(u16, this.src.sliceZ(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err2| return .initErr(link_err2), + } + }, + .UV_ENOENT, + .NOENT, + => { + const dest_parent = this.dest.dirname() orelse { + return .initErr(link_err1); + }; + + FD.cwd().makePath(u16, dest_parent) catch {}; + switch (sys.link(u16, this.src.sliceZ(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err2| return .initErr(link_err2), + } + }, + else => return .initErr(link_err1), + }, + } + }, + else => {}, + } + } + + return .success; + } + + while (switch (walker.next()) { + .result => |res| res, + .err => |err| return .initErr(err), + }) |entry| { + var dest_save = this.dest.save(); + defer dest_save.restore(); + + this.dest.append(entry.path); + + switch (entry.kind) { + .directory => { + FD.cwd().makePath(u8, this.dest.sliceZ()) catch {}; + }, + .file => { + switch (sys.linkatZ(entry.dir, entry.basename, FD.cwd(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err1| { + switch (link_err1.getErrno()) { + .EXIST => { + FD.cwd().deleteTree(this.dest.slice()) catch {}; + switch (sys.linkatZ(entry.dir, entry.basename, FD.cwd(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err2| return .initErr(link_err2), + } + }, + .NOENT => { + const dest_parent = this.dest.dirname() orelse { + return .initErr(link_err1); + }; + + FD.cwd().makePath(u8, dest_parent) catch {}; + switch (sys.linkatZ(entry.dir, entry.basename, FD.cwd(), this.dest.sliceZ())) { + .result => {}, + .err => |link_err2| return .initErr(link_err2), + } + }, + else => return .initErr(link_err1), + } + }, + } + }, + else => {}, + } + } + + return .success; + } +}; + +// @sortImports + +const Walker = @import("../../walker_skippable.zig"); + +const bun = @import("bun"); +const Environment = bun.Environment; +const FD = bun.FD; +const OOM = bun.OOM; +const sys = bun.sys; diff --git a/src/install/isolated_install/Installer.zig b/src/install/isolated_install/Installer.zig new file mode 100644 index 0000000000..2209558e77 --- /dev/null +++ b/src/install/isolated_install/Installer.zig @@ -0,0 +1,1139 @@ +pub const Installer = struct { + trusted_dependencies_mutex: bun.Mutex, + // this is not const for `lockfile.trusted_dependencies` + lockfile: *Lockfile, + + summary: PackageInstall.Summary = .{ .successfully_installed = .empty }, + installed: Bitset, + install_node: ?*Progress.Node, + scripts_node: ?*Progress.Node, + + manager: *PackageManager, + command_ctx: Command.Context, + + store: *const Store, + + tasks: bun.UnboundedQueue(Task, .next) = .{}, + preallocated_tasks: Task.Preallocated, + + trusted_dependencies_from_update_requests: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void), + + pub fn deinit(this: *const Installer) void { + this.trusted_dependencies_from_update_requests.deinit(this.lockfile.allocator); + } + + pub fn startTask(this: *Installer, entry_id: Store.Entry.Id) void { + const task = this.preallocated_tasks.get(); + + task.* = .{ + .entry_id = entry_id, + .installer = this, + }; + + this.manager.thread_pool.schedule(.from(&task.task)); + } + + pub fn onPackageExtracted(this: *Installer, task_id: install.Task.Id) void { + if (this.manager.task_queue.fetchRemove(task_id)) |removed| { + for (removed.value.items) |install_ctx| { + const entry_id = install_ctx.isolated_package_install_context; + this.startTask(entry_id); + } + } + } + + pub fn onTaskFail(this: *Installer, entry_id: Store.Entry.Id, err: Task.Error) void { + const string_buf = this.lockfile.buffers.string_bytes.items; + + const entries = this.store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + + const nodes = this.store.nodes.slice(); + const node_pkg_ids = nodes.items(.pkg_id); + + const pkgs = this.lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + const node_id = entry_node_ids[entry_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + + const pkg_name = pkg_names[pkg_id]; + const pkg_res = pkg_resolutions[pkg_id]; + + switch (err) { + .link_package => |link_err| { + Output.err(link_err, "failed to link package: {s}@{}", .{ + pkg_name.slice(string_buf), + pkg_res.fmt(string_buf, .auto), + }); + }, + .symlink_dependencies => |symlink_err| { + Output.err(symlink_err, "failed to symlink dependencies for package: {s}@{}", .{ + pkg_name.slice(string_buf), + pkg_res.fmt(string_buf, .auto), + }); + }, + else => {}, + } + Output.flush(); + + // attempt deleting the package so the next install will install it again + switch (pkg_res.tag) { + .uninitialized, + .single_file_module, + .root, + .workspace, + .symlink, + => {}, + + _ => {}, + + // to be safe make sure we only delete packages in the store + .npm, + .git, + .github, + .local_tarball, + .remote_tarball, + .folder, + => { + var store_path: bun.RelPath(.{ .sep = .auto }) = .init(); + defer store_path.deinit(); + + store_path.appendFmt("node_modules/{}", .{ + Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile), + }); + + _ = sys.unlink(store_path.sliceZ()); + }, + } + + if (this.manager.options.enable.fail_early) { + Global.exit(1); + } + + this.summary.fail += 1; + + this.decrementPendingTasks(entry_id); + this.resumeUnblockedTasks(); + } + + pub fn decrementPendingTasks(this: *Installer, entry_id: Store.Entry.Id) void { + _ = entry_id; + this.manager.decrementPendingTasks(); + } + + pub fn onTaskBlocked(this: *Installer, entry_id: Store.Entry.Id) void { + + // race: task decides it is blocked because one of it's dependencies has not finished. + // before the task can mark itself as blocked, the dependency finishes it's install, + // causing the task to never finish because resumeUnblockedTasks is called before + // it's state is set to blocked. + // + // fix: check if the task is unblocked after the task returns blocked, and only set/unset + // blocked from the main thread. + + var parent_dedupe: std.AutoArrayHashMap(Store.Entry.Id, void) = .init(bun.default_allocator); + defer parent_dedupe.deinit(); + + if (this.isTaskUnblocked(entry_id, &parent_dedupe)) { + this.store.entries.items(.step)[entry_id.get()].store(.symlink_dependency_binaries, .monotonic); + this.startTask(entry_id); + return; + } + + this.store.entries.items(.step)[entry_id.get()].store(.blocked, .monotonic); + } + + fn isTaskUnblocked(this: *Installer, entry_id: Store.Entry.Id, parent_dedupe: *std.AutoArrayHashMap(Store.Entry.Id, void)) bool { + const entries = this.store.entries.slice(); + const entry_deps = entries.items(.dependencies); + const entry_steps = entries.items(.step); + + const deps = entry_deps[entry_id.get()]; + for (deps.slice()) |dep| { + if (entry_steps[dep.entry_id.get()].load(.monotonic) != .done) { + parent_dedupe.clearRetainingCapacity(); + if (this.store.isCycle(entry_id, dep.entry_id, parent_dedupe)) { + continue; + } + + return false; + } + } + + return true; + } + + pub fn onTaskComplete(this: *Installer, entry_id: Store.Entry.Id, state: enum { success, skipped, fail }) void { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(this.store.entries.items(.step)[entry_id.get()].load(.monotonic) == .done, @src()); + } + + this.decrementPendingTasks(entry_id); + this.resumeUnblockedTasks(); + + if (this.install_node) |node| { + node.completeOne(); + } + + switch (state) { + .success => { + this.summary.success += 1; + }, + .skipped => { + this.summary.skipped += 1; + }, + .fail => { + this.summary.fail += 1; + return; + }, + } + + const pkg_id = pkg_id: { + if (entry_id == .root) { + return; + } + + const node_id = this.store.entries.items(.node_id)[entry_id.get()]; + const nodes = this.store.nodes.slice(); + + const dep_id = nodes.items(.dep_id)[node_id.get()]; + + if (dep_id == invalid_dependency_id) { + // should be coverd by `entry_id == .root` above, but + // just in case + return; + } + + const dep = this.lockfile.buffers.dependencies.items[dep_id]; + + if (dep.behavior.isWorkspaceOnly()) { + return; + } + + break :pkg_id nodes.items(.pkg_id)[node_id.get()]; + }; + + const is_duplicate = this.installed.isSet(pkg_id); + this.summary.success += @intFromBool(!is_duplicate); + this.installed.set(pkg_id); + } + + // This function runs only on the main thread. The installer tasks threads + // will be changing values in `entry_step`, but the blocked state is only + // set on the main thread, allowing the code between + // `entry_steps[entry_id.get()].load(.monotonic)` + // and + // `entry_steps[entry_id.get()].store(.symlink_dependency_binaries, .monotonic)` + pub fn resumeUnblockedTasks(this: *Installer) void { + const entries = this.store.entries.slice(); + const entry_steps = entries.items(.step); + + var parent_dedupe: std.AutoArrayHashMap(Store.Entry.Id, void) = .init(bun.default_allocator); + defer parent_dedupe.deinit(); + + for (0..this.store.entries.len) |_entry_id| { + const entry_id: Store.Entry.Id = .from(@intCast(_entry_id)); + + const entry_step = entry_steps[entry_id.get()].load(.monotonic); + if (entry_step != .blocked) { + continue; + } + + if (!this.isTaskUnblocked(entry_id, &parent_dedupe)) { + continue; + } + + entry_steps[entry_id.get()].store(.symlink_dependency_binaries, .monotonic); + this.startTask(entry_id); + } + } + + pub const Task = struct { + const Preallocated = bun.HiveArray(Task, 128).Fallback; + + entry_id: Store.Entry.Id, + installer: *Installer, + + task: ThreadPool.Task = .{ .callback = &callback }, + next: ?*Task = null, + + result: Result = .none, + + const Result = union(enum) { + none, + err: Error, + blocked, + done, + }; + + const Error = union(Step) { + link_package: sys.Error, + symlink_dependencies: sys.Error, + check_if_blocked, + symlink_dependency_binaries, + run_preinstall: anyerror, + binaries: anyerror, + @"run (post)install and (pre/post)prepare": anyerror, + done, + blocked, + + pub fn clone(this: *const Error, allocator: std.mem.Allocator) Error { + return switch (this.*) { + .link_package => |err| .{ .link_package = err.clone(allocator) }, + .symlink_dependencies => |err| .{ .symlink_dependencies = err.clone(allocator) }, + .check_if_blocked => .check_if_blocked, + .symlink_dependency_binaries => .symlink_dependency_binaries, + .run_preinstall => |err| .{ .run_preinstall = err }, + .binaries => |err| .{ .binaries = err }, + .@"run (post)install and (pre/post)prepare" => |err| .{ .@"run (post)install and (pre/post)prepare" = err }, + .done => .done, + .blocked => .blocked, + }; + } + }; + + pub const Step = enum(u8) { + link_package, + symlink_dependencies, + + check_if_blocked, + + // blocked can only happen here + + symlink_dependency_binaries, + run_preinstall, + + // pause here while preinstall runs + + binaries, + @"run (post)install and (pre/post)prepare", + + // pause again while remaining scripts run. + + done, + + // only the main thread sets blocked, and only the main thread + // sets a blocked task to symlink_dependency_binaries + blocked, + }; + + fn nextStep(this: *Task, comptime current_step: Step) Step { + const next_step: Step = switch (comptime current_step) { + .link_package => .symlink_dependencies, + .symlink_dependencies => .check_if_blocked, + .check_if_blocked => .symlink_dependency_binaries, + .symlink_dependency_binaries => .run_preinstall, + .run_preinstall => .binaries, + .binaries => .@"run (post)install and (pre/post)prepare", + .@"run (post)install and (pre/post)prepare" => .done, + + .done, + .blocked, + => @compileError("unexpected step"), + }; + + this.installer.store.entries.items(.step)[this.entry_id.get()].store(next_step, .monotonic); + + return next_step; + } + + const Yield = union(enum) { + yield, + done, + blocked, + fail: Error, + + pub fn failure(e: Error) Yield { + return .{ .fail = e }; + } + }; + + fn run(this: *Task) OOM!Yield { + const installer = this.installer; + const manager = installer.manager; + const lockfile = installer.lockfile; + + const pkgs = installer.lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_name_hashes = pkgs.items(.name_hash); + const pkg_resolutions = pkgs.items(.resolution); + const pkg_bins = pkgs.items(.bin); + const pkg_script_lists = pkgs.items(.scripts); + + const entries = installer.store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + const entry_dependencies = entries.items(.dependencies); + const entry_steps = entries.items(.step); + const entry_scripts = entries.items(.scripts); + + const nodes = installer.store.nodes.slice(); + const node_pkg_ids = nodes.items(.pkg_id); + const node_dep_ids = nodes.items(.dep_id); + + const node_id = entry_node_ids[this.entry_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; + + const pkg_name = pkg_names[pkg_id]; + const pkg_name_hash = pkg_name_hashes[pkg_id]; + const pkg_res = pkg_resolutions[pkg_id]; + + return next_step: switch (entry_steps[this.entry_id.get()].load(.monotonic)) { + inline .link_package => |current_step| { + const string_buf = lockfile.buffers.string_bytes.items; + + var pkg_cache_dir_subpath: bun.RelPath(.{ .sep = .auto }) = .from(switch (pkg_res.tag) { + else => |tag| pkg_cache_dir_subpath: { + const patch_info = try installer.packagePatchInfo( + pkg_name, + pkg_name_hash, + &pkg_res, + ); + + break :pkg_cache_dir_subpath switch (tag) { + .npm => manager.cachedNPMPackageFolderName(pkg_name.slice(string_buf), pkg_res.value.npm.version, patch_info.contentsHash()), + .git => manager.cachedGitFolderName(&pkg_res.value.git, patch_info.contentsHash()), + .github => manager.cachedGitHubFolderName(&pkg_res.value.github, patch_info.contentsHash()), + .local_tarball => manager.cachedTarballFolderName(pkg_res.value.local_tarball, patch_info.contentsHash()), + .remote_tarball => manager.cachedTarballFolderName(pkg_res.value.remote_tarball, patch_info.contentsHash()), + + else => { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(false, @src()); + } + + continue :next_step this.nextStep(current_step); + }, + }; + }, + + .folder => { + // the folder does not exist in the cache + const folder_dir = switch (bun.openDirForIteration(FD.cwd(), pkg_res.value.folder.slice(string_buf))) { + .result => |fd| fd, + .err => |err| return .failure(.{ .link_package = err }), + }; + defer folder_dir.close(); + + var src: bun.AbsPath(.{ .unit = .os, .sep = .auto }) = .initTopLevelDir(); + defer src.deinit(); + src.append(pkg_res.value.folder.slice(string_buf)); + + var dest: bun.RelPath(.{ .unit = .os, .sep = .auto }) = .init(); + defer dest.deinit(); + + installer.appendStorePath(&dest, this.entry_id); + + var hardlinker: Hardlinker = .{ + .src_dir = folder_dir, + .src = src, + .dest = dest, + }; + + switch (try hardlinker.link(&.{comptime bun.OSPathLiteral("node_modules")})) { + .result => {}, + .err => |err| return .failure(.{ .link_package = err }), + } + + continue :next_step this.nextStep(current_step); + }, + }); + defer pkg_cache_dir_subpath.deinit(); + + const cache_dir, const cache_dir_path = manager.getCacheDirectoryAndAbsPath(); + defer cache_dir_path.deinit(); + + var dest_subpath: bun.RelPath(.{ .sep = .auto, .unit = .os }) = .init(); + defer dest_subpath.deinit(); + + installer.appendStorePath(&dest_subpath, this.entry_id); + + // link the package + if (comptime Environment.isMac) { + if (install.PackageInstall.supported_method == .clonefile) hardlink_fallback: { + switch (sys.clonefileat(cache_dir, pkg_cache_dir_subpath.sliceZ(), FD.cwd(), dest_subpath.sliceZ())) { + .result => { + // success! move to next step + continue :next_step this.nextStep(current_step); + }, + .err => |clonefile_err1| { + switch (clonefile_err1.getErrno()) { + .XDEV => break :hardlink_fallback, + .OPNOTSUPP => break :hardlink_fallback, + .NOENT => { + const parent_dest_dir = std.fs.path.dirname(dest_subpath.slice()) orelse { + return .failure(.{ .link_package = clonefile_err1 }); + }; + + FD.cwd().makePath(u8, parent_dest_dir) catch {}; + + switch (sys.clonefileat(cache_dir, pkg_cache_dir_subpath.sliceZ(), FD.cwd(), dest_subpath.sliceZ())) { + .result => { + continue :next_step this.nextStep(current_step); + }, + .err => |clonefile_err2| { + return .failure(.{ .link_package = clonefile_err2 }); + }, + } + }, + else => { + break :hardlink_fallback; + }, + } + }, + } + } + } + + const cached_package_dir = cached_package_dir: { + if (comptime Environment.isWindows) { + break :cached_package_dir switch (sys.openDirAtWindowsA( + cache_dir, + pkg_cache_dir_subpath.slice(), + .{ .iterable = true, .can_rename_or_delete = false, .read_only = true }, + )) { + .result => |dir_fd| dir_fd, + .err => |err| { + return .failure(.{ .link_package = err }); + }, + }; + } + break :cached_package_dir switch (sys.openat( + cache_dir, + pkg_cache_dir_subpath.sliceZ(), + bun.O.DIRECTORY | bun.O.CLOEXEC | bun.O.RDONLY, + 0, + )) { + .result => |fd| fd, + .err => |err| { + return .failure(.{ .link_package = err }); + }, + }; + }; + defer cached_package_dir.close(); + + var src: bun.AbsPath(.{ .sep = .auto, .unit = .os }) = .from(cache_dir_path.slice()); + defer src.deinit(); + src.append(pkg_cache_dir_subpath.slice()); + + var hardlinker: Hardlinker = .{ + .src_dir = cached_package_dir, + .src = src, + .dest = dest_subpath, + }; + + switch (try hardlinker.link(&.{})) { + .result => {}, + .err => |err| return .failure(.{ .link_package = err }), + } + + continue :next_step this.nextStep(current_step); + }, + inline .symlink_dependencies => |current_step| { + const string_buf = lockfile.buffers.string_bytes.items; + const dependencies = lockfile.buffers.dependencies.items; + + for (entry_dependencies[this.entry_id.get()].slice()) |dep| { + const dep_node_id = entry_node_ids[dep.entry_id.get()]; + const dep_dep_id = node_dep_ids[dep_node_id.get()]; + const dep_name = dependencies[dep_dep_id].name; + + var dest: bun.Path(.{ .sep = .auto }) = .initTopLevelDir(); + defer dest.deinit(); + + installer.appendStoreNodeModulesPath(&dest, this.entry_id); + dest.append(dep_name.slice(string_buf)); + + var dep_store_path: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer dep_store_path.deinit(); + + installer.appendStorePath(&dep_store_path, dep.entry_id); + + const target = target: { + var dest_save = dest.save(); + defer dest_save.restore(); + + dest.undo(1); + break :target dest.relative(&dep_store_path); + }; + defer target.deinit(); + + const symlinker: Symlinker = .{ + .dest = dest, + .target = target, + .fallback_junction_target = dep_store_path, + }; + + const link_strategy: Symlinker.Strategy = if (pkg_res.tag == .root or pkg_res.tag == .workspace) + // root and workspace packages ensure their dependency symlinks + // exist unconditionally. To make sure it's fast, first readlink + // then create the symlink if necessary + .expect_existing + else + .expect_missing; + + switch (symlinker.ensureSymlink(link_strategy)) { + .result => {}, + .err => |err| { + return .failure(.{ .symlink_dependencies = err }); + }, + } + } + continue :next_step this.nextStep(current_step); + }, + inline .check_if_blocked => |current_step| { + // preinstall scripts need to run before binaries can be linked. Block here if any dependencies + // of this entry are not finished. Do not count cycles towards blocking. + + var parent_dedupe: std.AutoArrayHashMap(Store.Entry.Id, void) = .init(bun.default_allocator); + defer parent_dedupe.deinit(); + + if (!installer.isTaskUnblocked(this.entry_id, &parent_dedupe)) { + return .blocked; + } + + continue :next_step this.nextStep(current_step); + }, + inline .symlink_dependency_binaries => |current_step| { + installer.linkDependencyBins(this.entry_id) catch |err| { + return .failure(.{ .binaries = err }); + }; + + switch (pkg_res.tag) { + .uninitialized, + .root, + .workspace, + .folder, + .symlink, + .single_file_module, + => {}, + + _ => {}, + + .npm, + .git, + .github, + .local_tarball, + .remote_tarball, + => { + const string_buf = lockfile.buffers.string_bytes.items; + + var hidden_hoisted_node_modules: bun.Path(.{ .sep = .auto }) = .init(); + defer hidden_hoisted_node_modules.deinit(); + + hidden_hoisted_node_modules.append( + "node_modules" ++ std.fs.path.sep_str ++ ".bun" ++ std.fs.path.sep_str ++ "node_modules", + ); + hidden_hoisted_node_modules.append(pkg_name.slice(installer.lockfile.buffers.string_bytes.items)); + + var target: bun.RelPath(.{ .sep = .auto }) = .init(); + defer target.deinit(); + + target.append(".."); + if (strings.containsChar(pkg_name.slice(installer.lockfile.buffers.string_bytes.items), '/')) { + target.append(".."); + } + + target.appendFmt("{}/node_modules/{s}", .{ + Store.Entry.fmtStorePath(this.entry_id, installer.store, installer.lockfile), + pkg_name.slice(string_buf), + }); + + var full_target: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer full_target.deinit(); + + installer.appendStorePath(&full_target, this.entry_id); + + const symlinker: Symlinker = .{ + .dest = hidden_hoisted_node_modules, + .target = target, + .fallback_junction_target = full_target, + }; + _ = symlinker.ensureSymlink(.ignore_failure); + }, + } + + continue :next_step this.nextStep(current_step); + }, + inline .run_preinstall => |current_step| { + if (!installer.manager.options.do.run_scripts or this.entry_id == .root) { + continue :next_step this.nextStep(current_step); + } + + const string_buf = installer.lockfile.buffers.string_bytes.items; + + const dep = installer.lockfile.buffers.dependencies.items[dep_id]; + const truncated_dep_name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash); + + const is_trusted, const is_trusted_through_update_request = brk: { + if (installer.trusted_dependencies_from_update_requests.contains(truncated_dep_name_hash)) { + break :brk .{ true, true }; + } + if (installer.lockfile.hasTrustedDependency(dep.name.slice(string_buf))) { + break :brk .{ true, false }; + } + break :brk .{ false, false }; + }; + + var pkg_cwd: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); + defer pkg_cwd.deinit(); + + installer.appendStorePath(&pkg_cwd, this.entry_id); + + if (pkg_res.tag != .root and (pkg_res.tag == .workspace or is_trusted)) { + const pkg_scripts: *Package.Scripts = &pkg_script_lists[pkg_id]; + + var log = bun.logger.Log.init(bun.default_allocator); + defer log.deinit(); + + const scripts_list = pkg_scripts.getList( + &log, + installer.lockfile, + &pkg_cwd, + dep.name.slice(string_buf), + &pkg_res, + ) catch |err| { + return .failure(.{ .run_preinstall = err }); + }; + + if (scripts_list) |list| { + entry_scripts[this.entry_id.get()] = bun.create(bun.default_allocator, Package.Scripts.List, list); + + if (is_trusted_through_update_request) { + const trusted_dep_to_add = try installer.manager.allocator.dupe(u8, dep.name.slice(string_buf)); + + installer.trusted_dependencies_mutex.lock(); + defer installer.trusted_dependencies_mutex.unlock(); + + try installer.manager.trusted_deps_to_add_to_package_json.append( + installer.manager.allocator, + trusted_dep_to_add, + ); + if (installer.lockfile.trusted_dependencies == null) { + installer.lockfile.trusted_dependencies = .{}; + } + try installer.lockfile.trusted_dependencies.?.put(installer.manager.allocator, truncated_dep_name_hash, {}); + } + + if (list.first_index != 0) { + // has scripts but not a preinstall + continue :next_step this.nextStep(current_step); + } + + installer.manager.spawnPackageLifecycleScripts( + installer.command_ctx, + list, + dep.behavior.optional, + false, + .{ + .entry_id = this.entry_id, + .installer = installer, + }, + ) catch |err| { + return .failure(.{ .run_preinstall = err }); + }; + + return .yield; + } + } + + continue :next_step this.nextStep(current_step); + }, + inline .binaries => |current_step| { + if (this.entry_id == .root) { + continue :next_step this.nextStep(current_step); + } + + const bin = pkg_bins[pkg_id]; + if (bin.tag == .none) { + continue :next_step this.nextStep(current_step); + } + + const string_buf = installer.lockfile.buffers.string_bytes.items; + const dependencies = installer.lockfile.buffers.dependencies.items; + + const dep_name = dependencies[dep_id].name.slice(string_buf); + + const abs_target_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(abs_target_buf); + const abs_dest_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(abs_dest_buf); + const rel_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(rel_buf); + + var seen: bun.StringHashMap(void) = .init(bun.default_allocator); + defer seen.deinit(); + + var node_modules_path: bun.AbsPath(.{}) = .initTopLevelDir(); + defer node_modules_path.deinit(); + + installer.appendStoreNodeModulesPath(&node_modules_path, this.entry_id); + + var bin_linker: Bin.Linker = .{ + .bin = bin, + .global_bin_path = installer.manager.options.bin_path, + .package_name = strings.StringOrTinyString.init(dep_name), + .string_buf = string_buf, + .extern_string_buf = installer.lockfile.buffers.extern_strings.items, + .seen = &seen, + .node_modules_path = &node_modules_path, + .abs_target_buf = abs_target_buf, + .abs_dest_buf = abs_dest_buf, + .rel_buf = rel_buf, + }; + + bin_linker.link(false); + + if (bin_linker.err) |err| { + return .failure(.{ .binaries = err }); + } + + continue :next_step this.nextStep(current_step); + }, + inline .@"run (post)install and (pre/post)prepare" => |current_step| { + if (!installer.manager.options.do.run_scripts or this.entry_id == .root) { + continue :next_step this.nextStep(current_step); + } + + var list = entry_scripts[this.entry_id.get()] orelse { + continue :next_step this.nextStep(current_step); + }; + + if (list.first_index == 0) { + for (list.items[1..], 1..) |item, i| { + if (item != null) { + list.first_index = @intCast(i); + break; + } + } + } + + if (list.first_index == 0) { + continue :next_step this.nextStep(current_step); + } + + const dep = installer.lockfile.buffers.dependencies.items[dep_id]; + + installer.manager.spawnPackageLifecycleScripts( + installer.command_ctx, + list.*, + dep.behavior.optional, + false, + .{ + .entry_id = this.entry_id, + .installer = installer, + }, + ) catch |err| { + return .failure(.{ .@"run (post)install and (pre/post)prepare" = err }); + }; + + // when these scripts finish the package install will be + // complete. the task does not have anymore work to complete + // so it does not return to the thread pool. + + return .yield; + }, + + .done => { + return .done; + }, + + .blocked => { + bun.debugAssert(false); + return .yield; + }, + }; + } + + pub fn callback(task: *ThreadPool.Task) void { + const this: *Task = @fieldParentPtr("task", task); + + const res = this.run() catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; + + switch (res) { + .yield => {}, + .done => { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(this.installer.store.entries.items(.step)[this.entry_id.get()].load(.monotonic) == .done, @src()); + } + this.result = .done; + this.installer.tasks.push(this); + this.installer.manager.wake(); + }, + .blocked => { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(this.installer.store.entries.items(.step)[this.entry_id.get()].load(.monotonic) == .check_if_blocked, @src()); + } + this.result = .blocked; + this.installer.tasks.push(this); + this.installer.manager.wake(); + }, + .fail => |err| { + if (comptime Environment.ci_assert) { + bun.assertWithLocation(this.installer.store.entries.items(.step)[this.entry_id.get()].load(.monotonic) != .done, @src()); + } + this.installer.store.entries.items(.step)[this.entry_id.get()].store(.done, .monotonic); + this.result = .{ .err = err.clone(bun.default_allocator) }; + this.installer.tasks.push(this); + this.installer.manager.wake(); + }, + } + } + }; + + const PatchInfo = union(enum) { + none, + remove: struct { + name_and_version_hash: u64, + }, + patch: struct { + name_and_version_hash: u64, + patch_path: string, + contents_hash: u64, + }, + + pub fn contentsHash(this: *const @This()) ?u64 { + return switch (this.*) { + .none, .remove => null, + .patch => |patch| patch.contents_hash, + }; + } + + pub fn nameAndVersionHash(this: *const @This()) ?u64 { + return switch (this.*) { + .none, .remove => null, + .patch => |patch| patch.name_and_version_hash, + }; + } + }; + + pub fn packagePatchInfo( + this: *Installer, + pkg_name: String, + pkg_name_hash: PackageNameHash, + pkg_res: *const Resolution, + ) OOM!PatchInfo { + if (this.lockfile.patched_dependencies.entries.len == 0 and this.manager.patched_dependencies_to_remove.entries.len == 0) { + return .none; + } + + const string_buf = this.lockfile.buffers.string_bytes.items; + + var version_buf: std.ArrayListUnmanaged(u8) = .empty; + defer version_buf.deinit(bun.default_allocator); + + var writer = version_buf.writer(this.lockfile.allocator); + try writer.print("{s}@", .{pkg_name.slice(string_buf)}); + + switch (pkg_res.tag) { + .workspace => { + if (this.lockfile.workspace_versions.get(pkg_name_hash)) |workspace_version| { + try writer.print("{}", .{workspace_version.fmt(string_buf)}); + } + }, + else => { + try writer.print("{}", .{pkg_res.fmt(string_buf, .posix)}); + }, + } + + const name_and_version_hash = String.Builder.stringHash(version_buf.items); + + if (this.lockfile.patched_dependencies.get(name_and_version_hash)) |patch| { + return .{ + .patch = .{ + .name_and_version_hash = name_and_version_hash, + .patch_path = patch.path.slice(string_buf), + .contents_hash = patch.patchfileHash().?, + }, + }; + } + + if (this.manager.patched_dependencies_to_remove.contains(name_and_version_hash)) { + return .{ + .remove = .{ + .name_and_version_hash = name_and_version_hash, + }, + }; + } + + return .none; + } + + pub fn linkDependencyBins(this: *const Installer, parent_entry_id: Store.Entry.Id) !void { + const lockfile = this.lockfile; + const store = this.store; + + const string_buf = lockfile.buffers.string_bytes.items; + const extern_string_buf = lockfile.buffers.extern_strings.items; + + const entries = store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + const entry_deps = entries.items(.dependencies); + + const nodes = store.nodes.slice(); + const node_pkg_ids = nodes.items(.pkg_id); + const node_dep_ids = nodes.items(.dep_id); + + const pkgs = lockfile.packages.slice(); + const pkg_bins = pkgs.items(.bin); + + const link_target_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(link_target_buf); + const link_dest_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(link_dest_buf); + const link_rel_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(link_rel_buf); + + var seen: bun.StringHashMap(void) = .init(bun.default_allocator); + defer seen.deinit(); + + var node_modules_path: bun.AbsPath(.{}) = .initTopLevelDir(); + defer node_modules_path.deinit(); + + this.appendStoreNodeModulesPath(&node_modules_path, parent_entry_id); + + for (entry_deps[parent_entry_id.get()].slice()) |dep| { + const node_id = entry_node_ids[dep.entry_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + const bin = pkg_bins[pkg_id]; + if (bin.tag == .none) { + continue; + } + + const alias = lockfile.buffers.dependencies.items[dep_id].name; + + var bin_linker: Bin.Linker = .{ + .bin = bin, + .global_bin_path = this.manager.options.bin_path, + .package_name = strings.StringOrTinyString.init(alias.slice(string_buf)), + .string_buf = string_buf, + .extern_string_buf = extern_string_buf, + .seen = &seen, + .node_modules_path = &node_modules_path, + .abs_target_buf = link_target_buf, + .abs_dest_buf = link_dest_buf, + .rel_buf = link_rel_buf, + }; + + bin_linker.link(false); + + if (bin_linker.err) |err| { + return err; + } + } + } + + pub fn appendStoreNodeModulesPath(this: *const Installer, buf: anytype, entry_id: Store.Entry.Id) void { + const string_buf = this.lockfile.buffers.string_bytes.items; + + const entries = this.store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + + const nodes = this.store.nodes.slice(); + const node_pkg_ids = nodes.items(.pkg_id); + + const pkgs = this.lockfile.packages.slice(); + const pkg_resolutions = pkgs.items(.resolution); + + const node_id = entry_node_ids[entry_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + const pkg_res = pkg_resolutions[pkg_id]; + + switch (pkg_res.tag) { + .root => { + buf.append("node_modules"); + }, + .workspace => { + buf.append(pkg_res.value.workspace.slice(string_buf)); + buf.append("node_modules"); + }, + else => { + buf.appendFmt("node_modules/" ++ Store.modules_dir_name ++ "/{}/node_modules", .{ + Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile), + }); + }, + } + } + + pub fn appendStorePath(this: *const Installer, buf: anytype, entry_id: Store.Entry.Id) void { + const string_buf = this.lockfile.buffers.string_bytes.items; + + const entries = this.store.entries.slice(); + const entry_node_ids = entries.items(.node_id); + + const nodes = this.store.nodes.slice(); + const node_pkg_ids = nodes.items(.pkg_id); + // const node_peers = nodes.items(.peers); + + const pkgs = this.lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + const node_id = entry_node_ids[entry_id.get()]; + // const peers = node_peers[node_id.get()]; + const pkg_id = node_pkg_ids[node_id.get()]; + const pkg_res = pkg_resolutions[pkg_id]; + + switch (pkg_res.tag) { + .root => {}, + .workspace => { + buf.append(pkg_res.value.workspace.slice(string_buf)); + }, + .symlink => { + const symlink_dir_path = this.manager.globalLinkDirPath(); + + buf.clear(); + buf.append(symlink_dir_path); + buf.append(pkg_res.value.symlink.slice(string_buf)); + }, + else => { + const pkg_name = pkg_names[pkg_id]; + buf.append("node_modules/" ++ Store.modules_dir_name); + buf.appendFmt("{}", .{ + Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile), + }); + buf.append("node_modules"); + buf.append(pkg_name.slice(string_buf)); + }, + } + } +}; + +// @sortImports + +const std = @import("std"); +const Hardlinker = @import("./Hardlinker.zig").Hardlinker; +const Symlinker = @import("./Symlinker.zig").Symlinker; + +const bun = @import("bun"); +const Environment = bun.Environment; +const FD = bun.FD; +const Global = bun.Global; +const OOM = bun.OOM; +const Output = bun.Output; +const Progress = bun.Progress; +const ThreadPool = bun.ThreadPool; +const string = bun.string; +const strings = bun.strings; +const sys = bun.sys; +const Bitset = bun.bit_set.DynamicBitSetUnmanaged; +const Command = bun.CLI.Command; +const String = bun.Semver.String; + +const install = bun.install; +const Bin = install.Bin; +const PackageInstall = install.PackageInstall; +const PackageManager = install.PackageManager; +const PackageNameHash = install.PackageNameHash; +const Resolution = install.Resolution; +const Store = install.Store; +const TruncatedPackageNameHash = install.TruncatedPackageNameHash; +const invalid_dependency_id = install.invalid_dependency_id; + +const Lockfile = install.Lockfile; +const Package = Lockfile.Package; diff --git a/src/install/isolated_install/Store.zig b/src/install/isolated_install/Store.zig new file mode 100644 index 0000000000..c3dea7ad9f --- /dev/null +++ b/src/install/isolated_install/Store.zig @@ -0,0 +1,548 @@ +const Ids = struct { + dep_id: DependencyID, + pkg_id: PackageID, +}; + +pub const Store = struct { + entries: Entry.List, + nodes: Node.List, + + const log = Output.scoped(.Store, false); + + pub const modules_dir_name = ".bun"; + + fn NewId(comptime T: type) type { + return enum(u32) { + comptime { + _ = T; + } + + root = 0, + invalid = max, + _, + + const max = std.math.maxInt(u32); + + pub fn from(id: u32) @This() { + bun.debugAssert(id != max); + return @enumFromInt(id); + } + + pub fn get(id: @This()) u32 { + bun.debugAssert(id != .invalid); + return @intFromEnum(id); + } + + pub fn tryGet(id: @This()) ?u32 { + return if (id == .invalid) null else @intFromEnum(id); + } + + pub fn getOr(id: @This(), default: u32) u32 { + return if (id == .invalid) default else @intFromEnum(id); + } + }; + } + + comptime { + bun.assert(NewId(Entry) != NewId(Node)); + bun.assert(NewId(Entry) == NewId(Entry)); + } + + pub const Installer = @import("./Installer.zig").Installer; + + pub fn isCycle(this: *const Store, id: Entry.Id, maybe_parent_id: Entry.Id, parent_dedupe: *std.AutoArrayHashMap(Entry.Id, void)) bool { + var i: usize = 0; + var len: usize = 0; + + const entry_parents = this.entries.items(.parents); + + for (entry_parents[id.get()].items) |parent_id| { + if (parent_id == .invalid) { + continue; + } + if (parent_id == maybe_parent_id) { + return true; + } + parent_dedupe.put(parent_id, {}) catch bun.outOfMemory(); + } + + len = parent_dedupe.count(); + while (i < len) { + for (entry_parents[parent_dedupe.keys()[i].get()].items) |parent_id| { + if (parent_id == .invalid) { + continue; + } + if (parent_id == maybe_parent_id) { + return true; + } + parent_dedupe.put(parent_id, {}) catch bun.outOfMemory(); + len = parent_dedupe.count(); + } + i += 1; + } + + return false; + } + + // A unique entry in the store. As a path looks like: + // './node_modules/.bun/name@version/node_modules/name' + // or if peers are involved: + // './node_modules/.bun/name@version_peer1@version+peer2@version/node_modules/name' + // + // Entries are created for workspaces (including the root), but only in memory. If + // a module depends on a workspace, a symlink is created pointing outside the store + // directory to the workspace. + pub const Entry = struct { + // Used to get dependency name for destination path and peers + // for store path + node_id: Node.Id, + // parent_id: Id, + dependencies: Dependencies, + parents: std.ArrayListUnmanaged(Id) = .empty, + step: std.atomic.Value(Installer.Task.Step) = .init(.link_package), + + peer_hash: PeerHash, + + scripts: ?*Package.Scripts.List = null, + + pub const PeerHash = enum(u64) { + none = 0, + _, + + pub fn from(int: u64) @This() { + return @enumFromInt(int); + } + + pub fn cast(this: @This()) u64 { + return @intFromEnum(this); + } + }; + + const StorePathFormatter = struct { + entry_id: Id, + store: *const Store, + lockfile: *const Lockfile, + + pub fn format(this: @This(), comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { + const store = this.store; + const entries = store.entries.slice(); + const entry_peer_hashes = entries.items(.peer_hash); + const entry_node_ids = entries.items(.node_id); + + const peer_hash = entry_peer_hashes[this.entry_id.get()]; + const node_id = entry_node_ids[this.entry_id.get()]; + const pkg_id = store.nodes.items(.pkg_id)[node_id.get()]; + + const string_buf = this.lockfile.buffers.string_bytes.items; + + const pkgs = this.lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + const pkg_name = pkg_names[pkg_id]; + const pkg_res = pkg_resolutions[pkg_id]; + + switch (pkg_res.tag) { + .folder => { + try writer.print("{}@file+{}", .{ + pkg_name.fmtStorePath(string_buf), + pkg_res.value.folder.fmtStorePath(string_buf), + }); + }, + else => { + try writer.print("{}@{}", .{ + pkg_name.fmtStorePath(string_buf), + pkg_res.fmtStorePath(string_buf), + }); + }, + } + + if (peer_hash != .none) { + try writer.print("+{}", .{ + bun.fmt.hexIntLower(peer_hash.cast()), + }); + } + } + }; + + pub fn fmtStorePath(entry_id: Id, store: *const Store, lockfile: *const Lockfile) StorePathFormatter { + return .{ .entry_id = entry_id, .store = store, .lockfile = lockfile }; + } + + pub fn debugGatherAllParents(entry_id: Id, store: *const Store) []const Id { + var i: usize = 0; + var len: usize = 0; + + const entry_parents = store.entries.items(.parents); + + var parents: std.AutoArrayHashMapUnmanaged(Entry.Id, void) = .empty; + // defer parents.deinit(bun.default_allocator); + + for (entry_parents[entry_id.get()].items) |parent_id| { + if (parent_id == .invalid) { + continue; + } + parents.put(bun.default_allocator, parent_id, {}) catch bun.outOfMemory(); + } + + len = parents.count(); + while (i < len) { + for (entry_parents[parents.entries.items(.key)[i].get()].items) |parent_id| { + if (parent_id == .invalid) { + continue; + } + parents.put(bun.default_allocator, parent_id, {}) catch bun.outOfMemory(); + len = parents.count(); + } + i += 1; + } + + return parents.keys(); + } + + pub const List = bun.MultiArrayList(Entry); + + const DependenciesItem = struct { + entry_id: Id, + + // TODO: this can be removed, and instead dep_id can be retrieved through: + // entry_id -> node_id -> node_dep_ids + dep_id: DependencyID, + }; + pub const Dependencies = OrderedArraySet(DependenciesItem, DependenciesOrderedArraySetCtx); + + pub const DependenciesOrderedArraySetCtx = struct { + string_buf: string, + dependencies: []const Dependency, + + pub fn eql(ctx: *const DependenciesOrderedArraySetCtx, l_item: DependenciesItem, r_item: DependenciesItem) bool { + if (l_item.entry_id != r_item.entry_id) { + return false; + } + + const dependencies = ctx.dependencies; + const l_dep = dependencies[l_item.dep_id]; + const r_dep = dependencies[r_item.dep_id]; + + return l_dep.name_hash == r_dep.name_hash; + } + + pub fn order(ctx: *const DependenciesOrderedArraySetCtx, l: DependenciesItem, r: DependenciesItem) std.math.Order { + const dependencies = ctx.dependencies; + const l_dep = dependencies[l.dep_id]; + const r_dep = dependencies[r.dep_id]; + + if (l.entry_id == r.entry_id and l_dep.name_hash == r_dep.name_hash) { + return .eq; + } + + // TODO: y r doing + if (l.entry_id == .invalid) { + if (r.entry_id == .invalid) { + return .eq; + } + return .lt; + } else if (r.entry_id == .invalid) { + if (l.entry_id == .invalid) { + return .eq; + } + return .gt; + } + + const string_buf = ctx.string_buf; + const l_dep_name = l_dep.name; + const r_dep_name = r_dep.name; + + return l_dep_name.order(&r_dep_name, string_buf, string_buf); + } + }; + + pub const Id = NewId(Entry); + + pub fn debugPrintList(list: *const List, lockfile: *Lockfile) void { + const string_buf = lockfile.buffers.string_bytes.items; + + const pkgs = lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + for (0..list.len) |entry_id| { + const entry = list.get(entry_id); + const entry_pkg_name = pkg_names[entry.pkg_id].slice(string_buf); + log( + \\entry ({d}): '{s}@{}' + \\ dep_name: {s} + \\ pkg_id: {d} + \\ parent_id: {} + \\ + , .{ + entry_id, + entry_pkg_name, + pkg_resolutions[entry.pkg_id].fmt(string_buf, .posix), + entry.dep_name.slice(string_buf), + entry.pkg_id, + entry.parent_id, + }); + + log(" dependencies ({d}):\n", .{entry.dependencies.items.len}); + for (entry.dependencies.items) |dep_entry_id| { + const dep_entry = list.get(dep_entry_id.get()); + log(" {s}@{}\n", .{ + pkg_names[dep_entry.pkg_id].slice(string_buf), + pkg_resolutions[dep_entry.pkg_id].fmt(string_buf, .posix), + }); + } + } + } + }; + + pub fn OrderedArraySet(comptime T: type, comptime Ctx: type) type { + return struct { + list: std.ArrayListUnmanaged(T) = .empty, + + pub const empty: @This() = .{}; + + pub fn initCapacity(allocator: std.mem.Allocator, n: usize) OOM!@This() { + const list: std.ArrayListUnmanaged(T) = try .initCapacity(allocator, n); + return .{ .list = list }; + } + + pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { + this.list.deinit(allocator); + } + + pub fn slice(this: *const @This()) []const T { + return this.list.items; + } + + pub fn len(this: *const @This()) usize { + return this.list.items.len; + } + + pub fn eql(l: *const @This(), r: *const @This(), ctx: *const Ctx) bool { + if (l.list.items.len != r.list.items.len) { + return false; + } + + for (l.list.items, r.list.items) |l_item, r_item| { + if (!ctx.eql(l_item, r_item)) { + return false; + } + } + + return true; + } + + pub fn insert(this: *@This(), allocator: std.mem.Allocator, new: T, ctx: *const Ctx) OOM!void { + for (0..this.list.items.len) |i| { + const existing = this.list.items[i]; + if (ctx.eql(new, existing)) { + return; + } + + const order = ctx.order(new, existing); + + if (order == .eq) { + return; + } + + if (order == .lt) { + try this.list.insert(allocator, i, new); + return; + } + } + + try this.list.append(allocator, new); + } + + pub fn insertAssumeCapacity(this: *@This(), new: T, ctx: *const Ctx) void { + for (0..this.list.items.len) |i| { + const existing = this.list.items[i]; + if (ctx.eql(new, existing)) { + return; + } + + const order = ctx.order(new, existing); + + if (order == .eq) { + return; + } + + if (order == .lt) { + this.list.insertAssumeCapacity(i, new); + return; + } + } + + this.list.appendAssumeCapacity(new); + } + }; + } + + // A node used to represent the full dependency tree. Uniqueness is determined + // from `pkg_id` and `peers` + pub const Node = struct { + dep_id: DependencyID, + pkg_id: PackageID, + parent_id: Id, + + dependencies: std.ArrayListUnmanaged(Ids) = .empty, + peers: Peers = .empty, + + // each node in this list becomes a symlink in the package's node_modules + nodes: std.ArrayListUnmanaged(Id) = .empty, + + pub const Peers = OrderedArraySet(TransitivePeer, TransitivePeer.OrderedArraySetCtx); + + pub const TransitivePeer = struct { + dep_id: DependencyID, + pkg_id: PackageID, + auto_installed: bool, + + pub const OrderedArraySetCtx = struct { + string_buf: string, + pkg_names: []const String, + + pub fn eql(ctx: *const OrderedArraySetCtx, l_item: TransitivePeer, r_item: TransitivePeer) bool { + _ = ctx; + return l_item.pkg_id == r_item.pkg_id; + } + + pub fn order(ctx: *const OrderedArraySetCtx, l: TransitivePeer, r: TransitivePeer) std.math.Order { + const l_pkg_id = l.pkg_id; + const r_pkg_id = r.pkg_id; + if (l_pkg_id == r_pkg_id) { + return .eq; + } + + const string_buf = ctx.string_buf; + const pkg_names = ctx.pkg_names; + const l_pkg_name = pkg_names[l_pkg_id]; + const r_pkg_name = pkg_names[r_pkg_id]; + + return l_pkg_name.order(&r_pkg_name, string_buf, string_buf); + } + }; + }; + + pub const List = bun.MultiArrayList(Node); + + pub fn deinitList(list: *const List, allocator: std.mem.Allocator) void { + list.deinit(allocator); + } + + pub fn debugPrint(this: *const Node, id: Id, lockfile: *const Lockfile) void { + const pkgs = lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + const string_buf = lockfile.buffers.string_bytes.items; + const deps = lockfile.buffers.dependencies.items; + + const dep_name = if (this.dep_id == invalid_dependency_id) "root" else deps[this.dep_id].name.slice(string_buf); + const dep_version = if (this.dep_id == invalid_dependency_id) "root" else deps[this.dep_id].version.literal.slice(string_buf); + + log( + \\node({d}) + \\ deps: {s}@{s} + \\ res: {s}@{} + \\ + , .{ + id, + dep_name, + dep_version, + pkg_names[this.pkg_id].slice(string_buf), + pkg_resolutions[this.pkg_id].fmt(string_buf, .posix), + }); + } + + pub const Id = NewId(Node); + + pub fn debugPrintList(list: *const List, lockfile: *const Lockfile) void { + const string_buf = lockfile.buffers.string_bytes.items; + const dependencies = lockfile.buffers.dependencies.items; + + const pkgs = lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_resolutions = pkgs.items(.resolution); + + for (0..list.len) |node_id| { + const node = list.get(node_id); + const node_pkg_name = pkg_names[node.pkg_id].slice(string_buf); + log( + \\node ({d}): '{s}' + \\ dep_id: {d} + \\ pkg_id: {d} + \\ parent_id: {} + \\ + , .{ + node_id, + node_pkg_name, + node.dep_id, + node.pkg_id, + node.parent_id, + }); + + log(" dependencies ({d}):\n", .{node.dependencies.items.len}); + for (node.dependencies.items) |ids| { + const dep = dependencies[ids.dep_id]; + const dep_name = dep.name.slice(string_buf); + + const pkg_name = pkg_names[ids.pkg_id].slice(string_buf); + const pkg_res = pkg_resolutions[ids.pkg_id]; + + log(" {s}@{} ({s}@{s})\n", .{ + pkg_name, + pkg_res.fmt(string_buf, .posix), + dep_name, + dep.version.literal.slice(string_buf), + }); + } + + log(" nodes ({d}): ", .{node.nodes.items.len}); + for (node.nodes.items, 0..) |id, i| { + log("{d}", .{id.get()}); + if (i != node.nodes.items.len - 1) { + log(",", .{}); + } + } + + log("\n peers ({d}):\n", .{node.peers.list.items.len}); + for (node.peers.list.items) |ids| { + const dep = dependencies[ids.dep_id]; + const dep_name = dep.name.slice(string_buf); + const pkg_name = pkg_names[ids.pkg_id].slice(string_buf); + const pkg_res = pkg_resolutions[ids.pkg_id]; + + log(" {s}@{} ({s}@{s})\n", .{ + pkg_name, + pkg_res.fmt(string_buf, .posix), + dep_name, + dep.version.literal.slice(string_buf), + }); + } + } + } + }; +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const OOM = bun.OOM; +const Output = bun.Output; +const string = bun.string; + +const Semver = bun.Semver; +const String = Semver.String; + +const install = bun.install; +const Dependency = install.Dependency; +const DependencyID = install.DependencyID; +const PackageID = install.PackageID; +const invalid_dependency_id = install.invalid_dependency_id; + +const Lockfile = install.Lockfile; +const Package = Lockfile.Package; diff --git a/src/install/isolated_install/Symlinker.zig b/src/install/isolated_install/Symlinker.zig new file mode 100644 index 0000000000..48c4233b24 --- /dev/null +++ b/src/install/isolated_install/Symlinker.zig @@ -0,0 +1,115 @@ +pub const Symlinker = struct { + dest: bun.Path(.{ .sep = .auto }), + target: bun.RelPath(.{ .sep = .auto }), + fallback_junction_target: bun.AbsPath(.{ .sep = .auto }), + + pub fn symlink(this: *const @This()) sys.Maybe(void) { + if (comptime Environment.isWindows) { + return sys.symlinkOrJunction(this.dest.sliceZ(), this.target.sliceZ(), this.fallback_junction_target.sliceZ()); + } + return sys.symlink(this.target.sliceZ(), this.dest.sliceZ()); + } + + pub const Strategy = enum { + expect_existing, + expect_missing, + ignore_failure, + }; + + pub fn ensureSymlink( + this: *const @This(), + strategy: Strategy, + ) sys.Maybe(void) { + return switch (strategy) { + .ignore_failure => { + return switch (this.symlink()) { + .result => .success, + .err => |symlink_err| switch (symlink_err.getErrno()) { + .NOENT => { + const dest_parent = this.dest.dirname() orelse { + return .success; + }; + + FD.cwd().makePath(u8, dest_parent) catch {}; + _ = this.symlink(); + return .success; + }, + else => .success, + }, + }; + }, + .expect_missing => { + return switch (this.symlink()) { + .result => .success, + .err => |symlink_err1| switch (symlink_err1.getErrno()) { + .NOENT => { + const dest_parent = this.dest.dirname() orelse { + return .initErr(symlink_err1); + }; + + FD.cwd().makePath(u8, dest_parent) catch {}; + return this.symlink(); + }, + .EXIST => { + FD.cwd().deleteTree(this.dest.sliceZ()) catch {}; + return this.symlink(); + }, + else => .initErr(symlink_err1), + }, + }; + }, + .expect_existing => { + const current_link_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(current_link_buf); + var current_link: []const u8 = switch (sys.readlink(this.dest.sliceZ(), current_link_buf)) { + .result => |res| res, + .err => |readlink_err| return switch (readlink_err.getErrno()) { + .NOENT => switch (this.symlink()) { + .result => .success, + .err => |symlink_err| switch (symlink_err.getErrno()) { + .NOENT => { + const dest_parent = this.dest.dirname() orelse { + return .initErr(symlink_err); + }; + + FD.cwd().makePath(u8, dest_parent) catch {}; + return this.symlink(); + }, + else => .initErr(symlink_err), + }, + }, + else => { + FD.cwd().deleteTree(this.dest.sliceZ()) catch {}; + return this.symlink(); + }, + }, + }; + + // libuv adds a trailing slash to junctions. + current_link = strings.withoutTrailingSlash(current_link); + + if (strings.eqlLong(current_link, this.target.sliceZ(), true)) { + return .success; + } + + if (comptime Environment.isWindows) { + if (strings.eqlLong(current_link, this.fallback_junction_target.slice(), true)) { + return .success; + } + } + + // this existing link is pointing to the wrong package + _ = sys.unlink(this.dest.sliceZ()); + return this.symlink(); + }, + }; + } +}; + +// @sortImports + +const bun = @import("bun"); +const Environment = bun.Environment; +const FD = bun.FD; +const strings = bun.strings; +const sys = bun.sys; diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index 0ccb974c52..2b249771b8 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -8,6 +8,7 @@ const Global = bun.Global; const JSC = bun.JSC; const Timer = std.time.Timer; const string = bun.string; +const Store = bun.install.Store; const Process = bun.spawn.Process; const log = Output.scoped(.Script, false); @@ -24,6 +25,7 @@ pub const LifecycleScriptSubprocess = struct { has_called_process_exit: bool = false, manager: *PackageManager, envp: [:null]?[*:0]const u8, + shell_bin: ?[:0]const u8, timer: ?Timer = null, @@ -33,8 +35,15 @@ pub const LifecycleScriptSubprocess = struct { optional: bool = false, started_at: u64 = 0, + ctx: ?InstallCtx, + heap: bun.io.heap.IntrusiveField(LifecycleScriptSubprocess) = .{}, + pub const InstallCtx = struct { + entry_id: Store.Entry.Id, + installer: *Store.Installer, + }; + pub const List = bun.io.heap.Intrusive(LifecycleScriptSubprocess, *PackageManager, sortByStartedAt); fn sortByStartedAt(_: *PackageManager, a: *LifecycleScriptSubprocess, b: *LifecycleScriptSubprocess) bool { @@ -94,9 +103,6 @@ pub const LifecycleScriptSubprocess = struct { this.handleExit(process.status); } - // This is only used on the main thread. - var cwd_z_buf: bun.PathBuffer = undefined; - fn resetOutputFlags(output: *OutputReader, fd: bun.FileDescriptor) void { output.flags.nonblocking = true; output.flags.socket = true; @@ -139,7 +145,6 @@ pub const LifecycleScriptSubprocess = struct { const manager = this.manager; const original_script = this.scripts.items[next_script_index].?; const cwd = this.scripts.cwd; - const env = manager.env; this.stdout.setParent(this); this.stderr.setParent(this); @@ -148,11 +153,9 @@ pub const LifecycleScriptSubprocess = struct { this.current_script_index = next_script_index; this.has_called_process_exit = false; - const shell_bin = if (Environment.isWindows) null else bun.CLI.RunCommand.findShell(env.get("PATH") orelse "", cwd) orelse null; - - var copy_script = try std.ArrayList(u8).initCapacity(manager.allocator, original_script.script.len + 1); + var copy_script = try std.ArrayList(u8).initCapacity(manager.allocator, original_script.len + 1); defer copy_script.deinit(); - try bun.CLI.RunCommand.replacePackageManagerRun(©_script, original_script.script); + try bun.CLI.RunCommand.replacePackageManagerRun(©_script, original_script); try copy_script.append(0); const combined_script: [:0]u8 = copy_script.items[0 .. copy_script.items.len - 1 :0]; @@ -174,8 +177,8 @@ pub const LifecycleScriptSubprocess = struct { log("{s} - {s} $ {s}", .{ this.package_name, this.scriptName(), combined_script }); - var argv = if (shell_bin != null and !Environment.isWindows) [_]?[*:0]const u8{ - shell_bin.?, + var argv = if (this.shell_bin != null and !Environment.isWindows) [_]?[*:0]const u8{ + this.shell_bin.?, "-c", combined_script, null, @@ -347,6 +350,10 @@ pub const LifecycleScriptSubprocess = struct { if (exit.code > 0) { if (this.optional) { + if (this.ctx) |ctx| { + ctx.installer.store.entries.items(.step)[ctx.entry_id.get()].store(.done, .monotonic); + ctx.installer.onTaskComplete(ctx.entry_id, .fail); + } _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic); this.deinitAndDeletePackage(); return; @@ -383,6 +390,21 @@ pub const LifecycleScriptSubprocess = struct { } } + if (this.ctx) |ctx| { + switch (this.current_script_index) { + // preinstall + 0 => { + const previous_step = ctx.installer.store.entries.items(.step)[ctx.entry_id.get()].swap(.binaries, .monotonic); + bun.debugAssert(previous_step == .run_preinstall); + ctx.installer.startTask(ctx.entry_id); + _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic); + this.deinit(); + return; + }, + else => {}, + } + } + for (this.current_script_index + 1..Lockfile.Scripts.names.len) |new_script_index| { if (this.scripts.items[new_script_index] != null) { this.resetPolls(); @@ -403,6 +425,15 @@ pub const LifecycleScriptSubprocess = struct { }); } + if (this.ctx) |ctx| { + const previous_step = ctx.installer.store.entries.items(.step)[ctx.entry_id.get()].swap(.done, .monotonic); + if (comptime Environment.ci_assert) { + bun.assertWithLocation(this.current_script_index != 0, @src()); + bun.assertWithLocation(previous_step == .@"run (post)install and (pre/post)prepare", @src()); + } + ctx.installer.onTaskComplete(ctx.entry_id, .success); + } + // the last script finished _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic); @@ -422,6 +453,10 @@ pub const LifecycleScriptSubprocess = struct { }, .err => |err| { if (this.optional) { + if (this.ctx) |ctx| { + ctx.installer.store.entries.items(.step)[ctx.entry_id.get()].store(.done, .monotonic); + ctx.installer.onTaskComplete(ctx.entry_id, .fail); + } _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic); this.deinitAndDeletePackage(); return; @@ -506,17 +541,21 @@ pub const LifecycleScriptSubprocess = struct { manager: *PackageManager, list: Lockfile.Package.Scripts.List, envp: [:null]?[*:0]const u8, + shell_bin: ?[:0]const u8, optional: bool, log_level: PackageManager.Options.LogLevel, foreground: bool, + ctx: ?InstallCtx, ) !void { var lifecycle_subprocess = LifecycleScriptSubprocess.new(.{ .manager = manager, .envp = envp, + .shell_bin = shell_bin, .scripts = list, .package_name = list.package_name, .foreground = foreground, .optional = optional, + .ctx = ctx, }); if (log_level.isVerbose()) { diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 1bb7834bc4..209af71f1a 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -26,15 +26,52 @@ patched_dependencies: PatchedDependenciesMap = .{}, overrides: OverrideMap = .{}, catalogs: CatalogMap = .{}, +node_linker: NodeLinker = .auto, + +pub const NodeLinker = enum(u8) { + // If workspaces are used: isolated + // If not: hoisted + // Used when nodeLinker is absent from package.json/bun.lock/bun.lockb + auto, + + hoisted, + isolated, + + pub fn fromStr(input: string) ?NodeLinker { + if (strings.eqlComptime(input, "hoisted")) { + return .hoisted; + } + if (strings.eqlComptime(input, "isolated")) { + return .isolated; + } + return null; + } +}; + +pub const DepSorter = struct { + lockfile: *const Lockfile, + + pub fn isLessThan(sorter: @This(), l: DependencyID, r: DependencyID) bool { + const deps_buf = sorter.lockfile.buffers.dependencies.items; + const string_buf = sorter.lockfile.buffers.string_bytes.items; + + const l_dep = &deps_buf[l]; + const r_dep = &deps_buf[r]; + + return switch (l_dep.behavior.cmp(r_dep.behavior)) { + .lt => true, + .gt => false, + .eq => strings.order(l_dep.name.slice(string_buf), r_dep.name.slice(string_buf)) == .lt, + }; + } +}; + pub const Stream = std.io.FixedBufferStream([]u8); pub const default_filename = "bun.lockb"; pub const Scripts = struct { const MAX_PARALLEL_PROCESSES = 10; - pub const Entry = struct { - script: string, - }; - pub const Entries = std.ArrayListUnmanaged(Entry); + pub const Entries = std.ArrayListUnmanaged(string); pub const names = [_]string{ "preinstall", @@ -73,7 +110,7 @@ pub const Scripts = struct { inline for (Scripts.names) |hook| { const list = &@field(this, hook); for (list.items) |entry| { - allocator.free(entry.script); + allocator.free(entry); } list.deinit(allocator); } @@ -618,6 +655,8 @@ pub fn cleanWithLogger( try new.buffers.preallocate(old.buffers, old.allocator); try new.patched_dependencies.ensureTotalCapacity(old.allocator, old.patched_dependencies.entries.len); + new.node_linker = old.node_linker; + old.scratch.dependency_list_queue.head = 0; { @@ -873,8 +912,6 @@ pub fn hoist( const allocator = lockfile.allocator; var slice = lockfile.packages.slice(); - var path_buf: bun.PathBuffer = undefined; - var builder = Tree.Builder(method){ .name_hashes = slice.items(.name_hash), .queue = .init(allocator), @@ -885,7 +922,6 @@ pub fn hoist( .log = log, .lockfile = lockfile, .manager = manager, - .path_buf = &path_buf, .install_root_dependencies = install_root_dependencies, .workspace_filters = workspace_filters, }; @@ -895,7 +931,6 @@ pub fn hoist( Tree.invalid_id, method, &builder, - if (method == .filter) manager.options.log_level, ); // This goes breadth-first @@ -905,7 +940,6 @@ pub fn hoist( item.hoist_root_id, method, &builder, - if (method == .filter) manager.options.log_level, ); } @@ -1207,6 +1241,7 @@ pub fn initEmpty(this: *Lockfile, allocator: Allocator) void { .workspace_versions = .{}, .overrides = .{}, .catalogs = .{}, + .node_linker = .auto, .meta_hash = zero_hash, }; } @@ -1807,8 +1842,8 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag inline for (comptime std.meta.fieldNames(Lockfile.Scripts)) |field_name| { const scripts = @field(this.scripts, field_name); for (scripts.items) |script| { - if (script.script.len > 0) { - string_builder.fmtCount("{s}: {s}\n", .{ field_name, script.script }); + if (script.len > 0) { + string_builder.fmtCount("{s}: {s}\n", .{ field_name, script }); has_scripts = true; } } @@ -1843,8 +1878,8 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag inline for (comptime std.meta.fieldNames(Lockfile.Scripts)) |field_name| { const scripts = @field(this.scripts, field_name); for (scripts.items) |script| { - if (script.script.len > 0) { - _ = string_builder.fmt("{s}: {s}\n", .{ field_name, script.script }); + if (script.len > 0) { + _ = string_builder.fmt("{s}: {s}\n", .{ field_name, script }); } } } @@ -1952,17 +1987,17 @@ pub const default_trusted_dependencies = brk: { @compileError("default-trusted-dependencies.txt is too large, please increase 'max_default_trusted_dependencies' in lockfile.zig"); } - // just in case there's duplicates from truncating - if (map.has(dep)) @compileError("Duplicate hash due to u64 -> u32 truncation"); - - map.putAssumeCapacity(dep, {}); + const entry = map.getOrPutAssumeCapacity(dep); + if (entry.found_existing) { + @compileError("Duplicate trusted dependency: " ++ dep); + } } const final = map; break :brk &final; }; -pub fn hasTrustedDependency(this: *Lockfile, name: []const u8) bool { +pub fn hasTrustedDependency(this: *const Lockfile, name: []const u8) bool { if (this.trusted_dependencies) |trusted_dependencies| { const hash = @as(u32, @truncate(String.Builder.stringHash(name))); return trusted_dependencies.contains(hash); diff --git a/src/install/lockfile/Package.zig b/src/install/lockfile/Package.zig index 09e1ebb295..8edae3a083 100644 --- a/src/install/lockfile/Package.zig +++ b/src/install/lockfile/Package.zig @@ -527,6 +527,7 @@ pub const Package = extern struct { update: u32 = 0, overrides_changed: bool = false, catalogs_changed: bool = false, + node_linker_changed: bool = false, // bool for if this dependency should be added to lockfile trusted dependencies. // it is false when the new trusted dependency is coming from the default list. @@ -543,6 +544,7 @@ pub const Package = extern struct { pub inline fn hasDiffs(this: Summary) bool { return this.add > 0 or this.remove > 0 or this.update > 0 or this.overrides_changed or this.catalogs_changed or + this.node_linker_changed or this.added_trusted_dependencies.count() > 0 or this.removed_trusted_dependencies.count() > 0 or this.patched_dependencies_changed; @@ -658,6 +660,10 @@ pub const Package = extern struct { } } } + + if (from_lockfile.node_linker != to_lockfile.node_linker) { + summary.node_linker_changed = true; + } } trusted_dependencies: { @@ -1576,6 +1582,19 @@ pub const Package = extern struct { if (json.get("workspaces")) |workspaces_expr| { lockfile.catalogs.parseCount(lockfile, workspaces_expr, &string_builder); + + if (workspaces_expr.get("nodeLinker")) |node_linker_expr| { + if (!node_linker_expr.isString()) { + try log.addError(source, node_linker_expr.loc, "Expected one of \"isolated\" or \"hoisted\""); + return error.InvalidPackageJSON; + } + + const node_linker_str = node_linker_expr.data.e_string.slice(allocator); + lockfile.node_linker = Lockfile.NodeLinker.fromStr(node_linker_str) orelse { + try log.addError(source, node_linker_expr.loc, "Expected one of \"isolated\" or \"hoisted\""); + return error.InvalidPackageJSON; + }; + } } } diff --git a/src/install/lockfile/Package/Scripts.zig b/src/install/lockfile/Package/Scripts.zig index a93ce33020..516a8017e8 100644 --- a/src/install/lockfile/Package/Scripts.zig +++ b/src/install/lockfile/Package/Scripts.zig @@ -17,12 +17,22 @@ pub const Scripts = extern struct { } pub const List = struct { - items: [Lockfile.Scripts.names.len]?Lockfile.Scripts.Entry, + items: [Lockfile.Scripts.names.len]?string, first_index: u8, total: u8, cwd: stringZ, package_name: string, + pub fn initPreinstall(allocator: std.mem.Allocator, preinstall: string, cwd: string, package_name: string) @This() { + return .{ + .items = .{ allocator.dupe(u8, preinstall) catch bun.outOfMemory(), null, null, null, null, null }, + .first_index = 0, + .total = 1, + .cwd = allocator.dupeZ(u8, cwd) catch bun.outOfMemory(), + .package_name = allocator.dupe(u8, package_name) catch bun.outOfMemory(), + }; + } + pub fn printScripts( this: Package.Scripts.List, resolution: *const Resolution, @@ -51,28 +61,28 @@ pub const Scripts = extern struct { if (maybe_script) |script| { Output.pretty(fmt, .{ Lockfile.Scripts.names[script_index], - script.script, + script, }); } } } - pub fn first(this: Package.Scripts.List) Lockfile.Scripts.Entry { + pub fn first(this: Package.Scripts.List) string { if (comptime Environment.allow_assert) { assert(this.items[this.first_index] != null); } return this.items[this.first_index].?; } - pub fn deinit(this: Package.Scripts.List, allocator: std.mem.Allocator) void { - for (this.items) |maybe_item| { - if (maybe_item) |item| { - allocator.free(item.script); - } - } + // pub fn deinit(this: Package.Scripts.List, allocator: std.mem.Allocator) void { + // for (this.items) |maybe_item| { + // if (maybe_item) |item| { + // allocator.free(item); + // } + // } - allocator.free(this.cwd); - } + // allocator.free(this.cwd); + // } pub fn appendToLockfile(this: Package.Scripts.List, lockfile: *Lockfile) void { inline for (this.items, 0..) |maybe_script, i| { @@ -110,37 +120,31 @@ pub const Scripts = extern struct { pub fn getScriptEntries( this: *const Package.Scripts, - lockfile: *Lockfile, + lockfile: *const Lockfile, lockfile_buf: string, resolution_tag: Resolution.Tag, add_node_gyp_rebuild_script: bool, // return: first_index, total, entries - ) struct { i8, u8, [Lockfile.Scripts.names.len]?Lockfile.Scripts.Entry } { + ) struct { i8, u8, [Lockfile.Scripts.names.len]?string } { const allocator = lockfile.allocator; var script_index: u8 = 0; var first_script_index: i8 = -1; - var scripts: [6]?Lockfile.Scripts.Entry = .{null} ** 6; + var scripts: [6]?string = .{null} ** 6; var counter: u8 = 0; if (add_node_gyp_rebuild_script) { { script_index += 1; - const entry: Lockfile.Scripts.Entry = .{ - .script = allocator.dupe(u8, "node-gyp rebuild") catch unreachable, - }; if (first_script_index == -1) first_script_index = @intCast(script_index); - scripts[script_index] = entry; + scripts[script_index] = allocator.dupe(u8, "node-gyp rebuild") catch unreachable; script_index += 1; counter += 1; } // missing install and preinstall, only need to check postinstall if (!this.postinstall.isEmpty()) { - const entry: Lockfile.Scripts.Entry = .{ - .script = allocator.dupe(u8, this.preinstall.slice(lockfile_buf)) catch unreachable, - }; if (first_script_index == -1) first_script_index = @intCast(script_index); - scripts[script_index] = entry; + scripts[script_index] = allocator.dupe(u8, this.preinstall.slice(lockfile_buf)) catch unreachable; counter += 1; } script_index += 1; @@ -154,11 +158,8 @@ pub const Scripts = extern struct { inline for (install_scripts) |hook| { const script = @field(this, hook); if (!script.isEmpty()) { - const entry: Lockfile.Scripts.Entry = .{ - .script = allocator.dupe(u8, script.slice(lockfile_buf)) catch unreachable, - }; if (first_script_index == -1) first_script_index = @intCast(script_index); - scripts[script_index] = entry; + scripts[script_index] = allocator.dupe(u8, script.slice(lockfile_buf)) catch unreachable; counter += 1; } script_index += 1; @@ -176,11 +177,8 @@ pub const Scripts = extern struct { inline for (prepare_scripts) |hook| { const script = @field(this, hook); if (!script.isEmpty()) { - const entry: Lockfile.Scripts.Entry = .{ - .script = allocator.dupe(u8, script.slice(lockfile_buf)) catch unreachable, - }; if (first_script_index == -1) first_script_index = @intCast(script_index); - scripts[script_index] = entry; + scripts[script_index] = allocator.dupe(u8, script.slice(lockfile_buf)) catch unreachable; counter += 1; } script_index += 1; @@ -189,11 +187,8 @@ pub const Scripts = extern struct { .workspace => { script_index += 1; if (!this.prepare.isEmpty()) { - const entry: Lockfile.Scripts.Entry = .{ - .script = allocator.dupe(u8, this.prepare.slice(lockfile_buf)) catch unreachable, - }; if (first_script_index == -1) first_script_index = @intCast(script_index); - scripts[script_index] = entry; + scripts[script_index] = allocator.dupe(u8, this.prepare.slice(lockfile_buf)) catch unreachable; counter += 1; } script_index += 2; @@ -206,9 +201,9 @@ pub const Scripts = extern struct { pub fn createList( this: *const Package.Scripts, - lockfile: *Lockfile, + lockfile: *const Lockfile, lockfile_buf: []const u8, - cwd_: string, + cwd_: *bun.AbsPath(.{ .sep = .auto }), package_name: string, resolution_tag: Resolution.Tag, add_node_gyp_rebuild_script: bool, @@ -219,16 +214,10 @@ pub const Scripts = extern struct { var cwd_buf: if (Environment.isWindows) bun.PathBuffer else void = undefined; const cwd = if (comptime !Environment.isWindows) - cwd_ + cwd_.slice() else brk: { - @memcpy(cwd_buf[0..cwd_.len], cwd_); - cwd_buf[cwd_.len] = 0; - const cwd_handle = bun.openDirNoRenamingOrDeletingWindows(bun.invalid_fd, cwd_buf[0..cwd_.len :0]) catch break :brk cwd_; - - var buf: bun.WPathBuffer = undefined; - const new_cwd = bun.windows.GetFinalPathNameByHandle(cwd_handle.fd, .{}, &buf) catch break :brk cwd_; - - break :brk strings.convertUTF16toUTF8InBuffer(&cwd_buf, new_cwd) catch break :brk cwd_; + const cwd_handle = bun.openDirNoRenamingOrDeletingWindows(bun.invalid_fd, cwd_.sliceZ()) catch break :brk cwd_.slice(); + break :brk FD.fromStdDir(cwd_handle).getFdPath(&cwd_buf) catch break :brk cwd_.slice(); }; return .{ @@ -274,54 +263,36 @@ pub const Scripts = extern struct { pub fn getList( this: *Package.Scripts, log: *logger.Log, - lockfile: *Lockfile, - node_modules: *PackageManager.PackageInstaller.LazyPackageDestinationDir, - abs_node_modules_path: string, + lockfile: *const Lockfile, + folder_path: *bun.AbsPath(.{ .sep = .auto }), folder_name: string, resolution: *const Resolution, ) !?Package.Scripts.List { - var path_buf: [bun.MAX_PATH_BYTES * 2]u8 = undefined; if (this.hasAny()) { const add_node_gyp_rebuild_script = if (lockfile.hasTrustedDependency(folder_name) and this.install.isEmpty() and this.preinstall.isEmpty()) brk: { - const binding_dot_gyp_path = Path.joinAbsStringZ( - abs_node_modules_path, - &[_]string{ folder_name, "binding.gyp" }, - .auto, - ); + var save = folder_path.save(); + defer save.restore(); + folder_path.append("binding.gyp"); - break :brk bun.sys.exists(binding_dot_gyp_path); + break :brk bun.sys.exists(folder_path.slice()); } else false; - const cwd = Path.joinAbsStringBufZTrailingSlash( - abs_node_modules_path, - &path_buf, - &[_]string{folder_name}, - .auto, - ); - return this.createList( lockfile, lockfile.buffers.string_bytes.items, - cwd, + folder_path, folder_name, resolution.tag, add_node_gyp_rebuild_script, ); } else if (!this.filled) { - const abs_folder_path = Path.joinAbsStringBufZTrailingSlash( - abs_node_modules_path, - &path_buf, - &[_]string{folder_name}, - .auto, - ); return this.createFromPackageJSON( log, lockfile, - node_modules, - abs_folder_path, + folder_path, folder_name, resolution.tag, ); @@ -335,14 +306,16 @@ pub const Scripts = extern struct { allocator: std.mem.Allocator, string_builder: *Lockfile.StringBuilder, log: *logger.Log, - node_modules: *PackageManager.PackageInstaller.LazyPackageDestinationDir, - folder_name: string, + folder_path: *bun.AbsPath(.{ .sep = .auto }), ) !void { const json = brk: { + var save = folder_path.save(); + defer save.restore(); + folder_path.append("package.json"); + const json_src = brk2: { - const json_path = bun.path.joinZ([_]string{ folder_name, "package.json" }, .auto); - const buf = try bun.sys.File.readFrom(try node_modules.getDir(), json_path, allocator).unwrap(); - break :brk2 logger.Source.initPathString(json_path, buf); + const buf = try bun.sys.File.readFrom(bun.FD.cwd(), folder_path.sliceZ(), allocator).unwrap(); + break :brk2 logger.Source.initPathString(folder_path.slice(), buf); }; initializeStore(); @@ -362,9 +335,8 @@ pub const Scripts = extern struct { pub fn createFromPackageJSON( this: *Package.Scripts, log: *logger.Log, - lockfile: *Lockfile, - node_modules: *PackageManager.PackageInstaller.LazyPackageDestinationDir, - abs_folder_path: string, + lockfile: *const Lockfile, + folder_path: *bun.AbsPath(.{ .sep = .auto }), folder_name: string, resolution_tag: Resolution.Tag, ) !?Package.Scripts.List { @@ -372,22 +344,20 @@ pub const Scripts = extern struct { tmp.initEmpty(lockfile.allocator); defer tmp.deinit(); var builder = tmp.stringBuilder(); - try this.fillFromPackageJSON(lockfile.allocator, &builder, log, node_modules, folder_name); + try this.fillFromPackageJSON(lockfile.allocator, &builder, log, folder_path); const add_node_gyp_rebuild_script = if (this.install.isEmpty() and this.preinstall.isEmpty()) brk: { - const binding_dot_gyp_path = Path.joinAbsStringZ( - abs_folder_path, - &[_]string{"binding.gyp"}, - .auto, - ); + const save = folder_path.save(); + defer save.restore(); + folder_path.append("binding.gyp"); - break :brk bun.sys.exists(binding_dot_gyp_path); + break :brk bun.sys.exists(folder_path.slice()); } else false; return this.createList( lockfile, tmp.buffers.string_bytes.items, - abs_folder_path, + folder_path, folder_name, resolution_tag, add_node_gyp_rebuild_script, @@ -402,8 +372,6 @@ const JSAst = bun.JSAst; const JSON = bun.JSON; const Lockfile = install.Lockfile; const Output = bun.Output; -const PackageManager = install.PackageManager; -const Path = bun.path; const Resolution = bun.install.Resolution; const Semver = bun.Semver; const String = Semver.String; @@ -419,3 +387,4 @@ const stringZ = [:0]const u8; const strings = bun.strings; const Package = Lockfile.Package; const debug = Output.scoped(.Lockfile, true); +const FD = bun.FD; diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index f20ff9b013..46da478468 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -244,7 +244,6 @@ pub fn Builder(comptime method: BuilderMethod) type { sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{}, install_root_dependencies: if (method == .filter) bool else void, - path_buf: []u8, pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; @@ -316,13 +315,120 @@ pub fn Builder(comptime method: BuilderMethod) type { }; } +pub fn isFilteredDependencyOrWorkspace( + dep_id: DependencyID, + parent_pkg_id: PackageID, + workspace_filters: []const WorkspaceFilter, + install_root_dependencies: bool, + manager: *const PackageManager, + lockfile: *const Lockfile, +) bool { + const pkg_id = lockfile.buffers.resolutions.items[dep_id]; + if (pkg_id >= lockfile.packages.len) { + return true; + } + + const pkgs = lockfile.packages.slice(); + const pkg_names = pkgs.items(.name); + const pkg_metas = pkgs.items(.meta); + const pkg_resolutions = pkgs.items(.resolution); + + const dep = lockfile.buffers.dependencies.items[dep_id]; + const res = &pkg_resolutions[pkg_id]; + const parent_res = &pkg_resolutions[parent_pkg_id]; + + if (pkg_metas[pkg_id].isDisabled()) { + if (manager.options.log_level.isVerbose()) { + const meta = &pkg_metas[pkg_id]; + const name = lockfile.str(&pkg_names[pkg_id]); + if (!meta.os.isMatch() and !meta.arch.isMatch()) { + Output.prettyErrorln("Skip installing {s} - cpu & os mismatch", .{name}); + } else if (!meta.os.isMatch()) { + Output.prettyErrorln("Skip installing {s} - os mismatch", .{name}); + } else if (!meta.arch.isMatch()) { + Output.prettyErrorln("Skip installing {s} - cpu mismatch", .{name}); + } + } + return true; + } + + if (dep.behavior.isBundled()) { + return true; + } + + const dep_features = switch (parent_res.tag) { + .root, .workspace, .folder => manager.options.local_package_features, + else => manager.options.remote_package_features, + }; + + if (!dep.behavior.isEnabled(dep_features)) { + return true; + } + + // Filtering only applies to the root package dependencies. Also + // --filter has a different meaning if a new package is being installed. + if (manager.subcommand != .install or parent_pkg_id != 0) { + return false; + } + + if (!dep.behavior.isWorkspaceOnly()) { + if (!install_root_dependencies) { + return true; + } + + return false; + } + + var workspace_matched = workspace_filters.len == 0; + + for (workspace_filters) |filter| { + var filter_path: bun.AbsPath(.{ .sep = .posix }) = .initTopLevelDir(); + defer filter_path.deinit(); + + const pattern, const name_or_path = switch (filter) { + .all => { + workspace_matched = true; + continue; + }, + .name => |name_pattern| .{ + name_pattern, + pkg_names[pkg_id].slice(lockfile.buffers.string_bytes.items), + }, + .path => |path_pattern| path_pattern: { + if (res.tag != .workspace) { + return false; + } + + filter_path.join(&.{res.value.workspace.slice(lockfile.buffers.string_bytes.items)}); + + break :path_pattern .{ path_pattern, filter_path.slice() }; + }, + }; + + switch (bun.glob.match(undefined, pattern, name_or_path)) { + .match, .negate_match => workspace_matched = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + workspace_matched = false; + break; + }, + + .no_match => { + // keep looking + }, + } + } + + return !workspace_matched; +} + pub fn processSubtree( this: *const Tree, dependency_id: DependencyID, hoist_root_id: Tree.Id, comptime method: BuilderMethod, builder: *Builder(method), - log_level: if (method == .filter) PackageManager.Options.LogLevel else void, ) SubtreeError!void { const parent_pkg_id = switch (dependency_id) { root_dep_id => 0, @@ -350,8 +456,6 @@ pub fn processSubtree( const pkgs = builder.lockfile.packages.slice(); const pkg_resolutions = pkgs.items(.resolution); - const pkg_metas = pkgs.items(.meta); - const pkg_names = pkgs.items(.name); builder.sort_buf.clearRetainingCapacity(); try builder.sort_buf.ensureUnusedCapacity(builder.allocator, resolution_list.len); @@ -360,31 +464,13 @@ pub fn processSubtree( builder.sort_buf.appendAssumeCapacity(@intCast(dep_id)); } - const DepSorter = struct { - lockfile: *const Lockfile, - - pub fn isLessThan(sorter: @This(), l: DependencyID, r: DependencyID) bool { - const deps_buf = sorter.lockfile.buffers.dependencies.items; - const string_buf = sorter.lockfile.buffers.string_bytes.items; - - const l_dep = deps_buf[l]; - const r_dep = deps_buf[r]; - - return switch (l_dep.behavior.cmp(r_dep.behavior)) { - .lt => true, - .gt => false, - .eq => strings.order(l_dep.name.slice(string_buf), r_dep.name.slice(string_buf)) == .lt, - }; - } - }; - std.sort.pdq( DependencyID, builder.sort_buf.items, - DepSorter{ + Lockfile.DepSorter{ .lockfile = builder.lockfile, }, - DepSorter.isLessThan, + Lockfile.DepSorter.isLessThan, ); for (builder.sort_buf.items) |dep_id| { @@ -394,101 +480,16 @@ pub fn processSubtree( // filter out disabled dependencies if (comptime method == .filter) { - if (builder.lockfile.isResolvedDependencyDisabled( + if (isFilteredDependencyOrWorkspace( dep_id, - switch (pkg_resolutions[parent_pkg_id].tag) { - .root, .workspace, .folder => builder.manager.options.local_package_features, - else => builder.manager.options.remote_package_features, - }, - &pkg_metas[pkg_id], + parent_pkg_id, + builder.workspace_filters, + builder.install_root_dependencies, + builder.manager, + builder.lockfile, )) { - if (log_level.isVerbose()) { - const meta = &pkg_metas[pkg_id]; - const name = builder.lockfile.str(&pkg_names[pkg_id]); - if (!meta.os.isMatch() and !meta.arch.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' cpu & os mismatch", .{name}); - } else if (!meta.os.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' os mismatch", .{name}); - } else if (!meta.arch.isMatch()) { - Output.prettyErrorln("Skip installing '{s}' cpu mismatch", .{name}); - } - } - continue; } - - if (builder.manager.subcommand == .install) dont_skip: { - // only do this when parent is root. workspaces are always dependencies of the root - // package, and the root package is always called with `processSubtree` - if (parent_pkg_id == 0 and builder.workspace_filters.len > 0) { - if (!builder.dependencies[dep_id].behavior.isWorkspaceOnly()) { - if (builder.install_root_dependencies) { - break :dont_skip; - } - - continue; - } - - var match = false; - - for (builder.workspace_filters) |workspace_filter| { - const res_id = builder.resolutions[dep_id]; - - const pattern, const path_or_name = switch (workspace_filter) { - .name => |pattern| .{ pattern, pkg_names[res_id].slice(builder.buf()) }, - - .path => |pattern| path: { - const res = &pkg_resolutions[res_id]; - if (res.tag != .workspace) { - break :dont_skip; - } - const res_path = res.value.workspace.slice(builder.buf()); - - // occupy `builder.path_buf` - var abs_res_path = strings.withoutTrailingSlash(bun.path.joinAbsStringBuf( - FileSystem.instance.top_level_dir, - builder.path_buf, - &.{res_path}, - .auto, - )); - - if (comptime Environment.isWindows) { - abs_res_path = abs_res_path[Path.windowsVolumeNameLen(abs_res_path)[0]..]; - Path.dangerouslyConvertPathToPosixInPlace(u8, builder.path_buf[0..abs_res_path.len]); - } - - break :path .{ - pattern, - abs_res_path, - }; - }, - - .all => { - match = true; - continue; - }, - }; - - switch (bun.glob.walk.matchImpl(builder.allocator, pattern, path_or_name)) { - .match, .negate_match => match = true, - - .negate_no_match => { - // always skip if a pattern specifically says "!" - match = false; - break; - }, - - .no_match => { - // keep current - }, - } - } - - if (!match) { - continue; - } - } - } } const hoisted: HoistDependencyResult = hoisted: { @@ -646,7 +647,6 @@ const DependencyID = install.DependencyID; const DependencyIDList = Lockfile.DependencyIDList; const Environment = bun.Environment; const ExternalSlice = Lockfile.ExternalSlice; -const FileSystem = bun.fs.FileSystem; const Lockfile = install.Lockfile; const OOM = bun.OOM; const Output = bun.Output; @@ -666,7 +666,6 @@ const invalid_package_id = install.invalid_package_id; const logger = bun.logger; const string = []const u8; const stringZ = bun.stringZ; -const strings = bun.strings; const z_allocator = bun.z_allocator; const bun = @import("bun"); diff --git a/src/install/lockfile/bun.lock.zig b/src/install/lockfile/bun.lock.zig index 7b782d5f69..db50bdf193 100644 --- a/src/install/lockfile/bun.lock.zig +++ b/src/install/lockfile/bun.lock.zig @@ -91,6 +91,14 @@ pub const Stringifier = struct { try writer.print("\"lockfileVersion\": {d},\n", .{@intFromEnum(Version.current)}); try writeIndent(writer, indent); + if (lockfile.node_linker != .auto) { + try writer.print( + \\"nodeLinker": "{s}", + \\ + , .{@tagName(lockfile.node_linker)}); + try writeIndent(writer, indent); + } + try writer.writeAll("\"workspaces\": {\n"); try incIndent(writer, indent); { @@ -1002,6 +1010,7 @@ const ParseError = OOM || error{ InvalidOverridesObject, InvalidCatalogObject, InvalidCatalogsObject, + InvalidNodeLinkerValue, InvalidDependencyName, InvalidDependencyVersion, InvalidPackageResolution, @@ -1344,7 +1353,7 @@ pub fn parseIntoBinaryLockfile( if (!key.isString() or key.data.e_string.len() == 0) { try log.addError(source, key.loc, "Expected a non-empty string"); - return error.InvalidCatalogObject; + return error.InvalidCatalogsObject; } const dep_name_str = key.asString(allocator).?; @@ -1353,7 +1362,7 @@ pub fn parseIntoBinaryLockfile( if (!value.isString()) { try log.addError(source, value.loc, "Expected a string"); - return error.InvalidCatalogObject; + return error.InvalidCatalogsObject; } const version_str = value.asString(allocator).?; @@ -1374,7 +1383,7 @@ pub fn parseIntoBinaryLockfile( manager, ) orelse { try log.addError(source, value.loc, "Invalid catalog version"); - return error.InvalidCatalogObject; + return error.InvalidCatalogsObject; }, }; @@ -1386,7 +1395,7 @@ pub fn parseIntoBinaryLockfile( if (entry.found_existing) { try log.addError(source, key.loc, "Duplicate catalog entry"); - return error.InvalidCatalogObject; + return error.InvalidCatalogsObject; } entry.value_ptr.* = dep; @@ -1394,6 +1403,21 @@ pub fn parseIntoBinaryLockfile( } } + if (root.get("nodeLinker")) |node_linker_expr| { + if (!node_linker_expr.isString()) { + try log.addError(source, node_linker_expr.loc, "Expected a string"); + return error.InvalidNodeLinkerValue; + } + + const node_linker_str = node_linker_expr.data.e_string.slice(allocator); + lockfile.node_linker = BinaryLockfile.NodeLinker.fromStr(node_linker_str) orelse { + try log.addError(source, node_linker_expr.loc, "Expected one of \"isolated\" or \"hoisted\""); + return error.InvalidNodeLinkerValue; + }; + } else { + lockfile.node_linker = .auto; + } + const workspaces_obj = root.getObject("workspaces") orelse { try log.addError(source, root.loc, "Missing a workspaces object property"); return error.InvalidWorkspaceObject; diff --git a/src/install/lockfile/bun.lockb.zig b/src/install/lockfile/bun.lockb.zig index 8f546724f4..29f46c22eb 100644 --- a/src/install/lockfile/bun.lockb.zig +++ b/src/install/lockfile/bun.lockb.zig @@ -7,6 +7,7 @@ const has_trusted_dependencies_tag: u64 = @bitCast(@as([8]u8, "tRuStEDd".*)); const has_empty_trusted_dependencies_tag: u64 = @bitCast(@as([8]u8, "eMpTrUsT".*)); const has_overrides_tag: u64 = @bitCast(@as([8]u8, "oVeRriDs".*)); const has_catalogs_tag: u64 = @bitCast(@as([8]u8, "cAtAlOgS".*)); +const has_node_linker_tag: u64 = @bitCast(@as([8]u8, "nOdLiNkR".*)); pub fn save(this: *Lockfile, verbose_log: bool, bytes: *std.ArrayList(u8), total_size: *usize, end_pos: *usize) !void { @@ -244,6 +245,11 @@ pub fn save(this: *Lockfile, verbose_log: bool, bytes: *std.ArrayList(u8), total } } + if (this.node_linker != .auto) { + try writer.writeAll(std.mem.asBytes(&has_node_linker_tag)); + try writer.writeInt(u8, @intFromEnum(this.node_linker), .little); + } + total_size.* = try stream.getPos(); try writer.writeAll(&alignment_bytes_to_repeat_buffer); @@ -520,6 +526,21 @@ pub fn load( } } + { + lockfile.node_linker = .auto; + + const remaining_in_buffer = total_buffer_size -| stream.pos; + + if (remaining_in_buffer > 8 and total_buffer_size <= stream.buffer.len) { + const next_num = try reader.readInt(u64, .little); + if (next_num == has_node_linker_tag) { + lockfile.node_linker = try reader.readEnum(Lockfile.NodeLinker, .little); + } else { + stream.pos -= 8; + } + } + } + lockfile.scratch = Lockfile.Scratch.init(allocator); lockfile.package_index = PackageIndex.Map.initContext(allocator, .{}); lockfile.string_pool = StringPool.init(allocator); diff --git a/src/install/patch_install.zig b/src/install/patch_install.zig index 20463990ae..7723582ab6 100644 --- a/src/install/patch_install.zig +++ b/src/install/patch_install.zig @@ -84,7 +84,7 @@ pub const PatchTask = struct { cache_dir_subpath_without_patch_hash: stringZ, /// this is non-null if this was called before a Task, for example extracting - task_id: ?Task.Id.Type = null, + task_id: ?Task.Id = null, install_context: ?struct { dependency_id: DependencyID, tree_id: Lockfile.Tree.Id, @@ -324,7 +324,7 @@ pub const PatchTask = struct { .cache_dir_subpath = this.callback.apply.cache_dir_subpath_without_patch_hash, .destination_dir_subpath = tempdir_name, .destination_dir_subpath_buf = tmpname_buf[0..], - .patch = .{}, + .patch = null, .progress = null, .package_name = pkg_name, .package_version = resolution_label, diff --git a/src/install/repository.zig b/src/install/repository.zig index a41a81af60..09f8953c76 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -293,11 +293,52 @@ pub const Repository = extern struct { return lhs.resolved.eql(rhs.resolved, lhs_buf, rhs_buf); } - pub fn formatAs(this: *const Repository, label: string, buf: []const u8, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + pub fn formatAs(this: *const Repository, label: string, buf: []const u8, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { const formatter = Formatter{ .label = label, .repository = this, .buf = buf }; return try formatter.format(layout, opts, writer); } + pub fn fmtStorePath(this: *const Repository, label: string, string_buf: string) StorePathFormatter { + return .{ + .repo = this, + .label = label, + .string_buf = string_buf, + }; + } + + pub const StorePathFormatter = struct { + repo: *const Repository, + label: string, + string_buf: string, + + pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { + try writer.print("{}", .{Install.fmtStorePath(this.label)}); + + if (!this.repo.owner.isEmpty()) { + try writer.print("{}", .{this.repo.owner.fmtStorePath(this.string_buf)}); + // try writer.writeByte(if (this.opts.replace_slashes) '+' else '/'); + try writer.writeByte('+'); + } else if (Dependency.isSCPLikePath(this.repo.repo.slice(this.string_buf))) { + // try writer.print("ssh:{s}", .{if (this.opts.replace_slashes) "++" else "//"}); + try writer.writeAll("ssh:++"); + } + + try writer.print("{}", .{this.repo.repo.fmtStorePath(this.string_buf)}); + + if (!this.repo.resolved.isEmpty()) { + try writer.writeByte('+'); // this would be '#' but it's not valid on windows + var resolved = this.repo.resolved.slice(this.string_buf); + if (strings.lastIndexOfChar(resolved, '-')) |i| { + resolved = resolved[i + 1 ..]; + } + try writer.print("{}", .{Install.fmtStorePath(resolved)}); + } else if (!this.repo.committish.isEmpty()) { + try writer.writeByte('+'); // this would be '#' but it's not valid on windows + try writer.print("{}", .{this.repo.committish.fmtStorePath(this.string_buf)}); + } + } + }; + pub fn fmt(this: *const Repository, label: string, buf: []const u8) Formatter { return .{ .repository = this, @@ -310,7 +351,7 @@ pub const Repository = extern struct { label: []const u8 = "", buf: []const u8, repository: *const Repository, - pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { if (comptime Environment.allow_assert) bun.assert(formatter.label.len > 0); try writer.writeAll(formatter.label); @@ -458,14 +499,14 @@ pub const Repository = extern struct { env: DotEnv.Map, log: *logger.Log, cache_dir: std.fs.Dir, - task_id: u64, + task_id: Install.Task.Id, name: string, url: string, attempt: u8, ) !std.fs.Dir { bun.Analytics.Features.git_dependencies += 1; const folder_name = try std.fmt.bufPrintZ(&folder_name_buf, "{any}.git", .{ - bun.fmt.hexIntLower(task_id), + bun.fmt.hexIntLower(task_id.get()), }); return if (cache_dir.openDirZ(folder_name, .{})) |dir| fetch: { @@ -523,10 +564,10 @@ pub const Repository = extern struct { repo_dir: std.fs.Dir, name: string, committish: string, - task_id: u64, + task_id: Install.Task.Id, ) !string { const path = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{try std.fmt.bufPrint(&folder_name_buf, "{any}.git", .{ - bun.fmt.hexIntLower(task_id), + bun.fmt.hexIntLower(task_id.get()), })}, .auto); _ = repo_dir; diff --git a/src/install/resolution.zig b/src/install/resolution.zig index 50fe0d936e..b30775c6aa 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -189,6 +189,37 @@ pub const Resolution = extern struct { }; } + const StorePathFormatter = struct { + res: *const Resolution, + string_buf: string, + // opts: String.StorePathFormatter.Options, + + pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { + const string_buf = this.string_buf; + const res = this.res.value; + switch (this.res.tag) { + .root => try writer.writeAll("root"), + .npm => try writer.print("{}", .{res.npm.version.fmt(string_buf)}), + .local_tarball => try writer.print("{}", .{res.local_tarball.fmtStorePath(string_buf)}), + .remote_tarball => try writer.print("{}", .{res.remote_tarball.fmtStorePath(string_buf)}), + .folder => try writer.print("{}", .{res.folder.fmtStorePath(string_buf)}), + .git => try writer.print("{}", .{res.git.fmtStorePath("git+", string_buf)}), + .github => try writer.print("{}", .{res.github.fmtStorePath("github+", string_buf)}), + .workspace => try writer.print("{}", .{res.workspace.fmtStorePath(string_buf)}), + .symlink => try writer.print("{}", .{res.symlink.fmtStorePath(string_buf)}), + .single_file_module => try writer.print("{}", .{res.single_file_module.fmtStorePath(string_buf)}), + else => {}, + } + } + }; + + pub fn fmtStorePath(this: *const Resolution, string_buf: string) StorePathFormatter { + return .{ + .res = this, + .string_buf = string_buf, + }; + } + pub fn fmtURL(this: *const Resolution, string_bytes: []const u8) URLFormatter { return URLFormatter{ .resolution = this, .buf = string_bytes }; } @@ -257,7 +288,7 @@ pub const Resolution = extern struct { buf: []const u8, - pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { const buf = formatter.buf; const value = formatter.resolution.value; switch (formatter.resolution.tag) { @@ -280,7 +311,7 @@ pub const Resolution = extern struct { buf: []const u8, path_sep: bun.fmt.PathFormatOptions.Sep, - pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { const buf = formatter.buf; const value = formatter.resolution.value; switch (formatter.resolution.tag) { diff --git a/src/macho.zig b/src/macho.zig index 9ada9746e6..051a0628b8 100644 --- a/src/macho.zig +++ b/src/macho.zig @@ -169,7 +169,7 @@ pub const MachoFile = struct { // We need to shift [...data after __BUN] forward by size_diff bytes. const after_bun_slice = self.data.items[original_data_end + @as(usize, @intCast(size_diff)) ..]; const prev_after_bun_slice = prev_data_slice[original_segsize..]; - bun.move(after_bun_slice, prev_after_bun_slice); + bun.memmove(after_bun_slice, prev_after_bun_slice); // Now we copy the u32 size header std.mem.writeInt(u32, self.data.items[original_fileoff..][0..4], @intCast(data.len), .little); diff --git a/src/multi_array_list.zig b/src/multi_array_list.zig index b544d3043d..a9a4276490 100644 --- a/src/multi_array_list.zig +++ b/src/multi_array_list.zig @@ -184,9 +184,9 @@ pub fn MultiArrayList(comptime T: type) type { }; /// Release all allocated memory. - pub fn deinit(self: *Self, gpa: Allocator) void { + pub fn deinit(self: *const Self, gpa: Allocator) void { gpa.free(self.allocatedBytes()); - self.* = undefined; + @constCast(self).* = undefined; } /// The caller owns the returned memory. Empties this MultiArrayList. diff --git a/src/paths.zig b/src/paths.zig new file mode 100644 index 0000000000..dd9aea3a0f --- /dev/null +++ b/src/paths.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const bun = @import("bun"); +const Environment = bun.Environment; + +const paths = @import("./paths/Path.zig"); +pub const Path = paths.Path; +pub const AbsPath = paths.AbsPath; +pub const RelPath = paths.RelPath; + +pub const EnvPath = @import("./paths/EnvPath.zig").EnvPath; + +const pools = @import("./paths/path_buffer_pool.zig"); +pub const path_buffer_pool = pools.path_buffer_pool; +pub const w_path_buffer_pool = pools.w_path_buffer_pool; +pub const os_path_buffer_pool = pools.os_path_buffer_pool; + +pub const MAX_PATH_BYTES: usize = if (Environment.isWasm) 1024 else std.fs.max_path_bytes; +pub const PathBuffer = [MAX_PATH_BYTES]u8; +pub const PATH_MAX_WIDE = std.os.windows.PATH_MAX_WIDE; +pub const WPathBuffer = [PATH_MAX_WIDE]u16; +pub const OSPathChar = if (Environment.isWindows) u16 else u8; +pub const OSPathSliceZ = [:0]const OSPathChar; +pub const OSPathSlice = []const OSPathChar; +pub const OSPathBuffer = if (Environment.isWindows) WPathBuffer else PathBuffer; diff --git a/src/paths/EnvPath.zig b/src/paths/EnvPath.zig new file mode 100644 index 0000000000..63e86bef49 --- /dev/null +++ b/src/paths/EnvPath.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const bun = @import("bun"); +const AbsPath = bun.AbsPath; +const string = bun.string; +const strings = bun.strings; +const OOM = bun.OOM; + +pub const EnvPathOptions = struct { + // +}; + +fn trimPathDelimiters(input: string) string { + var trimmed = input; + while (trimmed.len > 0 and trimmed[0] == std.fs.path.delimiter) { + trimmed = trimmed[1..]; + } + while (trimmed.len > 0 and trimmed[trimmed.len - 1] == std.fs.path.delimiter) { + trimmed = trimmed[0 .. trimmed.len - 1]; + } + return trimmed; +} + +pub fn EnvPath(comptime opts: EnvPathOptions) type { + return struct { + allocator: std.mem.Allocator, + buf: std.ArrayListUnmanaged(u8) = .empty, + + pub fn init(allocator: std.mem.Allocator) @This() { + return .{ .allocator = allocator }; + } + + pub fn initCapacity(allocator: std.mem.Allocator, capacity: usize) OOM!@This() { + return .{ .allocator = allocator, .buf = try .initCapacity(allocator, capacity) }; + } + + pub fn deinit(this: *const @This()) void { + @constCast(this).buf.deinit(this.allocator); + } + + pub fn slice(this: *const @This()) string { + return this.buf.items; + } + + pub fn append(this: *@This(), input: anytype) OOM!void { + const trimmed: string = switch (@TypeOf(input)) { + []u8, []const u8 => strings.withoutTrailingSlash(trimPathDelimiters(input)), + + // assume already trimmed + else => input.slice(), + }; + + if (trimmed.len == 0) { + return; + } + + if (this.buf.items.len != 0) { + try this.buf.ensureUnusedCapacity(this.allocator, trimmed.len + 1); + this.buf.appendAssumeCapacity(std.fs.path.delimiter); + this.buf.appendSliceAssumeCapacity(trimmed); + } else { + try this.buf.appendSlice(this.allocator, trimmed); + } + } + + pub const PathComponentBuilder = struct { + env_path: *EnvPath(opts), + path_buf: AbsPath(.{ .sep = .auto }), + + pub fn append(this: *@This(), component: string) void { + this.path_buf.append(component); + } + + pub fn appendFmt(this: *@This(), comptime component_fmt: string, component_args: anytype) void { + this.path_buf.appendFmt(component_fmt, component_args); + } + + pub fn apply(this: *@This()) OOM!void { + try this.env_path.append(&this.path_buf); + this.path_buf.deinit(); + } + }; + + pub fn pathComponentBuilder(this: *@This()) PathComponentBuilder { + return .{ + .env_path = this, + .path_buf = .init(), + }; + } + }; +} diff --git a/src/paths/Path.zig b/src/paths/Path.zig new file mode 100644 index 0000000000..61339c44d4 --- /dev/null +++ b/src/paths/Path.zig @@ -0,0 +1,808 @@ +const std = @import("std"); +const bun = @import("bun"); +const Output = bun.Output; +const PathBuffer = bun.PathBuffer; +const WPathBuffer = bun.WPathBuffer; +const Environment = bun.Environment; +const FD = bun.FD; + +const Options = struct { + check_length: CheckLength = .assume_always_less_than_max_path, + sep: PathSeparators = .any, + kind: Kind = .any, + buf_type: BufType = .pool, + unit: Unit = .u8, + + const Unit = enum { + u8, + u16, + os, + }; + + const BufType = enum { + pool, + // stack, + // array_list, + }; + + const Kind = enum { + abs, + rel, + + // not recommended, but useful when you don't know + any, + }; + + const CheckLength = enum { + assume_always_less_than_max_path, + check_for_greater_than_max_path, + }; + + const PathSeparators = enum { + any, + auto, + posix, + windows, + + pub fn char(comptime sep: @This()) u8 { + return switch (sep) { + .any => @compileError("use the existing slash"), + .auto => std.fs.path.sep, + .posix => std.fs.path.sep_posix, + .windows => std.fs.path.sep_windows, + }; + } + }; + + pub fn pathUnit(comptime opts: @This()) type { + return switch (opts.unit) { + .u8 => u8, + .u16 => u16, + .os => if (Environment.isWindows) u16 else u8, + }; + } + + pub fn notPathUnit(comptime opts: @This()) type { + return switch (opts.unit) { + .u8 => u16, + .u16 => u8, + .os => if (Environment.isWindows) u8 else u16, + }; + } + + pub fn maxPathLength(comptime opts: @This()) usize { + switch (comptime opts.check_length) { + .assume_always_less_than_max_path => @compileError("max path length is not needed"), + .check_for_greater_than_max_path => { + return switch (comptime opts.unit) { + .u8 => bun.MAX_PATH_BYTES, + .u16 => bun.PATH_MAX_WIDE, + .os => if (Environment.isWindows) bun.PATH_MAX_WIDE else bun.MAX_PATH_BYTES, + }; + }, + } + } + + pub fn Buf(comptime opts: @This()) type { + return switch (opts.buf_type) { + .pool => struct { + pooled: switch (opts.unit) { + .u8 => *PathBuffer, + .u16 => *WPathBuffer, + .os => if (Environment.isWindows) *WPathBuffer else *PathBuffer, + }, + len: usize, + + pub fn setLength(this: *@This(), new_len: usize) void { + this.len = new_len; + } + + pub fn append(this: *@This(), characters: anytype, add_separator: bool) void { + if (add_separator) { + switch (comptime opts.sep) { + .any, .auto => this.pooled[this.len] = std.fs.path.sep, + .posix => this.pooled[this.len] = std.fs.path.sep_posix, + .windows => this.pooled[this.len] = std.fs.path.sep_windows, + } + this.len += 1; + } + + if (opts.inputChildType(@TypeOf(characters)) == opts.pathUnit()) { + switch (comptime opts.sep) { + .any => { + @memcpy(this.pooled[this.len..][0..characters.len], characters); + this.len += characters.len; + }, + .auto, .posix, .windows => { + for (characters) |c| { + switch (c) { + '/', '\\' => this.pooled[this.len] = opts.sep.char(), + else => this.pooled[this.len] = c, + } + this.len += 1; + } + }, + } + } else { + switch (opts.inputChildType(@TypeOf(characters))) { + u8 => { + const converted = bun.strings.convertUTF8toUTF16InBuffer(this.pooled[this.len..], characters); + if (comptime opts.sep != .any) { + for (this.pooled[this.len..][0..converted.len], 0..) |c, off| { + switch (c) { + '/', '\\' => this.pooled[this.len + off] = opts.sep.char(), + else => {}, + } + } + } + this.len += converted.len; + }, + u16 => { + const converted = bun.strings.convertUTF16toUTF8InBuffer(this.pooled[this.len..], characters) catch unreachable; + if (comptime opts.sep != .any) { + for (this.pooled[this.len..][0..converted.len], 0..) |c, off| { + switch (c) { + '/', '\\' => this.pooled[this.len + off] = opts.sep.char(), + else => {}, + } + } + } + this.len += converted.len; + }, + else => @compileError("unexpected character type"), + } + } + + // switch (@TypeOf(characters)) { + // []u8, []const u8, [:0]u8, [:0]const u8 => { + // if (opts.unit == .u8) { + // this.appendT() + // } + // } + // } + } + + // fn append(this: *@This(), characters: []const opts.pathUnit(), add_separator: bool) void { + // if (add_separator) {} + // switch (comptime opts.sep) { + // .any => { + // @memcpy(this.pooled[this.len..][0..characters.len], characters); + // this.len += characters.len; + // }, + // .auto, .posix, .windows => { + // for (characters) |c| { + // switch (c) { + // '/', '\\' => this.pooled[this.len] = opts.sep.char(), + // else => this.pooled[this.len] = c, + // } + // this.len += 1; + // } + // }, + // } + // } + + fn convertAppend(this: *@This(), characters: []const opts.notPathUnit()) void { + _ = this; + _ = characters; + // switch (comptime opts.sep) { + // .any => { + // switch (opts.notPathUnit()) { + // .u8 => { + // const converted = bun.strings.convertUTF8toUTF16InBuffer(this.pooled[this.len..], characters); + // }, + // } + // }, + // } + } + }, + // .stack => struct { + // buf: PathBuffer, + // len: u16, + // }, + // .array_list => struct { + // list: std.ArrayList(opts.pathUnit()), + // }, + + }; + } + + const Error = error{MaxPathExceeded}; + + pub fn ResultFn(comptime opts: @This()) fn (comptime T: type) type { + return struct { + pub fn Result(comptime T: type) type { + return switch (opts.check_length) { + .assume_always_less_than_max_path => T, + .check_for_greater_than_max_path => Error!T, + }; + } + }.Result; + } + + pub fn inputChildType(comptime opts: @This(), comptime InputType: type) type { + _ = opts; + return switch (@typeInfo(std.meta.Child(InputType))) { + // handle string literals + .array => |array| array.child, + else => std.meta.Child(InputType), + }; + } +}; + +pub fn AbsPath(comptime opts: Options) type { + var copy = opts; + copy.kind = .abs; + return Path(copy); +} + +pub fn RelPath(comptime opts: Options) type { + var copy = opts; + copy.kind = .rel; + return Path(copy); +} + +pub fn Path(comptime opts: Options) type { + const Result = opts.ResultFn(); + + // if (opts.unit == .u16 and !Environment.isWindows) { + // @compileError("utf16 not supported"); + // } + + // const log = Output.scoped(.Path, false); + + return struct { + _buf: opts.Buf(), + + pub fn init() @This() { + switch (comptime opts.buf_type) { + .pool => { + return .{ + ._buf = .{ + .pooled = switch (opts.unit) { + .u8 => bun.path_buffer_pool.get(), + .u16 => bun.w_path_buffer_pool.get(), + .os => if (comptime Environment.isWindows) + bun.w_path_buffer_pool.get() + else + bun.path_buffer_pool.get(), + }, + .len = 0, + }, + }; + }, + } + } + + pub fn deinit(this: *const @This()) void { + switch (comptime opts.buf_type) { + .pool => { + switch (opts.unit) { + .u8 => bun.path_buffer_pool.put(this._buf.pooled), + .u16 => bun.w_path_buffer_pool.put(this._buf.pooled), + .os => if (comptime Environment.isWindows) + bun.w_path_buffer_pool.put(this._buf.pooled) + else + bun.path_buffer_pool.put(this._buf.pooled), + } + }, + } + @constCast(this).* = undefined; + } + + pub fn move(this: *const @This()) @This() { + const moved = this.*; + @constCast(this).* = undefined; + return moved; + } + + pub fn initTopLevelDir() @This() { + bun.debugAssert(bun.fs.FileSystem.instance_loaded); + const top_level_dir = bun.fs.FileSystem.instance.top_level_dir; + + const trimmed = switch (comptime opts.kind) { + .abs => trimmed: { + bun.debugAssert(isInputAbsolute(top_level_dir)); + break :trimmed trimInput(.abs, top_level_dir); + }, + .rel => @compileError("cannot create a relative path from top_level_dir"), + .any => trimInput(.abs, top_level_dir), + }; + + var this = init(); + this._buf.append(trimmed, false); + return this; + } + + pub fn initFdPath(fd: FD) !@This() { + switch (comptime opts.kind) { + .abs => {}, + .rel => @compileError("cannot create a relative path from getFdPath"), + .any => {}, + } + + var this = init(); + switch (comptime opts.buf_type) { + .pool => { + const raw = try fd.getFdPath(this._buf.pooled); + const trimmed = trimInput(.abs, raw); + this._buf.len = trimmed.len; + }, + } + + return this; + } + + pub fn from(input: anytype) Result(@This()) { + switch (comptime @TypeOf(input)) { + []u8, []const u8, [:0]u8, [:0]const u8 => {}, + []u16, []const u16, [:0]u16, [:0]const u16 => {}, + else => @compileError("unsupported type: " ++ @typeName(@TypeOf(input))), + } + const trimmed = switch (comptime opts.kind) { + .abs => trimmed: { + bun.debugAssert(isInputAbsolute(input)); + break :trimmed trimInput(.abs, input); + }, + .rel => trimmed: { + bun.debugAssert(!isInputAbsolute(input)); + break :trimmed trimInput(.rel, input); + }, + .any => trimInput(if (isInputAbsolute(input)) .abs else .rel, input), + }; + + if (comptime opts.check_length == .check_for_greater_than_max_path) { + if (trimmed.len >= opts.maxPathLength()) { + return error.MaxPathExceeded; + } + } + + var this = init(); + this._buf.append(trimmed, false); + return this; + } + + pub fn isAbsolute(this: *const @This()) bool { + return switch (comptime opts.kind) { + .abs => @compileError("already known to be absolute"), + .rel => @compileError("already known to not be absolute"), + .any => isInputAbsolute(this.slice()), + }; + } + + pub fn basename(this: *const @This()) []const opts.pathUnit() { + return bun.strings.basename(opts.pathUnit(), this.slice()); + } + + pub fn basenameZ(this: *const @This()) [:0]const opts.pathUnit() { + const full = this.sliceZ(); + const base = bun.strings.basename(opts.pathUnit(), full); + return full[full.len - base.len ..][0..base.len :0]; + } + + pub fn dirname(this: *const @This()) ?[]const opts.pathUnit() { + return bun.Dirname.dirname(opts.pathUnit(), this.slice()); + } + + pub fn slice(this: *const @This()) []const opts.pathUnit() { + switch (comptime opts.buf_type) { + .pool => return this._buf.pooled[0..this._buf.len], + } + } + + pub fn sliceZ(this: *const @This()) [:0]const opts.pathUnit() { + switch (comptime opts.buf_type) { + .pool => { + this._buf.pooled[this._buf.len] = 0; + return this._buf.pooled[0..this._buf.len :0]; + }, + } + } + + // pub fn buf(this: *const @This()) []opts.pathUnit() { + // switch (comptime opts.buf_type) { + // .pool => { + // return this._buf.pooled; + // }, + // } + // } + + pub fn len(this: *const @This()) usize { + switch (comptime opts.buf_type) { + .pool => { + return this._buf.len; + }, + } + } + + pub fn clone(this: *const @This()) @This() { + switch (comptime opts.buf_type) { + .pool => { + var cloned = init(); + @memcpy(cloned._buf.pooled[0..this._buf.len], this._buf.pooled[0..this._buf.len]); + cloned._buf.len = this._buf.len; + return cloned; + }, + } + } + + pub fn clear(this: *@This()) void { + this._buf.setLength(0); + } + + pub fn rootLen(input: anytype) ?usize { + if (comptime Environment.isWindows) { + if (input.len > 2 and input[1] == ':' and switch (input[2]) { + '/', '\\' => true, + else => false, + }) { + const letter = input[0]; + if (('a' <= letter and letter <= 'z') or ('A' <= letter and letter <= 'Z')) { + // C:\ + return 3; + } + } + + if (input.len > 5 and + switch (input[0]) { + '/', '\\' => true, + else => false, + } and + switch (input[1]) { + '/', '\\' => true, + else => false, + } and + switch (input[2]) { + '\\', '.' => false, + else => true, + }) + { + var i: usize = 3; + // \\network\share\ + // ^ + while (i < input.len and switch (input[i]) { + '/', '\\' => false, + else => true, + }) { + i += 1; + } + + i += 1; + // \\network\share\ + // ^ + const start = i; + while (i < input.len and switch (input[i]) { + '/', '\\' => false, + else => true, + }) { + i += 1; + } + + if (start != i and i < input.len and switch (input[i]) { + '/', '\\' => true, + else => false, + }) { + // \\network\share\ + // ^ + if (i + 1 < input.len) { + return i + 1; + } + return i; + } + } + + if (input.len > 0 and switch (input[0]) { + '/', '\\' => true, + else => false, + }) { + // \ + return 1; + } + + return null; + } + + if (input.len > 0 and input[0] == '/') { + // / + return 1; + } + + return null; + } + + const TrimInputKind = enum { + abs, + rel, + }; + + fn trimInput(kind: TrimInputKind, input: anytype) []const opts.inputChildType(@TypeOf(input)) { + var trimmed: []const opts.inputChildType(@TypeOf(input)) = input[0..]; + + if (comptime Environment.isWindows) { + switch (kind) { + .abs => { + const root_len = rootLen(input) orelse 0; + while (trimmed.len > root_len and switch (trimmed[trimmed.len - 1]) { + '/', '\\' => true, + else => false, + }) { + trimmed = trimmed[0 .. trimmed.len - 1]; + } + }, + .rel => { + if (trimmed.len > 1 and trimmed[0] == '.') { + const c = trimmed[1]; + if (c == '/' or c == '\\') { + trimmed = trimmed[2..]; + } + } + while (trimmed.len > 0 and switch (trimmed[0]) { + '/', '\\' => true, + else => false, + }) { + trimmed = trimmed[1..]; + } + while (trimmed.len > 0 and switch (trimmed[trimmed.len - 1]) { + '/', '\\' => true, + else => false, + }) { + trimmed = trimmed[0 .. trimmed.len - 1]; + } + }, + } + + return trimmed; + } + + switch (kind) { + .abs => { + const root_len = rootLen(input) orelse 0; + while (trimmed.len > root_len and trimmed[trimmed.len - 1] == '/') { + trimmed = trimmed[0 .. trimmed.len - 1]; + } + }, + .rel => { + if (trimmed.len > 1 and trimmed[0] == '.' and trimmed[1] == '/') { + trimmed = trimmed[2..]; + } + while (trimmed.len > 0 and trimmed[0] == '/') { + trimmed = trimmed[1..]; + } + + while (trimmed.len > 0 and trimmed[trimmed.len - 1] == '/') { + trimmed = trimmed[0 .. trimmed.len - 1]; + } + }, + } + + return trimmed; + } + + fn isInputAbsolute(input: anytype) bool { + if (input.len == 0) { + return false; + } + + if (input[0] == '/') { + return true; + } + + if (comptime Environment.isWindows) { + if (input[0] == '\\') { + return true; + } + + if (input.len < 3) { + return false; + } + + if (input[1] == ':' and switch (input[2]) { + '/', '\\' => true, + else => false, + }) { + return true; + } + } + + return false; + } + + pub fn append(this: *@This(), input: anytype) Result(void) { + const needs_sep = this.len() > 0 and switch (comptime opts.sep) { + .any => switch (this.slice()[this.len() - 1]) { + '/', '\\' => false, + else => true, + }, + else => this.slice()[this.len() - 1] != opts.sep.char(), + }; + + switch (comptime opts.kind) { + .abs => { + const has_root = this.len() > 0; + + if (comptime Environment.isDebug) { + if (has_root) { + bun.debugAssert(!isInputAbsolute(input)); + } else { + bun.debugAssert(isInputAbsolute(input)); + } + } + + const trimmed = trimInput(if (has_root) .rel else .abs, input); + + if (trimmed.len == 0) { + return; + } + + if (comptime opts.check_length == .check_for_greater_than_max_path) { + if (this.len() + trimmed.len + @intFromBool(needs_sep) >= opts.maxPathLength()) { + return error.MaxPathExceeded; + } + } + + this._buf.append(trimmed, needs_sep); + }, + .rel => { + bun.debugAssert(!isInputAbsolute(input)); + + const trimmed = trimInput(.rel, input); + + if (trimmed.len == 0) { + return; + } + + if (comptime opts.check_length == .check_for_greater_than_max_path) { + if (this.len() + trimmed.len + @intFromBool(needs_sep) >= opts.maxPathLength()) { + return error.MaxPathExceeded; + } + } + + this._buf.append(trimmed, needs_sep); + }, + .any => { + const input_is_absolute = isInputAbsolute(input); + + if (comptime Environment.isDebug) { + if (needs_sep) { + bun.debugAssert(!input_is_absolute); + } + } + + const trimmed = trimInput(if (this.len() > 0) + // anything appended to an existing path should be trimmed + // as a relative path + .rel + else if (isInputAbsolute(input)) + // path is empty, trim based on input + .abs + else + .rel, input); + + if (trimmed.len == 0) { + return; + } + + if (comptime opts.check_length == .check_for_greater_than_max_path) { + if (this.len() + trimmed.len + @intFromBool(needs_sep) >= opts.maxPathLength()) { + return error.MaxPathExceeded; + } + } + + this._buf.append(trimmed, needs_sep); + }, + } + } + + pub fn appendFmt(this: *@This(), comptime fmt: []const u8, args: anytype) Result(void) { + // TODO: there's probably a better way to do this. needed for trimming slashes + var temp: Path(.{ .buf_type = .pool }) = .init(); + defer temp.deinit(); + + const input = switch (comptime opts.buf_type) { + .pool => std.fmt.bufPrint(temp._buf.pooled, fmt, args) catch { + if (comptime opts.check_length == .check_for_greater_than_max_path) { + return error.MaxPathExceeded; + } + unreachable; + }, + }; + + return this.append(input); + } + + pub fn join(this: *@This(), parts: []const []const opts.pathUnit()) Result(void) { + switch (comptime opts.unit) { + .u8 => {}, + .u16 => @compileError("unsupported unit type"), + .os => if (Environment.isWindows) @compileError("unsupported unit type"), + } + + switch (comptime opts.kind) { + .abs => {}, + .rel => @compileError("cannot join with relative path"), + .any => { + bun.debugAssert(this.isAbsolute()); + }, + } + + const cloned = this.clone(); + defer cloned.deinit(); + + switch (comptime opts.buf_type) { + .pool => { + const joined = bun.path.joinAbsStringBuf( + cloned.slice(), + this._buf.pooled, + parts, + switch (opts.sep) { + .any, .auto => .auto, + .posix => .posix, + .windows => .windows, + }, + ); + + const trimmed = trimInput(.abs, joined); + this._buf.len = trimmed.len; + }, + } + } + + pub fn relative(this: *const @This(), to: anytype) RelPath(opts) { + switch (comptime opts.buf_type) { + .pool => { + var output: RelPath(opts) = .init(); + const rel = bun.path.relativeBufZ(output._buf.pooled, this.slice(), to.slice()); + const trimmed = trimInput(.rel, rel); + output._buf.len = trimmed.len; + return output; + }, + } + } + + pub fn undo(this: *@This(), n_components: usize) void { + const min_len = switch (comptime opts.kind) { + .abs => rootLen(this.slice()) orelse 0, + .rel => 0, + .any => min_len: { + if (this.isAbsolute()) { + break :min_len rootLen(this.slice()) orelse 0; + } + break :min_len 0; + }, + }; + + var i: usize = 0; + while (i < n_components) { + const slash = switch (comptime opts.sep) { + .any => std.mem.lastIndexOfAny(opts.pathUnit(), this.slice(), &.{ std.fs.path.sep_posix, std.fs.path.sep_windows }), + .auto => std.mem.lastIndexOfScalar(opts.pathUnit(), this.slice(), std.fs.path.sep), + .posix => std.mem.lastIndexOfScalar(opts.pathUnit(), this.slice(), std.fs.path.sep_posix), + .windows => std.mem.lastIndexOfScalar(opts.pathUnit(), this.slice(), std.fs.path.sep_windows), + } orelse { + this._buf.setLength(min_len); + return; + }; + + if (slash < min_len) { + this._buf.setLength(min_len); + return; + } + + this._buf.setLength(slash); + i += 1; + } + } + + const ResetScope = struct { + path: *Path(opts), + saved_len: usize, + + pub fn restore(this: *const ResetScope) void { + this.path._buf.setLength(this.saved_len); + } + }; + + pub fn save(this: *@This()) ResetScope { + return .{ .path = this, .saved_len = this.len() }; + } + }; +} diff --git a/src/paths/path_buffer_pool.zig b/src/paths/path_buffer_pool.zig new file mode 100644 index 0000000000..3489d920a0 --- /dev/null +++ b/src/paths/path_buffer_pool.zig @@ -0,0 +1,34 @@ +const bun = @import("bun"); +const Environment = bun.Environment; +const ObjectPool = bun.ObjectPool; +const PathBuffer = bun.PathBuffer; +const WPathBuffer = bun.WPathBuffer; + +// This pool exists because on Windows, each path buffer costs 64 KB. +// This makes the stack memory usage very unpredictable, which means we can't really know how much stack space we have left. +// This pool is a workaround to make the stack memory usage more predictable. +// We keep up to 4 path buffers alive per thread at a time. +fn PathBufferPoolT(comptime T: type) type { + return struct { + const Pool = ObjectPool(T, null, true, 4); + + pub fn get() *T { + // use a threadlocal allocator so mimalloc deletes it on thread deinit. + return &Pool.get(bun.threadlocalAllocator()).data; + } + + pub fn put(buffer: *const T) void { + // there's no deinit function on T so @constCast is fine + var node: *Pool.Node = @alignCast(@fieldParentPtr("data", @constCast(buffer))); + node.release(); + } + + pub fn deleteAll() void { + Pool.deleteAll(); + } + }; +} + +pub const path_buffer_pool = PathBufferPoolT(PathBuffer); +pub const w_path_buffer_pool = PathBufferPoolT(WPathBuffer); +pub const os_path_buffer_pool = if (Environment.isWindows) w_path_buffer_pool else path_buffer_pool; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 733127d023..24d52e4a00 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -89,7 +89,7 @@ const bufs = struct { pub threadlocal var esm_absolute_package_path_joined: bun.PathBuffer = undefined; pub threadlocal var dir_entry_paths_to_resolve: [256]DirEntryResolveQueueItem = undefined; - pub threadlocal var open_dirs: [256]std.fs.Dir = undefined; + pub threadlocal var open_dirs: [256]FD = undefined; pub threadlocal var resolve_without_remapping: bun.PathBuffer = undefined; pub threadlocal var index: bun.PathBuffer = undefined; pub threadlocal var dir_info_uncached_filename: bun.PathBuffer = undefined; @@ -2216,7 +2216,7 @@ pub const Resolver = struct { var dir_entries_option: *Fs.FileSystem.RealFS.EntriesOption = undefined; var needs_iter = true; var in_place: ?*Fs.FileSystem.DirEntry = null; - const open_dir = bun.openDirForIteration(std.fs.cwd(), dir_path) catch |err| { + const open_dir = bun.openDirForIteration(FD.cwd(), dir_path).unwrap() catch |err| { // TODO: handle this error better r.log.addErrorFmt( null, @@ -2264,10 +2264,10 @@ pub const Resolver = struct { dir_entries_ptr.* = new_entry; if (r.store_fd) { - dir_entries_ptr.fd = .fromStdDir(open_dir); + dir_entries_ptr.fd = open_dir; } - bun.fs.debug("readdir({}, {s}) = {d}", .{ bun.FD.fromStdDir(open_dir), dir_path, dir_entries_ptr.data.count() }); + bun.fs.debug("readdir({}, {s}) = {d}", .{ open_dir, dir_path, dir_entries_ptr.data.count() }); dir_entries_option = rfs.entries.put(&cached_dir_entry_result, .{ .entries = dir_entries_ptr, @@ -2288,7 +2288,7 @@ pub const Resolver = struct { // to check for a parent package.json null, allocators.NotFound, - .fromStdDir(open_dir), + open_dir, package_id, ); return dir_info_ptr; @@ -2783,9 +2783,9 @@ pub const Resolver = struct { // When this function halts, any item not processed means it's not found. defer { if (open_dir_count > 0 and (!r.store_fd or r.fs.fs.needToCloseFiles())) { - const open_dirs: []std.fs.Dir = bufs(.open_dirs)[0..open_dir_count]; + const open_dirs = bufs(.open_dirs)[0..open_dir_count]; for (open_dirs) |open_dir| { - bun.FD.fromStdDir(open_dir).close(); + open_dir.close(); } } } @@ -2810,8 +2810,8 @@ pub const Resolver = struct { defer top_parent = queue_top.result; queue_slice.len -= 1; - const open_dir: std.fs.Dir = if (queue_top.fd.isValid()) - queue_top.fd.stdDir() + const open_dir: FD = if (queue_top.fd.isValid()) + queue_top.fd else open_dir: { // This saves us N copies of .toPosixPath // which was likely the perf gain from resolving directories relative to the parent directory, anyway. @@ -2820,19 +2820,20 @@ pub const Resolver = struct { defer path.ptr[queue_top.unsafe_path.len] = prev_char; const sentinel = path.ptr[0..queue_top.unsafe_path.len :0]; - const open_req = if (comptime Environment.isPosix) - std.fs.openDirAbsoluteZ( + const open_req = if (comptime Environment.isPosix) open_req: { + const dir_result = std.fs.openDirAbsoluteZ( sentinel, .{ .no_follow = !follow_symlinks, .iterate = true }, - ) - else if (comptime Environment.isWindows) open_req: { + ) catch |err| break :open_req err; + break :open_req FD.fromStdDir(dir_result); + } else if (comptime Environment.isWindows) open_req: { const dirfd_result = bun.sys.openDirAtWindowsA(bun.invalid_fd, sentinel, .{ .iterable = true, .no_follow = !follow_symlinks, .read_only = true, }); if (dirfd_result.unwrap()) |result| { - break :open_req result.stdDir(); + break :open_req result; } else |err| { break :open_req err; } @@ -2879,7 +2880,7 @@ pub const Resolver = struct { }; if (!queue_top.fd.isValid()) { - Fs.FileSystem.setMaxFd(open_dir.fd); + Fs.FileSystem.setMaxFd(open_dir.cast()); // these objects mostly just wrap the file descriptor, so it's fine to keep it. bufs(.open_dirs)[open_dir_count] = open_dir; open_dir_count += 1; @@ -2945,13 +2946,13 @@ pub const Resolver = struct { if (in_place) |existing| { existing.data.clearAndFree(allocator); } - new_entry.fd = if (r.store_fd) .fromStdDir(open_dir) else .invalid; + new_entry.fd = if (r.store_fd) open_dir else .invalid; var dir_entries_ptr = in_place orelse allocator.create(Fs.FileSystem.DirEntry) catch unreachable; dir_entries_ptr.* = new_entry; dir_entries_option = try rfs.entries.put(&cached_dir_entry_result, .{ .entries = dir_entries_ptr, }); - bun.fs.debug("readdir({}, {s}) = {d}", .{ bun.FD.fromStdDir(open_dir), dir_path, dir_entries_ptr.data.count() }); + bun.fs.debug("readdir({}, {s}) = {d}", .{ open_dir, dir_path, dir_entries_ptr.data.count() }); } // We must initialize it as empty so that the result index is correct. @@ -2966,7 +2967,7 @@ pub const Resolver = struct { cached_dir_entry_result.index, r.dir_cache.atIndex(top_parent.index), top_parent.index, - .fromStdDir(open_dir), + open_dir, null, ); diff --git a/src/semver/SemverString.zig b/src/semver/SemverString.zig index 9f278ac087..ee30c31e1a 100644 --- a/src/semver/SemverString.zig +++ b/src/semver/SemverString.zig @@ -135,7 +135,7 @@ pub const String = extern struct { str: *const String, buf: string, - pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { const str = formatter.str; try writer.writeAll(str.slice(formatter.buf)); } @@ -159,11 +159,33 @@ pub const String = extern struct { quote: bool = true, }; - pub fn format(formatter: JsonFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(formatter: JsonFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { try writer.print("{}", .{bun.fmt.formatJSONStringUTF8(formatter.str.slice(formatter.buf), .{ .quote = formatter.opts.quote })}); } }; + pub inline fn fmtStorePath(self: *const String, buf: []const u8) StorePathFormatter { + return .{ + .buf = buf, + .str = self, + }; + } + + pub const StorePathFormatter = struct { + str: *const String, + buf: string, + + pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { + for (this.str.slice(this.buf)) |c| { + switch (c) { + '/' => try writer.writeByte('+'), + '\\' => try writer.writeByte('+'), + else => try writer.writeByte(c), + } + } + } + }; + pub fn Sorter(comptime direction: enum { asc, desc }) type { return struct { lhs_buf: []const u8, diff --git a/src/shell/builtin/ls.zig b/src/shell/builtin/ls.zig index c3c3da25bd..06f75972c3 100644 --- a/src/shell/builtin/ls.zig +++ b/src/shell/builtin/ls.zig @@ -323,7 +323,7 @@ pub const ShellLsTask = struct { std.fmt.format(writer, "{s}:\n", .{this.path}) catch bun.outOfMemory(); } - var iterator = DirIterator.iterate(fd.stdDir(), .u8); + var iterator = DirIterator.iterate(fd, .u8); var entry = iterator.next(); // If `-a` is used, "." and ".." should show up as results. However, diff --git a/src/shell/builtin/rm.zig b/src/shell/builtin/rm.zig index 31c6268adf..8eca2e16da 100644 --- a/src/shell/builtin/rm.zig +++ b/src/shell/builtin/rm.zig @@ -860,7 +860,7 @@ pub const ShellRmTask = struct { return Maybe(void).success; } - var iterator = DirIterator.iterate(fd.stdDir(), .u8); + var iterator = DirIterator.iterate(fd, .u8); var entry = iterator.next(); var remove_child_vtable = RemoveFileVTable{ diff --git a/src/shell/builtin/which.zig b/src/shell/builtin/which.zig index 29eba3c740..50b55ee488 100644 --- a/src/shell/builtin/which.zig +++ b/src/shell/builtin/which.zig @@ -30,8 +30,8 @@ pub fn start(this: *Which) Yield { } if (this.bltn().stdout.needsIO() == null) { - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); const PATH = this.bltn().parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); var had_not_found = false; for (args) |arg_raw| { @@ -68,8 +68,8 @@ pub fn next(this: *Which) Yield { const arg_raw = multiargs.args_slice[multiargs.arg_idx]; const arg = arg_raw[0..std.mem.len(arg_raw)]; - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); const PATH = this.bltn().parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); const resolved = which(path_buf, PATH.slice(), this.bltn().parentCmd().base.shell.cwdZ(), arg) orelse { diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 1da175db59..a62c7e12b5 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -820,8 +820,8 @@ pub const Interpreter = struct { }; // Avoid the large stack allocation on Windows. - const pathbuf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(pathbuf); + const pathbuf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(pathbuf); const cwd: [:0]const u8 = switch (Syscall.getcwdZ(pathbuf)) { .result => |cwd| cwd, .err => |err| { @@ -1701,15 +1701,15 @@ pub const ShellSyscall = struct { pub fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) { if (bun.Environment.isWindows) { - const buf: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const path = switch (getPath(dir, path_, buf)) { .err => |e| return .{ .err = e }, .result => |p| p, }; return switch (Syscall.stat(path)) { - .err => |e| .{ .err = e.clone(bun.default_allocator) catch bun.outOfMemory() }, + .err => |e| .{ .err = e.clone(bun.default_allocator) }, .result => |s| .{ .result = s }, }; } @@ -1723,8 +1723,8 @@ pub const ShellSyscall = struct { if (bun.Environment.isWindows) { if (flags & bun.O.DIRECTORY != 0) { if (ResolvePath.Platform.posix.isAbsolute(path[0..path.len])) { - const buf: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, @@ -1740,8 +1740,8 @@ pub const ShellSyscall = struct { }; } - const buf: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(buf); + const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, diff --git a/src/shell/states/Cmd.zig b/src/shell/states/Cmd.zig index c5bf72fbe2..71cfa06a08 100644 --- a/src/shell/states/Cmd.zig +++ b/src/shell/states/Cmd.zig @@ -487,8 +487,8 @@ fn initSubproc(this: *Cmd) Yield { return this.exec.bltn.start(); } - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); const resolved = which(path_buf, spawn_args.PATH, spawn_args.cwd, first_arg_real) orelse blk: { if (bun.strings.eqlComptime(first_arg_real, "bun") or bun.strings.eqlComptime(first_arg_real, "bun-debug")) blk2: { break :blk bun.selfExePath() catch break :blk2; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index e664edf77d..b580dfe61d 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -1100,8 +1100,8 @@ pub fn getSourceMapImpl( // try to load a .map file if (load_hint != .is_inline_map) try_external: { - var load_path_buf: *bun.PathBuffer = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(load_path_buf); + var load_path_buf: *bun.PathBuffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(load_path_buf); if (source_filename.len + 4 > load_path_buf.len) break :try_external; @memcpy(load_path_buf[0..source_filename.len], source_filename); diff --git a/src/string/paths.zig b/src/string/paths.zig index 07813f037e..0a7eba3dc7 100644 --- a/src/string/paths.zig +++ b/src/string/paths.zig @@ -118,18 +118,6 @@ pub fn toNTPath16(wbuf: []u16, path: []const u16) [:0]u16 { return wbuf[0 .. toWPathNormalized16(wbuf[prefix.len..], path).len + prefix.len :0]; } -pub fn toNTMaxPath(buf: []u8, utf8: []const u8) [:0]const u8 { - if (!std.fs.path.isAbsoluteWindows(utf8) or utf8.len <= 260) { - @memcpy(buf[0..utf8.len], utf8); - buf[utf8.len] = 0; - return buf[0..utf8.len :0]; - } - - const prefix = bun.windows.nt_maxpath_prefix_u8; - buf[0..prefix.len].* = prefix; - return buf[0 .. toPathNormalized(buf[prefix.len..], utf8).len + prefix.len :0]; -} - pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]u16 { wbuf[0..bun.windows.nt_object_prefix.len].* = bun.windows.nt_object_prefix; @memcpy(wbuf[bun.windows.nt_object_prefix.len..][0..utf16.len], utf16); @@ -155,6 +143,11 @@ pub const toNTDir = toNTPath; pub fn toExtendedPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { bun.unsafeAssert(wbuf.len > 4); + if (hasPrefixComptime(utf8, bun.windows.long_path_prefix_u8) or + hasPrefixComptime(utf8, bun.windows.nt_object_prefix_u8)) + { + return toWPathNormalized(wbuf, utf8); + } wbuf[0..4].* = bun.windows.long_path_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } @@ -168,8 +161,8 @@ pub fn toWPathNormalizeAutoExtend(wbuf: []u16, utf8: []const u8) [:0]const u16 { } pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]u16 { - const renormalized = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(renormalized); + const renormalized = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(renormalized); var path_to_use = normalizeSlashesOnly(renormalized, utf8, '\\'); @@ -195,8 +188,8 @@ pub fn toWPathNormalized16(wbuf: []u16, path: []const u16) [:0]u16 { } pub fn toPathNormalized(buf: []u8, utf8: []const u8) [:0]const u8 { - const renormalized = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(renormalized); + const renormalized = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(renormalized); var path_to_use = normalizeSlashesOnly(renormalized, utf8, '\\'); @@ -235,12 +228,12 @@ pub fn normalizeSlashesOnly(buf: []u8, utf8: []const u8, comptime desired_slash: pub fn toWDirNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { var renormalized: ?*bun.PathBuffer = null; - defer if (renormalized) |r| bun.PathBufferPool.put(r); + defer if (renormalized) |r| bun.path_buffer_pool.put(r); var path_to_use = utf8; if (bun.strings.containsChar(utf8, '/')) { - renormalized = bun.PathBufferPool.get(); + renormalized = bun.path_buffer_pool.get(); @memcpy(renormalized.?[0..utf8.len], utf8); for (renormalized.?[0..utf8.len]) |*c| { if (c.* == '/') { @@ -447,6 +440,67 @@ pub fn removeLeadingDotSlash(slice: []const u8) callconv(bun.callconv_inline) [] return slice; } +// Copied from std, modified to accept input type +pub fn basename(comptime T: type, input: []const T) []const T { + if (comptime Environment.isWindows) { + return basenameWindows(T, input); + } + return basenamePosix(T, input); +} + +fn basenamePosix(comptime T: type, input: []const T) []const T { + if (input.len == 0) + return &[_]u8{}; + + var end_index: usize = input.len - 1; + while (input[end_index] == '/') { + if (end_index == 0) + return &.{}; + end_index -= 1; + } + var start_index: usize = end_index; + end_index += 1; + while (input[start_index] != '/') { + if (start_index == 0) + return input[0..end_index]; + start_index -= 1; + } + + return input[start_index + 1 .. end_index]; +} + +fn basenameWindows(comptime T: type, input: []const T) []const T { + if (input.len == 0) + return &.{}; + + var end_index: usize = input.len - 1; + while (true) { + const byte = input[end_index]; + if (byte == '/' or byte == '\\') { + if (end_index == 0) + return &.{}; + end_index -= 1; + continue; + } + if (byte == ':' and end_index == 1) { + return &.{}; + } + break; + } + + var start_index: usize = end_index; + end_index += 1; + while (input[start_index] != '/' and input[start_index] != '\\' and + !(input[start_index] == ':' and start_index == 1)) + { + if (start_index == 0) + return input[0..end_index]; + start_index -= 1; + } + + return input[start_index + 1 .. end_index]; +} + const bun = @import("bun"); const std = @import("std"); const Environment = bun.Environment; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 7c4f6637f3..2a78d74483 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -2329,7 +2329,6 @@ pub const startsWithWindowsDriveLetter = _paths.startsWithWindowsDriveLetter; pub const startsWithWindowsDriveLetterT = _paths.startsWithWindowsDriveLetterT; pub const toExtendedPathNormalized = _paths.toExtendedPathNormalized; pub const toKernel32Path = _paths.toKernel32Path; -pub const toNTMaxPath = _paths.toNTMaxPath; pub const toNTPath = _paths.toNTPath; pub const toNTPath16 = _paths.toNTPath16; pub const toPath = _paths.toPath; @@ -2347,6 +2346,7 @@ pub const withoutLeadingSlash = _paths.withoutLeadingSlash; pub const withoutNTPrefix = _paths.withoutNTPrefix; pub const withoutTrailingSlash = _paths.withoutTrailingSlash; pub const withoutTrailingSlashWindowsPath = _paths.withoutTrailingSlashWindowsPath; +pub const basename = _paths.basename; pub const log = bun.Output.scoped(.STR, true); pub const grapheme = @import("./grapheme.zig"); diff --git a/src/sys.zig b/src/sys.zig index 84490643ba..6ab9d10606 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -57,6 +57,7 @@ const Environment = bun.Environment; const JSC = bun.JSC; const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; const SystemError = JSC.SystemError; +const FD = bun.FD; const linux = syscall; @@ -215,6 +216,7 @@ pub const Tag = enum(u8) { chmod, chown, clonefile, + clonefileat, close, copy_file_range, copyfile, @@ -343,10 +345,10 @@ pub const Error = struct { syscall: sys.Tag = sys.Tag.TODO, dest: []const u8 = "", - pub fn clone(this: *const Error, allocator: std.mem.Allocator) !Error { + pub fn clone(this: *const Error, allocator: std.mem.Allocator) Error { var copy = this.*; - copy.path = try allocator.dupe(u8, copy.path); - copy.dest = try allocator.dupe(u8, copy.dest); + copy.path = allocator.dupe(u8, copy.path) catch bun.outOfMemory(); + copy.dest = allocator.dupe(u8, copy.dest) catch bun.outOfMemory(); return copy; } @@ -665,8 +667,8 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { buf[0] = 0; if (comptime Environment.isWindows) { - var wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + var wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); const len: windows.DWORD = kernel32.GetCurrentDirectoryW(wbuf.len, wbuf); if (Result.errnoSysP(len, .getcwd, buf)) |err| return err; return Result{ .result = bun.strings.fromWPath(buf, wbuf[0..len]) }; @@ -758,8 +760,8 @@ pub fn chdirOSPath(path: bun.stringZ, destination: if (Environment.isPosix) bun. } if (comptime Environment.isWindows) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); if (c.SetCurrentDirectoryW(bun.strings.toWDirPath(wbuf, destination)) == windows.FALSE) { log("SetCurrentDirectory({s}) = {d}", .{ destination, kernel32.GetLastError() }); return Maybe(void).errnoSysPD(0, .chdir, path, destination) orelse Maybe(void).success; @@ -906,8 +908,8 @@ pub fn lutimes(path: [:0]const u8, atime: JSC.Node.TimeLike, mtime: JSC.Node.Tim } pub fn mkdiratA(dir_fd: bun.FileDescriptor, file_path: []const u8) Maybe(void) { - const buf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(buf); + const buf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(buf); return mkdiratW(dir_fd, bun.strings.toWPathNormalized(buf, file_path)); } @@ -932,7 +934,7 @@ pub const mkdirat = if (Environment.isWindows) else mkdiratPosix; -pub fn mkdiratW(dir_fd: bun.FileDescriptor, file_path: []const u16, _: i32) Maybe(void) { +pub fn mkdiratW(dir_fd: bun.FileDescriptor, file_path: [:0]const u16, _: i32) Maybe(void) { const dir_to_make = openDirAtWindowsNtPath(dir_fd, file_path, .{ .iterable = false, .can_rename_or_delete = true, .create = true }); if (dir_to_make == .err) { return .{ .err = dir_to_make.err }; @@ -968,8 +970,8 @@ pub fn mkdir(file_path: [:0]const u8, flags: mode_t) Maybe(void) { .linux => Maybe(void).errnoSysP(syscall.mkdir(file_path, flags), .mkdir, file_path) orelse Maybe(void).success, .windows => { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); return Maybe(void).errnoSysP( bun.windows.CreateDirectoryW(bun.strings.toKernel32Path(wbuf, file_path).ptr, null), .mkdir, @@ -1001,8 +1003,8 @@ pub fn mkdirA(file_path: []const u8, flags: mode_t) Maybe(void) { } if (comptime Environment.isWindows) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); const wpath = bun.strings.toKernel32Path(wbuf, file_path); assertIsValidWindowsPath(u16, wpath); return Maybe(void).errnoSysP( @@ -1066,8 +1068,8 @@ pub fn normalizePathWindows( if (comptime T != u8 and T != u16) { @compileError("normalizePathWindows only supports u8 and u16 character types"); } - const wbuf = if (T != u16) bun.WPathBufferPool.get(); - defer if (T != u16) bun.WPathBufferPool.put(wbuf); + const wbuf = if (T != u16) bun.w_path_buffer_pool.get(); + defer if (T != u16) bun.w_path_buffer_pool.put(wbuf); var path = if (T == u16) path_ else bun.strings.convertUTF8toUTF16InBuffer(wbuf, path_); if (std.fs.path.isAbsoluteWindowsWTF16(path)) { @@ -1137,8 +1139,8 @@ pub fn normalizePathWindows( path = path[2..]; } - const buf1 = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(buf1); + const buf1 = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(buf1); @memcpy(buf1[0..base_path.len], base_path); buf1[base_path.len] = '\\'; @memcpy(buf1[base_path.len + 1 .. base_path.len + 1 + path.len], path); @@ -1292,8 +1294,8 @@ fn openDirAtWindowsT( path: []const T, options: WindowsOpenDirOptions, ) Maybe(bun.FileDescriptor) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); const norm = switch (normalizePathWindows(T, dirFd, path, wbuf, .{})) { .err => |err| return .{ .err = err }, @@ -1611,8 +1613,8 @@ pub fn openFileAtWindowsT( path: []const T, options: NtCreateFileOptions, ) Maybe(bun.FileDescriptor) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); const norm = switch (normalizePathWindows(T, dirFd, path, wbuf, .{})) { .err => |err| return .{ .err = err }, @@ -2555,11 +2557,11 @@ pub fn renameat2(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.F pub fn renameat(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.FileDescriptor, to: [:0]const u8) Maybe(void) { if (Environment.isWindows) { - const w_buf_from = bun.WPathBufferPool.get(); - const w_buf_to = bun.WPathBufferPool.get(); + const w_buf_from = bun.w_path_buffer_pool.get(); + const w_buf_to = bun.w_path_buffer_pool.get(); defer { - bun.WPathBufferPool.put(w_buf_from); - bun.WPathBufferPool.put(w_buf_to); + bun.w_path_buffer_pool.put(w_buf_from); + bun.w_path_buffer_pool.put(w_buf_to); } const rc = bun.windows.renameAtW( @@ -2599,8 +2601,10 @@ pub fn symlink(target: [:0]const u8, dest: [:0]const u8) Maybe(void) { while (true) { if (Maybe(void).errnoSys(syscall.symlink(target, dest), .symlink)) |err| { if (err.getErrno() == .INTR) continue; + log("symlink({s}, {s}) = {s}", .{ target, dest, @tagName(err.getErrno()) }); return err; } + log("symlink({s}, {s}) = 0", .{ target, dest }); return Maybe(void).success; } } @@ -2609,8 +2613,10 @@ pub fn symlinkat(target: [:0]const u8, dirfd: bun.FileDescriptor, dest: [:0]cons while (true) { if (Maybe(void).errnoSys(syscall.symlinkat(target, dirfd.cast(), dest), .symlinkat)) |err| { if (err.getErrno() == .INTR) continue; + log("symlinkat({s}, {}, {s}) = {s}", .{ target, dirfd, dest, @tagName(err.getErrno()) }); return err; } + log("symlinkat({s}, {}, {s}) = 0", .{ target, dirfd, dest }); return Maybe(void).success; } } @@ -2634,15 +2640,21 @@ pub const WindowsSymlinkOptions = packed struct { pub var has_failed_to_create_symlink = false; }; -pub fn symlinkOrJunction(dest: [:0]const u8, target: [:0]const u8) Maybe(void) { - if (comptime !Environment.isWindows) @compileError("symlinkOrJunction is windows only"); +/// Symlinks on Windows can be relative or absolute, and junctions can +/// only be absolute. Passing `null` for `abs_fallback_junction_target` +/// is saying `target` is already absolute. +pub fn symlinkOrJunction(dest: [:0]const u8, target: [:0]const u8, abs_fallback_junction_target: ?[:0]const u8) Maybe(void) { + if (comptime !Environment.isWindows) { + // return symlink(target, dest); + @compileError("windows only plz!!"); + } if (!WindowsSymlinkOptions.has_failed_to_create_symlink) { - const sym16 = bun.WPathBufferPool.get(); - const target16 = bun.WPathBufferPool.get(); + const sym16 = bun.w_path_buffer_pool.get(); + const target16 = bun.w_path_buffer_pool.get(); defer { - bun.WPathBufferPool.put(sym16); - bun.WPathBufferPool.put(target16); + bun.w_path_buffer_pool.put(sym16); + bun.w_path_buffer_pool.put(target16); } const sym_path = bun.strings.toWPathNormalizeAutoExtend(sym16, dest); const target_path = bun.strings.toWPathNormalizeAutoExtend(target16, target); @@ -2651,14 +2663,26 @@ pub fn symlinkOrJunction(dest: [:0]const u8, target: [:0]const u8) Maybe(void) { return Maybe(void).success; }, .err => |err| { - if (err.getErrno() == .EXIST) { - return .{ .err = err }; + switch (err.getErrno()) { + .EXIST, .NOENT => { + // if the destination already exists, or a component + // of the destination doesn't exist, return the error + // without trying junctions. + return .{ .err = err }; + }, + else => { + // fallthrough to junction + }, } }, } } - return sys_uv.symlinkUV(target, dest, bun.windows.libuv.UV_FS_SYMLINK_JUNCTION); + return sys_uv.symlinkUV( + abs_fallback_junction_target orelse target, + dest, + bun.windows.libuv.UV_FS_SYMLINK_JUNCTION, + ); } pub fn symlinkW(dest: [:0]const u16, target: [:0]const u16, options: WindowsSymlinkOptions) Maybe(void) { @@ -2684,6 +2708,20 @@ pub fn symlinkW(dest: [:0]const u16, target: [:0]const u16, options: WindowsSyml } if (errno.toSystemErrno()) |err| { + switch (err) { + .ENOENT, + .EEXIST, + => { + return .{ + .err = .{ + .errno = @intFromEnum(err), + .syscall = .symlink, + }, + }; + }, + + else => {}, + } WindowsSymlinkOptions.has_failed_to_create_symlink = true; return .{ .err = .{ @@ -2712,12 +2750,46 @@ pub fn clonefile(from: [:0]const u8, to: [:0]const u8) Maybe(void) { while (true) { if (Maybe(void).errnoSys(c.clonefile(from, to, 0), .clonefile)) |err| { if (err.getErrno() == .INTR) continue; + log("clonefile({s}, {s}) = {s}", .{ from, to, @tagName(err.getErrno()) }); return err; } + log("clonefile({s}, {s}) = 0", .{ from, to }); return Maybe(void).success; } } +pub fn clonefileat(from: FD, from_path: [:0]const u8, to: FD, to_path: [:0]const u8) Maybe(void) { + if (comptime !Environment.isMac) { + @compileError("macOS only"); + } + + while (true) { + if (Maybe(void).errnoSys(c.clonefileat(from.cast(), from_path, to.cast(), to_path, 0), .clonefileat)) |err| { + if (err.getErrno() == .INTR) continue; + log( + \\clonefileat( + \\ {}, + \\ {s}, + \\ {}, + \\ {s}, + \\) = {s} + \\ + , .{ from, from_path, to, to_path, @tagName(err.getErrno()) }); + return err; + } + log( + \\clonefileat( + \\ {}, + \\ {s}, + \\ {}, + \\ {s}, + \\) = 0 + \\ + , .{ from, from_path, to, to_path }); + return .success; + } +} + pub fn copyfile(from: [:0]const u8, to: [:0]const u8, flags: posix.system.COPYFILE) Maybe(void) { if (comptime !Environment.isMac) @compileError("macOS only"); @@ -2743,8 +2815,10 @@ pub fn fcopyfile(fd_in: bun.FileDescriptor, fd_out: bun.FileDescriptor, flags: p } pub fn unlinkW(from: [:0]const u16) Maybe(void) { - if (windows.DeleteFileW(from.ptr) != 0) { - return .{ .err = Error.fromCode(bun.windows.getLastErrno(), .unlink) }; + const ret = windows.DeleteFileW(from); + if (Maybe(void).errnoSys(ret, .unlink)) |err| { + log("DeleteFileW({s}) = {s}", .{ bun.fmt.fmtPath(u16, from, .{}), @tagName(err.getErrno()) }); + return err; } return Maybe(void).success; @@ -2752,14 +2826,15 @@ pub fn unlinkW(from: [:0]const u16) Maybe(void) { pub fn unlink(from: [:0]const u8) Maybe(void) { if (comptime Environment.isWindows) { - const w_buf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(w_buf); - return unlinkW(bun.strings.toNTPath(w_buf, from)); + const w_buf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(w_buf); + return unlinkW(bun.strings.toWPathNormalizeAutoExtend(w_buf, from)); } while (true) { if (Maybe(void).errnoSysP(syscall.unlink(from), .unlink, from)) |err| { if (err.getErrno() == .INTR) continue; + log("unlink({s}) = {s}", .{ from, @tagName(err.getErrno()) }); return err; } @@ -2775,8 +2850,8 @@ pub fn rmdirat(dirfd: bun.FileDescriptor, to: anytype) Maybe(void) { pub fn unlinkatWithFlags(dirfd: bun.FileDescriptor, to: anytype, flags: c_uint) Maybe(void) { if (Environment.isWindows) { if (comptime std.meta.Elem(@TypeOf(to)) == u8) { - const w_buf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(w_buf); + const w_buf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(w_buf); return unlinkatWithFlags(dirfd, bun.strings.toNTPath(w_buf, bun.span(to)), flags); } @@ -2790,7 +2865,7 @@ pub fn unlinkatWithFlags(dirfd: bun.FileDescriptor, to: anytype, flags: c_uint) if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, flags), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) - log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); + log("unlinkat({}, {s}) = {s}", .{ dirfd, bun.sliceTo(to, 0), @tagName(err.getErrno()) }); return err; } if (comptime Environment.allow_assert) @@ -2808,7 +2883,7 @@ pub fn unlinkat(dirfd: bun.FileDescriptor, to: anytype) Maybe(void) { if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, 0), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) - log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); + log("unlinkat({}, {s}) = {s}", .{ dirfd, bun.sliceTo(to, 0), @tagName(err.getErrno()) }); return err; } if (comptime Environment.allow_assert) @@ -3232,8 +3307,8 @@ pub fn getFileAttributes(path: anytype) ?WindowsFileAttributes { const attributes: WindowsFileAttributes = @bitCast(dword); return attributes; } else { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); const path_to_use = bun.strings.toKernel32Path(wbuf, path); return getFileAttributes(path_to_use); } @@ -3434,8 +3509,8 @@ pub const ExistsAtType = enum { }; pub fn existsAtType(fd: bun.FileDescriptor, subpath: anytype) Maybe(ExistsAtType) { if (comptime Environment.isWindows) { - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); + const wbuf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(wbuf); var path = if (std.meta.Child(@TypeOf(subpath)) == u16) bun.strings.toNTPath16(wbuf, subpath) else @@ -3496,8 +3571,8 @@ pub fn existsAtType(fd: bun.FileDescriptor, subpath: anytype) Maybe(ExistsAtType } if (std.meta.sentinel(@TypeOf(subpath)) == null) { - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); @memcpy(path_buf[0..subpath.len], subpath); path_buf[subpath.len] = 0; const slice: [:0]const u8 = @ptrCast(path_buf); @@ -3701,28 +3776,69 @@ pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { return dupWithFlags(fd, 0); } -pub fn linkat(dir_fd: bun.FileDescriptor, basename: []const u8, dest_dir_fd: bun.FileDescriptor, dest_name: []const u8) Maybe(void) { - return Maybe(void).errnoSysP( - std.c.linkat( - @intCast(dir_fd), - &(std.posix.toPosixPath(basename) catch return .{ - .err = .{ - .errno = @intFromEnum(E.NOMEM), - .syscall = .open, - }, - }), - @intCast(dest_dir_fd), - &(std.posix.toPosixPath(dest_name) catch return .{ - .err = .{ - .errno = @intFromEnum(E.NOMEM), - .syscall = .open, - }, - }), - 0, - ), - .link, - basename, - ) orelse Maybe(void).success; +pub fn link(comptime T: type, src: [:0]const T, dest: [:0]const T) Maybe(void) { + if (comptime Environment.isWindows) { + if (T == u8) { + return sys_uv.link(src, dest); + } + + const ret = bun.windows.CreateHardLinkW(dest, src, null); + if (Maybe(void).errnoSys(ret, .link)) |err| { + log("CreateHardLinkW({s}, {s}) = {s}", .{ + bun.fmt.fmtPath(T, dest, .{}), + bun.fmt.fmtPath(T, src, .{}), + @tagName(err.getErrno()), + }); + return err; + } + + log("CreateHardLinkW({s}, {s}) = 0", .{ + bun.fmt.fmtPath(T, dest, .{}), + bun.fmt.fmtPath(T, src, .{}), + }); + return .success; + } + + if (T == u16) { + @compileError("unexpected path type"); + } + + const ret = std.c.link(src, dest); + if (Maybe(void).errnoSysP(ret, .link, src)) |err| { + log("link({s}, {s}) = {s}", .{ src, dest, @tagName(err.getErrno()) }); + return err; + } + log("link({s}, {s}) = 0", .{ src, dest }); + return .success; +} + +pub fn linkat(src: bun.FileDescriptor, src_path: []const u8, dest: bun.FileDescriptor, dest_path: []const u8) Maybe(void) { + return linkatZ( + src, + &(std.posix.toPosixPath(src_path) catch return .{ + .err = .{ + .errno = @intFromEnum(E.NOMEM), + .syscall = .link, + }, + }), + dest, + &(std.posix.toPosixPath(dest_path) catch return .{ + .err = .{ + .errno = @intFromEnum(E.NOMEM), + .syscall = .link, + }, + }), + ); +} + +pub fn linkatZ(src: FD, src_path: [:0]const u8, dest: FD, dest_path: [:0]const u8) Maybe(void) { + const ret = std.c.linkat(src.cast(), src_path, dest.cast(), dest_path, 0); + if (Maybe(void).errnoSysP(ret, .link, src_path)) |err| { + log("linkat({}, {s}, {}, {s}) = {s}", .{ src, src_path, dest, dest_path, @tagName(err.getErrno()) }); + return err; + } + log("linkat({}, {s}, {}, {s}) = 0", .{ src, src_path, dest, dest_path }); + return .success; } pub fn linkatTmpfile(tmpfd: bun.FileDescriptor, dirfd: bun.FileDescriptor, name: [:0]const u8) Maybe(void) { diff --git a/src/walker_skippable.zig b/src/walker_skippable.zig index 679c5192dd..a352a32059 100644 --- a/src/walker_skippable.zig +++ b/src/walker_skippable.zig @@ -6,6 +6,9 @@ const path = std.fs.path; const DirIterator = bun.DirIterator; const Environment = bun.Environment; const OSPathSlice = bun.OSPathSlice; +const OSPathSliceZ = bun.OSPathSliceZ; +const OOM = bun.OOM; +const FD = bun.FD; stack: std.ArrayList(StackItem), name_buffer: NameBufferList, @@ -16,17 +19,16 @@ seed: u64 = 0, const NameBufferList = std.ArrayList(bun.OSPathChar); -const Dir = std.fs.Dir; const WrappedIterator = DirIterator.NewWrappedIterator(if (Environment.isWindows) .u16 else .u8); pub const WalkerEntry = struct { /// The containing directory. This can be used to operate directly on `basename` /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. /// The directory remains open until `next` or `deinit` is called. - dir: Dir, - basename: OSPathSlice, - path: OSPathSlice, - kind: Dir.Entry.Kind, + dir: FD, + basename: OSPathSliceZ, + path: OSPathSliceZ, + kind: std.fs.Dir.Entry.Kind, }; const StackItem = struct { @@ -37,13 +39,13 @@ const StackItem = struct { /// After each call to this function, and on deinit(), the memory returned /// from this function becomes invalid. A copy must be made in order to keep /// a reference to the path. -pub fn next(self: *Walker) !?WalkerEntry { +pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) { while (self.stack.items.len != 0) { // `top` becomes invalid after appending to `self.stack` var top = &self.stack.items[self.stack.items.len - 1]; var dirname_len = top.dirname_len; switch (top.iter.next()) { - .err => |err| return bun.errnoToZigErr(err.errno), + .err => |err| return .initErr(err), .result => |res| { if (res) |base| { switch (base.kind) { @@ -79,37 +81,32 @@ pub fn next(self: *Walker) !?WalkerEntry { self.name_buffer.shrinkRetainingCapacity(dirname_len); if (self.name_buffer.items.len != 0) { - try self.name_buffer.append(path.sep); + self.name_buffer.append(path.sep) catch bun.outOfMemory(); dirname_len += 1; } - try self.name_buffer.appendSlice(base.name.slice()); + self.name_buffer.appendSlice(base.name.slice()) catch bun.outOfMemory(); const cur_len = self.name_buffer.items.len; - try self.name_buffer.append(0); - self.name_buffer.shrinkRetainingCapacity(cur_len); + self.name_buffer.append(0) catch bun.outOfMemory(); if (base.kind == .directory) { - var new_dir = (if (Environment.isWindows) - top.iter.iter.dir.openDirW(base.name.sliceAssumeZ(), .{ .iterate = true }) - else - top.iter.iter.dir.openDir(base.name.slice(), .{ .iterate = true })) catch |err| switch (err) { - error.NameTooLong => unreachable, // no path sep in base.name - else => |e| return e, + const new_dir = switch (bun.openDirForIterationOSPath(top.iter.iter.dir, base.name.slice())) { + .result => |fd| fd, + .err => |err| return .initErr(err), }; { - errdefer new_dir.close(); - try self.stack.append(StackItem{ + self.stack.append(StackItem{ .iter = DirIterator.iterate(new_dir, if (Environment.isWindows) .u16 else .u8), - .dirname_len = self.name_buffer.items.len, - }); + .dirname_len = cur_len, + }) catch bun.outOfMemory(); top = &self.stack.items[self.stack.items.len - 1]; } } - return WalkerEntry{ + return .initResult(WalkerEntry{ .dir = top.iter.iter.dir, - .basename = self.name_buffer.items[dirname_len..], - .path = self.name_buffer.items, + .basename = self.name_buffer.items[dirname_len..cur_len :0], + .path = self.name_buffer.items[0..cur_len :0], .kind = base.kind, - }; + }); } else { var item = self.stack.pop().?; if (self.stack.items.len != 0) { @@ -119,7 +116,7 @@ pub fn next(self: *Walker) !?WalkerEntry { }, } } - return null; + return .initResult(null); } pub fn deinit(self: *Walker) void { @@ -142,11 +139,11 @@ pub fn deinit(self: *Walker) void { /// The order of returned file system entries is undefined. /// `self` will not be closed after walking it. pub fn walk( - self: Dir, + self: FD, allocator: Allocator, skip_filenames: []const OSPathSlice, skip_dirnames: []const OSPathSlice, -) !Walker { +) OOM!Walker { var name_buffer = NameBufferList.init(allocator); errdefer name_buffer.deinit(); diff --git a/src/which.zig b/src/which.zig index 34bed1b26b..2bb4f57340 100644 --- a/src/which.zig +++ b/src/which.zig @@ -20,8 +20,8 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con bun.Output.scoped(.which, true)("path={s} cwd={s} bin={s}", .{ path, cwd, bin }); if (bun.Environment.os == .windows) { - const convert_buf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(convert_buf); + const convert_buf = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(convert_buf); const result = whichWin(convert_buf, path, cwd, bin) orelse return null; const result_converted = bun.strings.convertUTF16toUTF8InBuffer(buf, result) catch unreachable; buf[result_converted.len] = 0; @@ -133,8 +133,8 @@ fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *bun.PathBuffer, path: []con /// It is similar to Get-Command in powershell. pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u16 { if (bin.len == 0) return null; - const path_buf = bun.PathBufferPool.get(); - defer bun.PathBufferPool.put(path_buf); + const path_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buf); const check_windows_extensions = !endsWithExtension(bin); diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index ea3693d1e6..31fa4e005d 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -12,7 +12,7 @@ beforeEach(() => { packageDir = tmpdirSync(); }); -async function packExpectError(cwd: string, env: NodeJS.ProcessEnv, ...args: string[]) { +async function packExpectError(cwd: string, env: NodeJS.Dict, ...args: string[]) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "pack", ...args], cwd, diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts new file mode 100644 index 0000000000..bf8aac23f6 --- /dev/null +++ b/test/cli/install/isolated-install.test.ts @@ -0,0 +1,433 @@ +import { file, write } from "bun"; +import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { existsSync, readlinkSync } from "fs"; +import { VerdaccioRegistry, bunEnv, readdirSorted, runBunInstall } from "harness"; +import { join } from "path"; + +const registry = new VerdaccioRegistry(); + +beforeAll(async () => { + setDefaultTimeout(10 * 60 * 1000); + await registry.start(); +}); + +afterAll(() => { + registry.stop(); +}); + +describe("basic", () => { + test("single dependency", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-1", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect(readlinkSync(join(packageDir, "node_modules", "no-deps"))).toBe( + join(".bun", "no-deps@1.0.0", "node_modules", "no-deps"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "no-deps"))).toBe( + join("..", "no-deps@1.0.0", "node_modules", "no-deps"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "no-deps@1.0.0", "node_modules", "no-deps", "package.json"), + ).json(), + ).toEqual({ + name: "no-deps", + version: "1.0.0", + }); + }); + + test("scope package", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-2", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "@types/is-number": "1.0.0", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect(readlinkSync(join(packageDir, "node_modules", "@types", "is-number"))).toBe( + join("..", ".bun", "@types+is-number@1.0.0", "node_modules", "@types", "is-number"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "@types", "is-number"))).toBe( + join("..", "..", "@types+is-number@1.0.0", "node_modules", "@types", "is-number"), + ); + expect( + await file( + join( + packageDir, + "node_modules", + ".bun", + "@types+is-number@1.0.0", + "node_modules", + "@types", + "is-number", + "package.json", + ), + ).json(), + ).toEqual({ + name: "@types/is-number", + version: "1.0.0", + }); + }); + + test("transitive dependencies", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-3", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "two-range-deps": "1.0.0", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bun", "two-range-deps"]); + expect(readlinkSync(join(packageDir, "node_modules", "two-range-deps"))).toBe( + join(".bun", "two-range-deps@1.0.0", "node_modules", "two-range-deps"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "two-range-deps"))).toBe( + join("..", "two-range-deps@1.0.0", "node_modules", "two-range-deps"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "no-deps"))).toBe( + join("..", "no-deps@1.1.0", "node_modules", "no-deps"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "@types", "is-number"))).toBe( + join("..", "..", "@types+is-number@2.0.0", "node_modules", "@types", "is-number"), + ); + expect( + await file( + join( + packageDir, + "node_modules", + ".bun", + "two-range-deps@1.0.0", + "node_modules", + "two-range-deps", + "package.json", + ), + ).json(), + ).toEqual({ + name: "two-range-deps", + version: "1.0.0", + dependencies: { + "no-deps": "^1.0.0", + "@types/is-number": ">=1.0.0", + }, + }); + expect( + await readdirSorted(join(packageDir, "node_modules", ".bun", "two-range-deps@1.0.0", "node_modules")), + ).toEqual(["@types", "no-deps", "two-range-deps"]); + expect( + readlinkSync( + join(packageDir, "node_modules", ".bun", "two-range-deps@1.0.0", "node_modules", "@types", "is-number"), + ), + ).toBe(join("..", "..", "..", "@types+is-number@2.0.0", "node_modules", "@types", "is-number")); + expect( + readlinkSync(join(packageDir, "node_modules", ".bun", "two-range-deps@1.0.0", "node_modules", "no-deps")), + ).toBe(join("..", "..", "no-deps@1.1.0", "node_modules", "no-deps")); + expect( + await file( + join(packageDir, "node_modules", ".bun", "no-deps@1.1.0", "node_modules", "no-deps", "package.json"), + ).json(), + ).toEqual({ + name: "no-deps", + version: "1.1.0", + }); + expect( + await file( + join( + packageDir, + "node_modules", + ".bun", + "@types+is-number@2.0.0", + "node_modules", + "@types", + "is-number", + "package.json", + ), + ).json(), + ).toEqual({ + name: "@types/is-number", + version: "2.0.0", + }); + }); +}); + +test("handles cyclic dependencies", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-cyclic", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "a-dep-b": "1.0.0", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect(readlinkSync(join(packageDir, "node_modules", "a-dep-b"))).toBe( + join(".bun", "a-dep-b@1.0.0", "node_modules", "a-dep-b"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "a-dep-b"))).toBe( + join("..", "a-dep-b@1.0.0", "node_modules", "a-dep-b"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "b-dep-a"))).toBe( + join("..", "b-dep-a@1.0.0", "node_modules", "b-dep-a"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "a-dep-b@1.0.0", "node_modules", "a-dep-b", "package.json"), + ).json(), + ).toEqual({ + name: "a-dep-b", + version: "1.0.0", + dependencies: { + "b-dep-a": "1.0.0", + }, + }); + + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "a-dep-b@1.0.0", "node_modules", "b-dep-a"))).toBe( + join("..", "..", "b-dep-a@1.0.0", "node_modules", "b-dep-a"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "a-dep-b@1.0.0", "node_modules", "b-dep-a", "package.json"), + ).json(), + ).toEqual({ + name: "b-dep-a", + version: "1.0.0", + dependencies: { + "a-dep-b": "1.0.0", + }, + }); +}); + +test("can install folder dependencies", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-folder-deps", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "folder-dep": "file:./pkg-1", + }, + }), + ); + + await write(join(packageDir, "pkg-1", "package.json"), JSON.stringify({ name: "folder-dep", version: "1.0.0" })); + + await runBunInstall(bunEnv, packageDir); + + expect(readlinkSync(join(packageDir, "node_modules", "folder-dep"))).toBe( + join(".bun", "folder-dep@file+pkg-1", "node_modules", "folder-dep"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "folder-dep@file+pkg-1", "node_modules", "folder-dep", "package.json"), + ).json(), + ).toEqual({ + name: "folder-dep", + version: "1.0.0", + }); + + await write(join(packageDir, "pkg-1", "index.js"), "module.exports = 'hello from pkg-1';"); + + await runBunInstall(bunEnv, packageDir, { savesLockfile: false }); + expect(readlinkSync(join(packageDir, "node_modules", "folder-dep"))).toBe( + join(".bun", "folder-dep@file+pkg-1", "node_modules", "folder-dep"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "folder-dep@file+pkg-1", "node_modules", "folder-dep", "index.js"), + ).text(), + ).toBe("module.exports = 'hello from pkg-1';"); +}); + +describe("isolated workspaces", () => { + test("basic", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "test-pkg-workspaces", + workspaces: { + nodeLinker: "isolated", + packages: ["pkg-1", "pkg-2"], + }, + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "pkg-1", "package.json"), + JSON.stringify({ + name: "pkg-1", + version: "1.0.0", + dependencies: { + "a-dep": "1.0.1", + "pkg-2": "workspace:", + "@types/is-number": "1.0.0", + }, + }), + ), + write( + join(packageDir, "pkg-2", "package.json"), + JSON.stringify({ + name: "pkg-2", + version: "1.0.0", + dependencies: { + "b-dep-a": "1.0.0", + }, + }), + ), + ]); + + await runBunInstall(bunEnv, packageDir); + + expect(existsSync(join(packageDir, "node_modules", "pkg-1"))).toBeFalse(); + expect(readlinkSync(join(packageDir, "pkg-1", "node_modules", "pkg-2"))).toBe(join("..", "..", "pkg-2")); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bun", "no-deps"]); + expect(readlinkSync(join(packageDir, "node_modules", "no-deps"))).toBe( + join(".bun", "no-deps@1.0.0", "node_modules", "no-deps"), + ); + + expect(await readdirSorted(join(packageDir, "pkg-1", "node_modules"))).toEqual(["@types", "a-dep", "pkg-2"]); + expect(await readdirSorted(join(packageDir, "pkg-2", "node_modules"))).toEqual(["b-dep-a"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bun"))).toEqual([ + "@types+is-number@1.0.0", + "a-dep-b@1.0.0", + "a-dep@1.0.1", + "b-dep-a@1.0.0", + "no-deps@1.0.0", + "node_modules", + ]); + + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "no-deps"))).toBe( + join("..", "no-deps@1.0.0", "node_modules", "no-deps"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "no-deps@1.0.0", "node_modules", "no-deps", "package.json"), + ).json(), + ).toEqual({ + name: "no-deps", + version: "1.0.0", + }); + }); +}); + +test("many transitive dependencies", async () => { + const { packageJson, packageDir } = await registry.createTestDir(); + + await write( + packageJson, + JSON.stringify({ + name: "test-pkg-many-transitive-deps", + workspaces: { + nodeLinker: "isolated", + }, + dependencies: { + "alias-loop-1": "1.0.0", + "alias-loop-2": "1.0.0", + "1-peer-dep-a": "1.0.0", + "basic-1": "1.0.0", + "is-number": "1.0.0", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + ".bun", + "1-peer-dep-a", + "alias-loop-1", + "alias-loop-2", + "basic-1", + "is-number", + ]); + expect(readlinkSync(join(packageDir, "node_modules", "alias-loop-1"))).toBe( + join(".bun", "alias-loop-1@1.0.0", "node_modules", "alias-loop-1"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "alias-loop-1"))).toBe( + join("..", "alias-loop-1@1.0.0", "node_modules", "alias-loop-1"), + ); + expect(readlinkSync(join(packageDir, "node_modules", ".bun", "node_modules", "alias-loop-2"))).toBe( + join("..", "alias-loop-2@1.0.0", "node_modules", "alias-loop-2"), + ); + expect( + await file( + join(packageDir, "node_modules", ".bun", "alias-loop-1@1.0.0", "node_modules", "alias-loop-1", "package.json"), + ).json(), + ).toEqual({ + name: "alias-loop-1", + version: "1.0.0", + dependencies: { + "alias1": "npm:alias-loop-2@*", + }, + }); + expect( + await file( + join(packageDir, "node_modules", ".bun", "alias-loop-2@1.0.0", "node_modules", "alias-loop-2", "package.json"), + ).json(), + ).toEqual({ + name: "alias-loop-2", + version: "1.0.0", + dependencies: { + "alias2": "npm:alias-loop-1@*", + }, + }); + // expect(await readdirSorted(join(packageDir, "node_modules", ".bun", "alias-loop-1@1.0.0", "node_modules"))).toEqual([ + // "alias1", + // "alias-loop-1", + // ]); + // expect(readlinkSync(join(packageDir, "node_modules", ".bun", "alias-loop-1@1.0.0", "node_modules", "alias1"))).toBe( + // join("..", "..", "alias-loop-2@1.0.0", "node_modules", "alias-loop-2"), + // ); + // expect(readlinkSync(join(packageDir, "node_modules", ".bun", "alias-loop-2@1.0.0", "node_modules", "alias2"))).toBe( + // join("..", "..", "alias-loop-1@1.0.0", "node_modules", "alias-loop-1"), + // ); +}); diff --git a/test/cli/install/registry/packages/a-dep-b/a-dep-b-1.0.0.tgz b/test/cli/install/registry/packages/a-dep-b/a-dep-b-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a97705bcbe47c14a47e60203db50f3497ea34d64 GIT binary patch literal 165 zcmV;W09yYaiwFP!00002|LxDs3c@fD1mK+e6id#wPRP%pZXzQSqb1(#@6|5kLFSw?Qp9QeN zlW))ciF-vPgFmxuN&0%=Sa))l%Nx&WoI>tZg9d9maO5y>=toYnr93B+-E-()w-wP+ TEutujqWD!0i|M`g00;m8uR%>* literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/a-dep-b/package.json b/test/cli/install/registry/packages/a-dep-b/package.json new file mode 100644 index 0000000000..6313607fcf --- /dev/null +++ b/test/cli/install/registry/packages/a-dep-b/package.json @@ -0,0 +1,44 @@ +{ + "name": "a-dep-b", + "versions": { + "1.0.0": { + "name": "a-dep-b", + "version": "1.0.0", + "dependencies": { + "b-dep-a": "1.0.0" + }, + "_id": "a-dep-b@1.0.0", + "_integrity": "sha512-PW1l4ruYaxcIw4rMkOVzb9zcR2srZhTPv2H2aH7QFc7vVxkD7EEMGHg1GPT8ycLFb8vriydUXEPwOy1FcbodaQ==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-PW1l4ruYaxcIw4rMkOVzb9zcR2srZhTPv2H2aH7QFc7vVxkD7EEMGHg1GPT8ycLFb8vriydUXEPwOy1FcbodaQ==", + "shasum": "ed69ada9bf7341ed905c41f1282bd87713cc315f", + "dist": { + "integrity": "sha512-PW1l4ruYaxcIw4rMkOVzb9zcR2srZhTPv2H2aH7QFc7vVxkD7EEMGHg1GPT8ycLFb8vriydUXEPwOy1FcbodaQ==", + "shasum": "ed69ada9bf7341ed905c41f1282bd87713cc315f", + "tarball": "http://http://localhost:4873/a-dep-b/-/a-dep-b-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-06-01T20:45:08.728Z", + "created": "2025-06-01T20:45:08.728Z", + "1.0.0": "2025-06-01T20:45:08.728Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "a-dep-b-1.0.0.tgz": { + "shasum": "ed69ada9bf7341ed905c41f1282bd87713cc315f", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "a-dep-b", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/b-dep-a/b-dep-a-1.0.0.tgz b/test/cli/install/registry/packages/b-dep-a/b-dep-a-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7dd885257cff9006e6b4804bea405244b0518ab0 GIT binary patch literal 165 zcmV;W09yYaiwFP!00002|LxDs3c@fD1mK+e6id#wPRP%pZXzQSqb1(#@6|5kLFSw?Qp9QeN zlW))ciF-vPgFmxuNp|(XvG#J8%Nx&WoI>tZg9d9maO5y>=toYnr93B+{yFSkw-wP+ TEutujqWD!08N(BQ00;m8#MDeY literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/b-dep-a/package.json b/test/cli/install/registry/packages/b-dep-a/package.json new file mode 100644 index 0000000000..349d09efc8 --- /dev/null +++ b/test/cli/install/registry/packages/b-dep-a/package.json @@ -0,0 +1,44 @@ +{ + "name": "b-dep-a", + "versions": { + "1.0.0": { + "name": "b-dep-a", + "version": "1.0.0", + "dependencies": { + "a-dep-b": "1.0.0" + }, + "_id": "b-dep-a@1.0.0", + "_integrity": "sha512-1owp4Wy5QE893BGgjDQGZm9Oayk38MA++fXmPTQA1WY/NFQv7CcCVpK2Ht/4mU4KejDeHOxaAj7qbzv1dSQA2w==", + "_nodeVersion": "22.6.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-1owp4Wy5QE893BGgjDQGZm9Oayk38MA++fXmPTQA1WY/NFQv7CcCVpK2Ht/4mU4KejDeHOxaAj7qbzv1dSQA2w==", + "shasum": "3d94682ad5231596f47745e03ef3d59af5945e1d", + "dist": { + "integrity": "sha512-1owp4Wy5QE893BGgjDQGZm9Oayk38MA++fXmPTQA1WY/NFQv7CcCVpK2Ht/4mU4KejDeHOxaAj7qbzv1dSQA2w==", + "shasum": "3d94682ad5231596f47745e03ef3d59af5945e1d", + "tarball": "http://http://localhost:4873/b-dep-a/-/b-dep-a-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-06-01T20:45:23.481Z", + "created": "2025-06-01T20:45:23.481Z", + "1.0.0": "2025-06-01T20:45:23.481Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "b-dep-a-1.0.0.tgz": { + "shasum": "3d94682ad5231596f47745e03ef3d59af5945e1d", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "b-dep-a", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/diff-peer-1/diff-peer-1-1.0.0.tgz b/test/cli/install/registry/packages/diff-peer-1/diff-peer-1-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6efe2d9c8067d03c98191e4877a15507781740f4 GIT binary patch literal 178 zcmV;j08RfNiwFP!00002|Lu>#3c@f9hI`&ql%DNO+1i0`Q`StU*p*ei$ll#_14ZyS z6rtbcFZq&?2HT$O9=Bo1@)!rtRt5k}UGq=%glZbjDs4b1V}SbTp>;*czQ8qJ>`14q z@JWI{^G~=JoC|NyD0W=9zUyQ_9HkPQByKpye(=eu5=vOA4g-3JzU?uRN-L;(uyLM! gZzeG>^n;|a_>$zNVq%$0CX@LM556MK3IGTI0QxsnWdHyG literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/diff-peer-1/package.json b/test/cli/install/registry/packages/diff-peer-1/package.json new file mode 100644 index 0000000000..3630c213f8 --- /dev/null +++ b/test/cli/install/registry/packages/diff-peer-1/package.json @@ -0,0 +1,42 @@ +{ + "name": "diff-peer-1", + "versions": { + "1.0.0": { + "name": "diff-peer-1", + "version": "1.0.0", + "dependencies": { + "has-peer": "1.0.0", + "peer-no-deps": "1.0.0" + }, + "_id": "diff-peer-1@1.0.0", + "_nodeVersion": "23.10.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-a9nTh3aUOE6VDmn23Q9v6JUqBGnsnSBGcZ7P5Qff+5YuJ3KhWd0rbY/+DLDpwO7zAsTzKP1Bs9KtWDwQHzocVA==", + "shasum": "2a72f1f0e12b5a7790c26cce6b0e018b47e06c90", + "tarball": "http://localhost:4873/diff-peer-1/-/diff-peer-1-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-06-08T19:48:23.111Z", + "created": "2025-06-08T19:48:23.111Z", + "1.0.0": "2025-06-08T19:48:23.111Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "diff-peer-1-1.0.0.tgz": { + "shasum": "2a72f1f0e12b5a7790c26cce6b0e018b47e06c90", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "diff-peer-1", + "readme": "ERROR: No README data found!" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/diff-peer-2/diff-peer-2-1.0.0.tgz b/test/cli/install/registry/packages/diff-peer-2/diff-peer-2-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..20e7f30b3f30e158c2b31f1ff8a6aef90611e748 GIT binary patch literal 180 zcmV;l089TLiwFP!00002|Lu>#3c@f9hI`&ql%DNO-PVC`Q`StU*p+p9k-fX=hJxU6 zC_=x>U-Bg(^`<$S18)72`6={{tqcI@vgDuY5mncm6_p01vh8gk104g>lDeLrHNn(ufu+E04G bz^U5jQ&i5?EQ+Eiil6cXvU0$J00;m8=gUs? literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/has-peer/package.json b/test/cli/install/registry/packages/has-peer/package.json new file mode 100644 index 0000000000..bd11b78261 --- /dev/null +++ b/test/cli/install/registry/packages/has-peer/package.json @@ -0,0 +1,41 @@ +{ + "name": "has-peer", + "versions": { + "1.0.0": { + "name": "has-peer", + "version": "1.0.0", + "peerDependencies": { + "peer-no-deps": "^1.0.0" + }, + "_id": "has-peer@1.0.0", + "_nodeVersion": "23.10.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-Q7Sg8KeLCUYEurarnoM/c31svn1IvmwYtkZ7DQdzJg4qzONeXs5u/q32iguDmzGS330ch/GnTiwnUVdhIuB8cQ==", + "shasum": "e0a4f8b2812eec8eada2aef68b71cdf572236702", + "tarball": "http://localhost:4873/has-peer/-/has-peer-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-06-08T19:49:59.426Z", + "created": "2025-06-08T19:49:59.426Z", + "1.0.0": "2025-06-08T19:49:59.426Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "has-peer-1.0.0.tgz": { + "shasum": "e0a4f8b2812eec8eada2aef68b71cdf572236702", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "has-peer", + "readme": "ERROR: No README data found!" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/peer-no-deps/package.json b/test/cli/install/registry/packages/peer-no-deps/package.json new file mode 100644 index 0000000000..844368ab96 --- /dev/null +++ b/test/cli/install/registry/packages/peer-no-deps/package.json @@ -0,0 +1,74 @@ +{ + "name": "peer-no-deps", + "versions": { + "1.0.0": { + "name": "peer-no-deps", + "version": "1.0.0", + "_id": "peer-no-deps@1.0.0", + "_nodeVersion": "23.10.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-SfaNgbuAdCAj30SPPmdUNQLMFYoQcBD2dS7cxyv+dutkDyCY/ZzxGwK2syEkzN7QuZNdXouiNRx43mdxC/YpfA==", + "shasum": "508a718b20f2e452919a86fc2add84c008f120d2", + "tarball": "http://localhost:4873/peer-no-deps/-/peer-no-deps-1.0.0.tgz" + }, + "contributors": [] + }, + "1.0.1": { + "name": "peer-no-deps", + "version": "1.0.1", + "_id": "peer-no-deps@1.0.1", + "_nodeVersion": "23.10.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-V/R/oUJEjX8GWwGs6Ayye+6alHjRj0eKkpDJPzywgUjTt0iQIaTDSRCgieMfHLgB1JSFs2ogyppAXX5cwQ7lWw==", + "shasum": "7f21c80e4f2ec05c453a73aa78f995e21d8008d1", + "tarball": "http://localhost:4873/peer-no-deps/-/peer-no-deps-1.0.1.tgz" + }, + "contributors": [] + }, + "2.0.0": { + "name": "peer-no-deps", + "version": "2.0.0", + "_id": "peer-no-deps@2.0.0", + "_nodeVersion": "23.10.0", + "_npmVersion": "10.9.2", + "dist": { + "integrity": "sha512-CR+AY66qH9+QUbKt7dxuH4iw36/mFIkpk1I8Lf+2DfucwGRcc0qwYswXQy+70jtz7ylHkmUMbhhgcMsIdsfK+w==", + "shasum": "5ae71b940adc2f9a1b346897183e7042591735c0", + "tarball": "http://localhost:4873/peer-no-deps/-/peer-no-deps-2.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-06-08T22:04:06.599Z", + "created": "2025-06-08T19:50:19.891Z", + "1.0.0": "2025-06-08T19:50:19.891Z", + "1.0.1": "2025-06-08T19:50:23.698Z", + "2.0.0": "2025-06-08T22:04:06.599Z" + }, + "users": {}, + "dist-tags": { + "latest": "2.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "peer-no-deps-1.0.0.tgz": { + "shasum": "508a718b20f2e452919a86fc2add84c008f120d2", + "version": "1.0.0" + }, + "peer-no-deps-1.0.1.tgz": { + "shasum": "7f21c80e4f2ec05c453a73aa78f995e21d8008d1", + "version": "1.0.1" + }, + "peer-no-deps-2.0.0.tgz": { + "shasum": "5ae71b940adc2f9a1b346897183e7042591735c0", + "version": "2.0.0" + } + }, + "_rev": "", + "_id": "peer-no-deps", + "readme": "ERROR: No README data found!" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/peer-no-deps/peer-no-deps-1.0.0.tgz b/test/cli/install/registry/packages/peer-no-deps/peer-no-deps-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5d240567bbfa795ead57737b88c6da180bd85b6a GIT binary patch literal 145 zcmV;C0B-*uiwFP!00002|LxDQ3IZ_<2H?*36d|)$Q@zXJ+Y}9=Vq03zMSOQ#1t-Tt zQ1UGa`7GTaWrim! zc-J3s>i{tLv)~3WiR3=SekZBmq9s?!GTaWrim! zc-J3s>i{tLv)~3WiR3=SekZBmq9s?!9e6d`BVWpm5Hw<#J##kRESMfUEt3Z6U; zLCJSX$e%+J-S*`6(#2P`#~i#_7!mO}!l(HY4`9|4Q{OXF*(=Rd_sM+0HD6pRr!4Sf z0q^=F?hOEXcNW|LMv>I|(C#E=T-D?zDIbEb;b6%UHnUMmDfI^)I$ab~00;m8SU^HB literal 0 HcmV?d00001 diff --git a/test/harness.ts b/test/harness.ts index 001d6b39e0..b6a0ad0a83 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1125,7 +1125,7 @@ export function tmpdirSync(pattern: string = "bun.test."): string { } export async function runBunInstall( - env: NodeJS.ProcessEnv, + env: NodeJS.Dict, cwd: string, options?: { allowWarnings?: boolean; @@ -1213,7 +1213,7 @@ export async function runBunUpdate( return { out: out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/), err, exitCode }; } -export async function pack(cwd: string, env: NodeJS.ProcessEnv, ...args: string[]) { +export async function pack(cwd: string, env: NodeJS.Dict, ...args: string[]) { const { stdout, stderr, exited } = Bun.spawn({ cmd: [bunExe(), "pm", "pack", ...args], cwd, @@ -1647,7 +1647,7 @@ export class VerdaccioRegistry { async writeBunfig(dir: string, opts: BunfigOpts = {}) { let bunfig = ` [install] - cache = "${join(dir, ".bun-cache")}" + cache = "${join(dir, ".bun-cache").replaceAll("\\", "\\\\")}" `; if ("saveTextLockfile" in opts) { bunfig += `saveTextLockfile = ${opts.saveTextLockfile} diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 1a78466ae7..9ef078e799 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -32,15 +32,15 @@ const words: Record "== alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "!= alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, - [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 243, regex: true }, + [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 242, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1866 }, - "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 179 }, - "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 }, + "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 170 }, + "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 102 }, "std.fs.File": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 62 }, ".stdFile()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 18 }, - ".stdDir()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 49 }, + ".stdDir()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 40 }, ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 280 }, "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 173 }, diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index e1a5bffacb..6275da6dc6 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -34,6 +34,7 @@ test/bundler/esbuild/css.test.ts test/bundler/esbuild/dce.test.ts test/bundler/esbuild/extra.test.ts test/bundler/esbuild/importstar.test.ts +test/cli/install/isolated-install.test.ts test/bundler/esbuild/importstar_ts.test.ts test/bundler/esbuild/loader.test.ts test/bundler/esbuild/lower.test.ts From 8b7888aeee877fcabdabe65ee6a2d20785b510b0 Mon Sep 17 00:00:00 2001 From: 190n Date: Wed, 9 Jul 2025 15:42:11 -0700 Subject: [PATCH 140/147] [publish images] upload encrypted core dumps from CI (#19189) Co-authored-by: 190n <7763597+190n@users.noreply.github.com> Co-authored-by: Ashcon Partovi --- .../bun-internal-test/src/runner.node.mjs | 2 +- scripts/bootstrap.sh | 84 +++++++++++++++++-- scripts/debug-coredump.ts | 63 ++++++++++++++ scripts/runner.node.mjs | 83 +++++++++++++++++- scripts/utils.mjs | 15 ++-- test/harness.ts | 2 +- 6 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 scripts/debug-coredump.ts diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index 161924ef5a..6fe978f53a 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -124,7 +124,7 @@ const argv0 = argv0_stdout.toString().trim(); console.log(`Testing ${argv0} v${revision}`); -const ntStatusPath = "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.22621.0\\shared\\ntstatus.h"; +const ntStatusPath = "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.26100.0\\shared\\ntstatus.h"; let ntstatus_header_cache = null; function lookupWindowsError(code) { if (ntstatus_header_cache === null) { diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 2ac531ef5f..99cd1c9ea7 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Version: 13 +# Version: 14 # A script that installs the dependencies needed to build and test Bun. # This should work on macOS and Linux with a POSIX shell. @@ -195,6 +195,17 @@ download_file() { print "$file_tmp_path" } +# path=$(download_and_verify_file URL sha256) +download_and_verify_file() { + file_url="$1" + hash="$2" + + path=$(download_file "$file_url") + execute sh -c 'echo "'"$hash $path"'" | sha256sum -c' >/dev/null 2>&1 + + print "$path" +} + append_to_profile() { content="$1" profiles=".profile .zprofile .bash_profile .bashrc .zshrc" @@ -400,7 +411,7 @@ check_package_manager() { pm="brew" ;; linux) - if [ -f "$(which apt)" ]; then + if [ -f "$(which apt-get)" ]; then pm="apt" elif [ -f "$(which dnf)" ]; then pm="dnf" @@ -470,10 +481,8 @@ check_ulimit() { print "Checking ulimits..." systemd_conf="/etc/systemd/system.conf" - if [ -f "$systemd_conf" ]; then - limits_conf="/etc/security/limits.d/99-unlimited.conf" - create_file "$limits_conf" - fi + limits_conf="/etc/security/limits.d/99-unlimited.conf" + create_file "$limits_conf" limits="core data fsize memlock nofile rss stack cpu nproc as locks sigpending msgqueue" for limit in $limits; do @@ -495,6 +504,10 @@ check_ulimit() { fi if [ -f "$systemd_conf" ]; then + # in systemd's configuration you need to say "infinity" when you mean "unlimited" + if [ "$limit_value" = "unlimited" ]; then + limit_value="infinity" + fi append_file "$systemd_conf" "DefaultLimit$limit_upper=$limit_value" fi done @@ -549,7 +562,7 @@ check_ulimit() { package_manager() { case "$pm" in apt) - execute_sudo apt "$@" + execute_sudo apt-get "$@" ;; dnf) case "$distro" in @@ -598,6 +611,7 @@ install_packages() { package_manager install \ --yes \ --no-install-recommends \ + --fix-missing \ "$@" ;; dnf) @@ -673,7 +687,7 @@ install_common_software() { esac case "$distro" in - amzn) + amzn | alpine) install_packages \ tar ;; @@ -1362,6 +1376,58 @@ install_chromium() { esac } +install_age() { + # we only use this to encrypt core dumps, which we only have on Linux + case "$os" in + linux) + age_tarball="" + case "$arch" in + x64) + age_tarball="$(download_and_verify_file https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz 7df45a6cc87d4da11cc03a539a7470c15b1041ab2b396af088fe9990f7c79d50)" + ;; + aarch64) + age_tarball="$(download_and_verify_file https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-arm64.tar.gz 57fd79a7ece5fe501f351b9dd51a82fbee1ea8db65a8839db17f5c080245e99f)" + ;; + esac + + age_extract_dir="$(create_tmp_directory)" + execute tar -C "$age_extract_dir" -zxf "$age_tarball" age/age + move_to_bin "$age_extract_dir/age/age" + ;; + esac +} + +configure_core_dumps() { + # we only have core dumps on Linux + case "$os" in + linux) + # set up a directory that the test runner will look in after running tests + cores_dir="/var/bun-cores-$distro-$release-$arch" + sysctl_file="/etc/sysctl.d/local.conf" + create_directory "$cores_dir" + # ensure core_pattern will point there + # %e = executable filename + # %p = pid + append_file "$sysctl_file" "kernel.core_pattern = $cores_dir/%e-%p.core" + + # disable apport.service if it exists since it will override the core_pattern + if which systemctl >/dev/null; then + if systemctl list-unit-files apport.service >/dev/null; then + execute_sudo "$systemctl" disable --now apport.service + fi + fi + + # load the new configuration + execute_sudo sysctl -p "$sysctl_file" + + # ensure that a regular user will be able to run sysctl + if [ -d /sbin ]; then + append_to_path /sbin + fi + ;; + esac +} + clean_system() { if ! [ "$ci" = "1" ]; then return @@ -1387,6 +1453,8 @@ main() { install_build_essentials install_chromium install_fuse_python + install_age + configure_core_dumps clean_system } diff --git a/scripts/debug-coredump.ts b/scripts/debug-coredump.ts new file mode 100644 index 0000000000..625afb727a --- /dev/null +++ b/scripts/debug-coredump.ts @@ -0,0 +1,63 @@ +import fs from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { parseArgs } from "node:util"; + +// usage: bun debug-coredump.ts +// -p (buildkite should show this) +// -b +// -c +// -d (default: lldb) +const { + values: { pid: stringPid, ["build-url"]: buildUrl, ["cores-url"]: coresUrl, debugger: debuggerPath }, +} = parseArgs({ + options: { + pid: { type: "string", short: "p" }, + ["build-url"]: { type: "string", short: "b" }, + ["cores-url"]: { type: "string", short: "c" }, + debugger: { type: "string", short: "d", default: "lldb" }, + }, +}); + +if (stringPid === undefined) throw new Error("no PID given"); +const pid = parseInt(stringPid); +if (buildUrl === undefined) throw new Error("no build-url given"); +if (coresUrl === undefined) throw new Error("no cores-url given"); +if (!process.env.AGE_CORES_IDENTITY?.startsWith("AGE-SECRET-KEY-")) + throw new Error("no identity given in $AGE_CORES_IDENTITY"); + +const id = Bun.hash(buildUrl + coresUrl).toString(36); +const dir = join(tmpdir(), `debug-coredump-${id}.tmp`); +fs.mkdirSync(dir, { recursive: true }); + +if (!fs.existsSync(join(dir, "bun-profile")) || !fs.existsSync(join(dir, `bun-${pid}.core`))) { + console.log("downloading bun-profile.zip"); + const zip = await (await fetch(buildUrl)).arrayBuffer(); + await Bun.write(join(dir, "bun-profile.zip"), zip); + // -j: junk paths (don't create directories when extracting) + // -o: overwrite without prompting + // -d: extract to this directory instead of cwd + await Bun.$`unzip -j -o ${join(dir, "bun-profile.zip")} -d ${dir}`; + + console.log("downloading cores"); + const cores = await (await fetch(coresUrl)).arrayBuffer(); + await Bun.$`bash -c ${`age -d -i <(echo "$AGE_CORES_IDENTITY")`} < ${cores} | tar -zxvC ${dir}`; + + console.log("moving cores out of nested directory"); + for await (const file of new Bun.Glob("bun-cores-*/bun-*.core").scan(dir)) { + fs.renameSync(join(dir, file), join(dir, basename(file))); + } +} else { + console.log(`already downloaded in ${dir}`); +} + +console.log("launching debugger:"); +console.log(`${debuggerPath} --core ${join(dir, `bun-${pid}.core`)} ${join(dir, "bun-profile")}`); + +const proc = await Bun.spawn([debuggerPath, "--core", join(dir, `bun-${pid}.core`), join(dir, "bun-profile")], { + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", +}); +await proc.exited; +process.exit(proc.exitCode); diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index bd24218397..8a320d5612 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -51,6 +51,7 @@ import { isBuildkite, isCI, isGithubAction, + isLinux, isMacOS, isWindows, isX64, @@ -59,6 +60,7 @@ import { startGroup, tmpdir, unzip, + uploadArtifact, } from "./utils.mjs"; let isQuiet = false; const cwd = import.meta.dirname ? dirname(import.meta.dirname) : process.cwd(); @@ -146,6 +148,10 @@ const { values: options, positionals: filters } = parseArgs({ type: "boolean", default: isBuildkite, }, + ["coredump-upload"]: { + type: "boolean", + default: isBuildkite && isLinux, + }, }, }); @@ -605,6 +611,78 @@ async function runTests() { } } + if (options["coredump-upload"]) { + try { + // this sysctl is set in bootstrap.sh to /var/bun-cores-$distro-$release-$arch + const sysctl = await spawnSafe({ command: "sysctl", args: ["-n", "kernel.core_pattern"] }); + let coresDir = sysctl.stdout; + if (sysctl.ok) { + if (coresDir.startsWith("|")) { + throw new Error("cores are being piped not saved"); + } + // change /foo/bar/%e-%p.core to /foo/bar + coresDir = dirname(sysctl.stdout); + } else { + throw new Error(`Failed to check core_pattern: ${sysctl.error}`); + } + + const coresDirBase = dirname(coresDir); + const coresDirName = basename(coresDir); + const coreFileNames = readdirSync(coresDir); + + if (coreFileNames.length > 0) { + console.log(`found ${coreFileNames.length} cores in ${coresDir}`); + let totalBytes = 0; + let totalBlocks = 0; + for (const f of coreFileNames) { + const stat = statSync(join(coresDir, f)); + totalBytes += stat.size; + totalBlocks += stat.blocks; + } + console.log(`total apparent size = ${totalBytes} bytes`); + console.log(`total size on disk = ${512 * totalBlocks} bytes`); + const outdir = mkdtempSync(join(tmpdir(), "cores-upload")); + const outfileName = `${coresDirName}.tar.gz.age`; + const outfileAbs = join(outdir, outfileName); + + // This matches an age identity known by Bun employees. Core dumps from CI have to be kept + // secret since they will contain API keys. + const ageRecipient = "age1eunsrgxwjjpzr48hm0y98cw2vn5zefjagt4r0qj4503jg2nxedqqkmz6fu"; // reject external PRs changing this, see above + + // Run tar in the parent directory of coresDir so that it creates archive entries with + // coresDirName in them. This way when you extract the tarball you get a folder named + // bun-cores-XYZ containing core files, instead of a bunch of core files strewn in your + // current directory + const before = Date.now(); + const zipAndEncrypt = await spawnSafe({ + command: "bash", + args: [ + "-c", + // tar -S: handle sparse files efficiently + `set -euo pipefail && tar -Sc "$0" | gzip -1 | age -e -r ${ageRecipient} -o "$1"`, + // $0 + coresDirName, + // $1 + outfileAbs, + ], + cwd: coresDirBase, + stdout: () => {}, + timeout: 60_000, + }); + const elapsed = Date.now() - before; + if (!zipAndEncrypt.ok) { + throw new Error(zipAndEncrypt.error); + } + console.log(`saved core dumps to ${outfileAbs} (${statSync(outfileAbs).size} bytes) in ${elapsed} ms`); + await uploadArtifact(outfileAbs); + } else { + console.log(`no cores found in ${coresDir}`); + } + } catch (err) { + console.error("Error collecting and uploading core dumps:", err); + } + } + if (!isCI && !isQuiet) { console.table({ "Total Tests": okResults.length + failedResults.length + flakyResults.length, @@ -780,6 +858,7 @@ async function spawnSafe(options) { const [, message] = error || []; error = message ? message.split("\n")[0].toLowerCase() : "crash"; error = error.indexOf("\\n") !== -1 ? error.substring(0, error.indexOf("\\n")) : error; + error = `pid ${subprocess.pid} ${error}`; } else if (signalCode) { if (signalCode === "SIGTERM" && duration >= timeout) { error = "timeout"; @@ -871,7 +950,7 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { }; if (basename(execPath).includes("asan")) { - bunEnv.ASAN_OPTIONS = "allow_user_segv_handler=1"; + bunEnv.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0"; } if (isWindows && bunEnv.Path) { @@ -1023,7 +1102,7 @@ function getTestTimeout(testPath) { if (/integration|3rd_party|docker|bun-install-registry|v8/i.test(testPath)) { return integrationTimeout; } - if (/napi/i.test(testPath)) { + if (/napi/i.test(testPath) || /v8/i.test(testPath)) { return napiTimeout; } return testTimeout; diff --git a/scripts/utils.mjs b/scripts/utils.mjs index 0f5a166644..83bcdc6dbc 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -16,7 +16,7 @@ import { } from "node:fs"; import { connect } from "node:net"; import { hostname, homedir as nodeHomedir, tmpdir as nodeTmpdir, release, userInfo } from "node:os"; -import { dirname, join, relative, resolve } from "node:path"; +import { basename, dirname, join, relative, resolve } from "node:path"; import { normalize as normalizeWindows } from "node:path/win32"; export const isWindows = process.platform === "win32"; @@ -1370,13 +1370,16 @@ export async function getLastSuccessfulBuild() { } /** - * @param {string} filename - * @param {string} [cwd] + * @param {string} filename Absolute path to file to upload */ -export async function uploadArtifact(filename, cwd) { +export async function uploadArtifact(filename) { if (isBuildkite) { - const relativePath = relative(cwd ?? process.cwd(), filename); - await spawnSafe(["buildkite-agent", "artifact", "upload", relativePath], { cwd, stdio: "inherit" }); + await spawnSafe(["buildkite-agent", "artifact", "upload", basename(filename)], { + cwd: dirname(filename), + stdio: "inherit", + }); + } else { + console.warn(`not in buildkite. artifact ${filename} not uploaded.`); } } diff --git a/test/harness.ts b/test/harness.ts index b6a0ad0a83..be7dc5b950 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -64,7 +64,7 @@ export const bunEnv: NodeJS.Dict = { const ciEnv = { ...bunEnv }; if (isASAN) { - bunEnv.ASAN_OPTIONS ??= "allow_user_segv_handler=1"; + bunEnv.ASAN_OPTIONS ??= "allow_user_segv_handler=1:disable_coredump=0"; } if (isWindows) { From 392acbee5a2f9b4bb197a0546f89066ed36393f4 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 9 Jul 2025 15:45:40 -0800 Subject: [PATCH 141/147] js: internal/validators: define simple validators in js (#20897) --- src/js/internal/validators.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index 4d91f4563c..f10b6c3052 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -65,12 +65,30 @@ function validateLinkHeaderValue(hints) { ); } +function validateString(value, name) { + if (typeof value !== "string") throw $ERR_INVALID_ARG_TYPE(name, "string", value); +} + +function validateFunction(value, name) { + if (typeof value !== "function") throw $ERR_INVALID_ARG_TYPE(name, "function", value); +} + +function validateBoolean(value, name) { + if (typeof value !== "boolean") throw $ERR_INVALID_ARG_TYPE(name, "boolean", value); +} + +function validateUndefined(value, name) { + if (value !== undefined) throw $ERR_INVALID_ARG_TYPE(name, "undefined", value); +} + function validateInternalField(object, fieldKey, className) { if (typeof object !== "object" || object === null || !ObjectPrototypeHasOwnProperty.$call(object, fieldKey)) { throw $ERR_INVALID_ARG_TYPE("this", className, object); } } + hideFromStack(validateLinkHeaderValue, validateInternalField); +hideFromStack(validateString, validateFunction, validateBoolean, validateUndefined); export default { /** (value, name) */ @@ -82,15 +100,15 @@ export default { /** `(value, name, min, max)` */ validateNumber: $newCppFunction("NodeValidator.cpp", "jsFunction_validateNumber", 0), /** `(value, name)` */ - validateString: $newCppFunction("NodeValidator.cpp", "jsFunction_validateString", 0), + validateString, /** `(number, name)` */ validateFiniteNumber: $newCppFunction("NodeValidator.cpp", "jsFunction_validateFiniteNumber", 0), /** `(number, name, lower, upper, def)` */ checkRangesOrGetDefault: $newCppFunction("NodeValidator.cpp", "jsFunction_checkRangesOrGetDefault", 0), /** `(value, name)` */ - validateFunction: $newCppFunction("NodeValidator.cpp", "jsFunction_validateFunction", 0), + validateFunction, /** `(value, name)` */ - validateBoolean: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBoolean", 0), + validateBoolean, /** `(port, name = 'Port', allowZero = true)` */ validatePort: $newCppFunction("NodeValidator.cpp", "jsFunction_validatePort", 0), /** `(signal, name)` */ @@ -108,7 +126,7 @@ export default { /** `(value, name)` */ validatePlainFunction: $newCppFunction("NodeValidator.cpp", "jsFunction_validatePlainFunction", 0), /** `(value, name)` */ - validateUndefined: $newCppFunction("NodeValidator.cpp", "jsFunction_validateUndefined", 0), + validateUndefined, /** `(buffer, name = 'buffer')` */ validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0), /** `(value, name, oneOf)` */ From 6e92f0b9cb2d0d4956d49c76a99e5da2fbb4cd5a Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 9 Jul 2025 19:22:57 -0700 Subject: [PATCH 142/147] make number smaller --- test/internal/ban-words.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 9ef078e799..5a06632661 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 242, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1866 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1865 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 170 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 102 }, From a1ef8f00acfb2915783d267a6e171b6350611276 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 9 Jul 2025 21:03:51 -0700 Subject: [PATCH 143/147] fix(install): clone arena memory on manifest parse errors (#20925) --- src/install/npm.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/install/npm.zig b/src/install/npm.zig index 4f0c652c91..b4fd767b1b 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -1572,7 +1572,14 @@ pub const PackageManifest = struct { source, log, arena.allocator(), - ) catch return null; + ) catch { + // don't use the arena memory! + var cloned_log: logger.Log = .init(bun.default_allocator); + try log.cloneToWithRecycled(&cloned_log, true); + log.* = cloned_log; + + return null; + }; if (json.asProperty("error")) |error_q| { if (error_q.expr.asString(allocator)) |err| { From 72b5c0885a4812de79c29c4c76d6e65bea1b6ab8 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 9 Jul 2025 22:02:43 -0800 Subject: [PATCH 144/147] test: fix process.test.js (#20932) --- test/js/node/process/process.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 8881891627..d8ef7c338a 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1144,6 +1144,6 @@ it.todoIf(isMacOS || isMusl)("should be the node version on the host that we exp }); let [out, exited] = await Promise.all([new Response(subprocess.stdout).text(), subprocess.exited]); - expect(out.trim()).toEqual("v24.3.0"); + expect(out.trim()).toEqual(isWindows ? "v24.3.0" : "v24.4.0"); // TODO: this *should* be v24.3.0 but scripts/bootstrap.sh needs to be enhanced to do so expect(exited).toBe(0); }); From 55a9cccac06403fe1486168378246b34a957f0a1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 10 Jul 2025 00:10:43 -0700 Subject: [PATCH 145/147] bun.sh -> bun.com (#20909) --- .github/ISSUE_TEMPLATE/2-bug-report.yml | 2 +- .../3-typescript-bug-report.yml | 74 +-- .github/workflows/release.yml | 2 +- CONTRIBUTING.md | 4 +- Makefile | 18 +- README.md | 588 +++++++++--------- SECURITY.md | 3 +- bench/expect-to-equal/README.md | 2 +- bench/snippets/dns-prefetch.mjs | 2 +- bench/websocket-server/README.md | 2 +- docs/api/binary-data.md | 2 +- docs/api/cc.md | 2 +- docs/api/dns.md | 6 +- docs/api/fetch.md | 10 +- docs/api/file-io.md | 6 +- docs/api/globals.md | 8 +- docs/api/http.md | 12 +- docs/api/s3.md | 2 +- docs/api/utils.md | 6 +- docs/bundler/fullstack.md | 4 +- docs/bundler/index.md | 16 +- docs/bundler/loaders.md | 10 +- docs/bundler/macros.md | 2 +- docs/bundler/plugins.md | 2 +- docs/bundler/vs-esbuild.md | 10 +- docs/cli/bun-create.md | 6 +- docs/cli/bun-install.md | 2 +- docs/cli/bun-upgrade.md | 2 +- docs/cli/filter.md | 4 +- docs/cli/install.md | 10 +- docs/cli/patch-commit.md | 2 +- docs/cli/run.md | 4 +- docs/cli/test.md | 12 +- docs/ecosystem/express.md | 6 +- docs/ecosystem/react.md | 2 +- docs/guides/binary/arraybuffer-to-array.md | 2 +- docs/guides/binary/arraybuffer-to-blob.md | 2 +- docs/guides/binary/arraybuffer-to-buffer.md | 2 +- docs/guides/binary/arraybuffer-to-string.md | 2 +- .../binary/arraybuffer-to-typedarray.md | 2 +- docs/guides/binary/blob-to-arraybuffer.md | 2 +- docs/guides/binary/blob-to-dataview.md | 2 +- docs/guides/binary/blob-to-stream.md | 2 +- docs/guides/binary/blob-to-string.md | 2 +- docs/guides/binary/blob-to-typedarray.md | 2 +- docs/guides/binary/buffer-to-arraybuffer.md | 2 +- docs/guides/binary/buffer-to-blob.md | 2 +- .../guides/binary/buffer-to-readablestream.md | 2 +- docs/guides/binary/buffer-to-string.md | 2 +- docs/guides/binary/buffer-to-typedarray.md | 2 +- docs/guides/binary/dataview-to-string.md | 2 +- .../binary/typedarray-to-arraybuffer.md | 2 +- docs/guides/binary/typedarray-to-blob.md | 2 +- docs/guides/binary/typedarray-to-buffer.md | 2 +- docs/guides/binary/typedarray-to-dataview.md | 2 +- .../binary/typedarray-to-readablestream.md | 2 +- docs/guides/binary/typedarray-to-string.md | 2 +- docs/guides/ecosystem/express.md | 2 +- docs/guides/ecosystem/neon-drizzle.md | 2 +- .../ecosystem/neon-serverless-postgres.md | 2 +- docs/guides/html-rewriter/extract-links.md | 8 +- .../html-rewriter/extract-social-meta.md | 6 +- docs/guides/http/fetch.md | 4 +- docs/guides/http/file-uploads.md | 2 +- docs/guides/http/hot.md | 2 +- docs/guides/http/server.md | 2 +- docs/guides/http/simple.md | 2 +- docs/guides/http/stream-file.md | 6 +- docs/guides/http/tls.md | 2 +- docs/guides/install/add-dev.md | 2 +- docs/guides/install/add-git.md | 2 +- docs/guides/install/add-optional.md | 2 +- docs/guides/install/add-peer.md | 2 +- docs/guides/install/add-tarball.md | 2 +- docs/guides/install/add.md | 2 +- docs/guides/install/azure-artifacts.md | 2 +- docs/guides/install/custom-registry.md | 4 +- docs/guides/install/git-diff-bun-lockfile.md | 2 +- docs/guides/install/npm-alias.md | 2 +- docs/guides/install/registry-scope.md | 6 +- docs/guides/install/trusted.md | 2 +- docs/guides/install/workspaces.md | 2 +- docs/guides/install/yarnlock.md | 4 +- docs/guides/process/ctrl-c.md | 2 +- docs/guides/process/ipc.md | 4 +- docs/guides/process/nanoseconds.md | 2 +- docs/guides/process/os-signals.md | 2 +- docs/guides/process/spawn-stderr.md | 4 +- docs/guides/process/spawn-stdout.md | 4 +- docs/guides/process/spawn.md | 4 +- docs/guides/process/stdin.md | 2 +- docs/guides/read-file/arraybuffer.md | 2 +- docs/guides/read-file/buffer.md | 2 +- docs/guides/read-file/exists.md | 2 +- docs/guides/read-file/mime.md | 2 +- docs/guides/read-file/stream.md | 2 +- docs/guides/read-file/uint8array.md | 2 +- docs/guides/read-file/watch.md | 2 +- docs/guides/runtime/delete-directory.md | 2 +- docs/guides/runtime/delete-file.md | 2 +- docs/guides/runtime/import-json.md | 2 +- docs/guides/runtime/import-toml.md | 2 +- docs/guides/runtime/read-env.md | 2 +- docs/guides/runtime/set-env.md | 2 +- docs/guides/runtime/shell.md | 2 +- docs/guides/runtime/tsconfig-paths.md | 2 +- docs/guides/runtime/typescript.md | 2 +- docs/guides/runtime/vscode-debugger.md | 2 +- docs/guides/streams/to-array.md | 2 +- docs/guides/streams/to-arraybuffer.md | 2 +- docs/guides/streams/to-blob.md | 2 +- docs/guides/streams/to-buffer.md | 2 +- docs/guides/streams/to-json.md | 2 +- docs/guides/streams/to-string.md | 2 +- docs/guides/streams/to-typedarray.md | 2 +- docs/guides/util/base64.md | 2 +- docs/guides/util/deep-equals.md | 6 +- docs/guides/util/deflate.md | 2 +- docs/guides/util/entrypoint.md | 4 +- docs/guides/util/escape-html.md | 2 +- docs/guides/util/file-url-to-path.md | 2 +- docs/guides/util/gzip.md | 2 +- docs/guides/util/hash-a-password.md | 2 +- docs/guides/util/import-meta-dir.md | 4 +- docs/guides/util/import-meta-file.md | 4 +- docs/guides/util/import-meta-path.md | 4 +- docs/guides/util/javascript-uuid.md | 2 +- docs/guides/util/main.md | 2 +- docs/guides/util/path-to-file-url.md | 2 +- docs/guides/util/sleep.md | 2 +- docs/guides/util/version.md | 2 +- .../util/which-path-to-executable-bin.md | 2 +- docs/guides/websocket/context.md | 2 +- docs/guides/websocket/simple.md | 2 +- docs/guides/write-file/basic.md | 4 +- docs/guides/write-file/blob.md | 4 +- docs/guides/write-file/cat.md | 4 +- docs/guides/write-file/file-cp.md | 4 +- docs/guides/write-file/filesink.md | 2 +- docs/guides/write-file/response.md | 6 +- docs/guides/write-file/stdout.md | 4 +- docs/guides/write-file/stream.md | 4 +- docs/guides/write-file/unlink.md | 2 +- docs/index.md | 2 +- docs/install/lockfile.md | 2 +- docs/install/npmrc.md | 8 +- docs/install/patch.md | 4 +- docs/install/registries.md | 2 +- docs/install/workspaces.md | 6 +- docs/installation.md | 14 +- docs/project/benchmarking.md | 2 +- docs/project/building-windows.md | 4 +- docs/quickstart.md | 2 +- docs/runtime/autoimport.md | 2 +- docs/runtime/bun-apis.md | 72 +-- docs/runtime/debugger.md | 2 +- docs/runtime/env.md | 2 +- docs/runtime/index.md | 38 +- docs/runtime/jsx.md | 2 +- docs/runtime/loaders.md | 6 +- docs/runtime/nodejs-apis.md | 8 +- docs/runtime/plugins.md | 10 +- docs/runtime/typescript.md | 2 +- examples/bun-hot-websockets.js | 2 +- packages/bun-error/index.tsx | 2 +- .../bun/__snapshots__/runner.test.ts.snap | 2 +- packages/bun-lambda/scripts/build-layer.ts | 2 +- packages/bun-plugin-svelte/README.md | 10 +- packages/bun-plugin-svelte/example/App.svelte | 21 +- packages/bun-plugin-svelte/example/index.html | 4 +- packages/bun-plugin-svelte/package.json | 2 +- .../npm/@oven/bun-darwin-aarch64/README.md | 2 +- .../@oven/bun-darwin-x64-baseline/README.md | 2 +- .../npm/@oven/bun-darwin-x64/README.md | 2 +- .../npm/@oven/bun-linux-aarch64/README.md | 2 +- .../@oven/bun-linux-x64-baseline/README.md | 2 +- .../npm/@oven/bun-linux-x64/README.md | 2 +- .../@oven/bun-windows-x64-baseline/README.md | 2 +- .../npm/@oven/bun-windows-x64/README.md | 2 +- packages/bun-release/npm/bun/README.md | 2 +- packages/bun-release/scripts/upload-npm.ts | 4 +- packages/bun-release/src/npm/install.ts | 2 +- packages/bun-types/README.md | 2 +- packages/bun-types/bun.d.ts | 18 +- packages/bun-types/devserver.d.ts | 6 +- packages/bun-types/package.json | 2 +- packages/bun-types/shell.d.ts | 4 +- packages/bun-vscode/README.md | 4 +- packages/bun-vscode/package.json | 2 +- scripts/nav2readme.ts | 4 +- src/bake/bake.zig | 2 +- src/bake/production.zig | 2 +- src/bun.js/api/server/ServerConfig.zig | 6 +- src/cli.zig | 16 +- src/cli/create_command.zig | 2 +- src/cli/discord_command.zig | 2 +- src/cli/init/README.default.md | 2 +- src/cli/init/README2.default.md | 2 +- src/cli/install.sh | 2 +- src/cli/package_manager_command.zig | 2 +- src/cli/pm_trusted_command.zig | 2 +- src/cli/pm_version_command.zig | 2 +- src/cli/run_command.zig | 2 +- src/cli/test_command.zig | 2 +- src/cli/upgrade_command.zig | 6 +- src/codegen/bindgen-lib.ts | 2 +- src/compile_target.zig | 2 +- .../REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html | 2 +- .../PackageManager/CommandLineArguments.zig | 26 +- .../PackageManager/PackageManagerEnqueue.zig | 2 +- src/install/windows-shim/bun_shim_impl.zig | 2 +- src/js/private.d.ts | 2 +- src/windows-app-info.rc | 2 +- test/js/bun/http/bun-serve-routes.test.ts | 6 +- 214 files changed, 749 insertions(+), 771 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yml b/.github/ISSUE_TEMPLATE/2-bug-report.yml index a0d51a2bb8..2767cee616 100644 --- a/.github/ISSUE_TEMPLATE/2-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/2-bug-report.yml @@ -12,7 +12,7 @@ body: If you need help or support using Bun, and are not reporting a bug, please join our [Discord](https://discord.gg/CXdq2DP29u) server, where you can ask questions in the [`#help`](https://discord.gg/32EtH6p7HN) forum. - Make sure you are running the [latest](https://bun.sh/docs/installation#upgrading) version of Bun. + Make sure you are running the [latest](https://bun.com/docs/installation#upgrading) version of Bun. The bug you are experiencing may already have been fixed. Please try to include as much information as possible. diff --git a/.github/ISSUE_TEMPLATE/3-typescript-bug-report.yml b/.github/ISSUE_TEMPLATE/3-typescript-bug-report.yml index 3913e25272..bb85f8e6be 100644 --- a/.github/ISSUE_TEMPLATE/3-typescript-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/3-typescript-bug-report.yml @@ -2,44 +2,44 @@ name: 🇹 TypeScript Type Bug Report description: Report an issue with TypeScript types labels: [bug, types] body: -- type: markdown - attributes: - value: | - Thank you for submitting a bug report. It helps make Bun better. + - type: markdown + attributes: + value: | + Thank you for submitting a bug report. It helps make Bun better. - If you need help or support using Bun, and are not reporting a bug, please - join our [Discord](https://discord.gg/CXdq2DP29u) server, where you can ask questions in the [`#help`](https://discord.gg/32EtH6p7HN) forum. + If you need help or support using Bun, and are not reporting a bug, please + join our [Discord](https://discord.gg/CXdq2DP29u) server, where you can ask questions in the [`#help`](https://discord.gg/32EtH6p7HN) forum. - Make sure you are running the [latest](https://bun.sh/docs/installation#upgrading) version of Bun. - The bug you are experiencing may already have been fixed. + Make sure you are running the [latest](https://bun.com/docs/installation#upgrading) version of Bun. + The bug you are experiencing may already have been fixed. - Please try to include as much information as possible. + Please try to include as much information as possible. -- type: input - attributes: - label: What version of Bun is running? - description: Copy the output of `bun --revision` -- type: input - attributes: - label: What platform is your computer? - description: | - For MacOS and Linux: copy the output of `uname -mprs` - For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console -- type: textarea - attributes: - label: What steps can reproduce the bug? - description: Explain the bug and provide a code snippet that can reproduce it. - validations: - required: true -- type: textarea - attributes: - label: What is the expected behavior? - description: If possible, please provide text instead of a screenshot. -- type: textarea - attributes: - label: What do you see instead? - description: If possible, please provide text instead of a screenshot. -- type: textarea - attributes: - label: Additional information - description: Is there anything else you think we should know? + - type: input + attributes: + label: What version of Bun is running? + description: Copy the output of `bun --revision` + - type: input + attributes: + label: What platform is your computer? + description: | + For MacOS and Linux: copy the output of `uname -mprs` + For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console + - type: textarea + attributes: + label: What steps can reproduce the bug? + description: Explain the bug and provide a code snippet that can reproduce it. + validations: + required: true + - type: textarea + attributes: + label: What is the expected behavior? + description: If possible, please provide text instead of a screenshot. + - type: textarea + attributes: + label: What do you see instead? + description: If possible, please provide text instead of a screenshot. + - type: textarea + attributes: + label: Additional information + description: Is there anything else you think we should know? diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a1f110a78..a3db1518f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -202,7 +202,7 @@ jobs: body: | Update `bun-types` version to ${{ steps.bun-version.outputs.BUN_VERSION }} - https://bun.sh/blog/${{ env.BUN_VERSION }} + https://bun.com/blog/${{ env.BUN_VERSION }} push-to-fork: oven-sh/DefinitelyTyped branch: ${{env.BUN_VERSION}} docker: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef45447465..f9799ba4ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts. -If you are using Windows, please refer to [this guide](https://bun.sh/docs/project/building-windows) +If you are using Windows, please refer to [this guide](https://bun.com/docs/project/building-windows) ## Install Dependencies @@ -37,7 +37,7 @@ Before starting, you will need to already have a release build of Bun installed, {% codetabs %} ```bash#Native -$ curl -fsSL https://bun.sh/install | bash +$ curl -fsSL https://bun.com/install | bash ``` ```bash#npm diff --git a/Makefile b/Makefile index 8826aa4ea7..91bb766afa 100644 --- a/Makefile +++ b/Makefile @@ -980,7 +980,7 @@ release-create-auto-updater: .PHONY: release-create release-create: gh release create --title "bun v$(PACKAGE_JSON_VERSION)" "$(BUN_BUILD_TAG)" - gh release create --repo=$(BUN_AUTO_UPDATER_REPO) --title "bun v$(PACKAGE_JSON_VERSION)" "$(BUN_BUILD_TAG)" -n "See https://github.com/oven-sh/bun/releases/tag/$(BUN_BUILD_TAG) for release notes. Using the install script or bun upgrade is the recommended way to install bun. Join bun's Discord to get access https://bun.sh/discord" + gh release create --repo=$(BUN_AUTO_UPDATER_REPO) --title "bun v$(PACKAGE_JSON_VERSION)" "$(BUN_BUILD_TAG)" -n "See https://github.com/oven-sh/bun/releases/tag/$(BUN_BUILD_TAG) for release notes. Using the install script or bun upgrade is the recommended way to install bun. Join bun's Discord to get access https://bun.com/discord" release-bin-entitlements: @@ -1977,7 +1977,7 @@ integration-test-dev: # to run integration tests USE_EXISTING_PROCESS=true TEST_SERVER_URL=http://localhost:3000 node test/scripts/browser.js copy-install: - cp src/cli/install.sh ../bun.sh/docs/install.html + cp src/cli/install.sh ../bun.com/docs/install.html copy-to-bun-release-dir: copy-to-bun-release-dir-bin copy-to-bun-release-dir-dsym @@ -2019,28 +2019,28 @@ vendor-dev: assert-deps submodule npm-install-dev vendor-without-npm .PHONY: bun bun: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' cpp: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' zig: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' dev: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' setup: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' bindings: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' help: @echo 'makefile is deprecated - use `cmake` / `bun run build`' - @echo 'See https://bun.sh/docs/project/contributing for more details' + @echo 'See https://bun.com/docs/project/contributing for more details' diff --git a/README.md b/README.md index d9d3f8e596..61733ac8e8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@

- Logo + Logo

Bun

- + stars Bun speed

- Documentation + Documentation   •   Discord   •   @@ -20,7 +20,7 @@
-### [Read the docs →](https://bun.sh/docs) +### [Read the docs →](https://bun.com/docs) ## What is Bun? @@ -47,14 +47,14 @@ Bun supports Linux (x64 & arm64), macOS (x64 & Apple Silicon) and Windows (x64). > **Linux users** — Kernel version 5.6 or higher is strongly recommended, but the minimum is 5.1. -> **x64 users** — if you see "illegal instruction" or similar errors, check our [CPU requirements](https://bun.sh/docs/installation#cpu-requirements-and-baseline-builds) +> **x64 users** — if you see "illegal instruction" or similar errors, check our [CPU requirements](https://bun.com/docs/installation#cpu-requirements-and-baseline-builds) ```sh # with install script (recommended) -curl -fsSL https://bun.sh/install | bash +curl -fsSL https://bun.com/install | bash # on windows -powershell -c "irm bun.sh/install.ps1 | iex" +powershell -c "irm bun.com/install.ps1 | iex" # with npm npm install -g bun @@ -87,351 +87,329 @@ bun upgrade --canary ## Quick links - Intro - - - [What is Bun?](https://bun.sh/docs/index) - - [Installation](https://bun.sh/docs/installation) - - [Quickstart](https://bun.sh/docs/quickstart) - - [TypeScript](https://bun.sh/docs/typescript) + - [What is Bun?](https://bun.com/docs/index) + - [Installation](https://bun.com/docs/installation) + - [Quickstart](https://bun.com/docs/quickstart) + - [TypeScript](https://bun.com/docs/typescript) - Templating - - - [`bun init`](https://bun.sh/docs/cli/init) - - [`bun create`](https://bun.sh/docs/cli/bun-create) + - [`bun init`](https://bun.com/docs/cli/init) + - [`bun create`](https://bun.com/docs/cli/bun-create) - CLI - - - [`bun upgrade`](https://bun.sh/docs/cli/bun-upgrade) + - [`bun upgrade`](https://bun.com/docs/cli/bun-upgrade) - Runtime - - - [`bun run`](https://bun.sh/docs/cli/run) - - [File types (Loaders)](https://bun.sh/docs/runtime/loaders) - - [TypeScript](https://bun.sh/docs/runtime/typescript) - - [JSX](https://bun.sh/docs/runtime/jsx) - - [Environment variables](https://bun.sh/docs/runtime/env) - - [Bun APIs](https://bun.sh/docs/runtime/bun-apis) - - [Web APIs](https://bun.sh/docs/runtime/web-apis) - - [Node.js compatibility](https://bun.sh/docs/runtime/nodejs-apis) - - [Single-file executable](https://bun.sh/docs/bundler/executables) - - [Plugins](https://bun.sh/docs/runtime/plugins) - - [Watch mode / Hot Reloading](https://bun.sh/docs/runtime/hot) - - [Module resolution](https://bun.sh/docs/runtime/modules) - - [Auto-install](https://bun.sh/docs/runtime/autoimport) - - [bunfig.toml](https://bun.sh/docs/runtime/bunfig) - - [Debugger](https://bun.sh/docs/runtime/debugger) - - [$ Shell](https://bun.sh/docs/runtime/shell) + - [`bun run`](https://bun.com/docs/cli/run) + - [File types (Loaders)](https://bun.com/docs/runtime/loaders) + - [TypeScript](https://bun.com/docs/runtime/typescript) + - [JSX](https://bun.com/docs/runtime/jsx) + - [Environment variables](https://bun.com/docs/runtime/env) + - [Bun APIs](https://bun.com/docs/runtime/bun-apis) + - [Web APIs](https://bun.com/docs/runtime/web-apis) + - [Node.js compatibility](https://bun.com/docs/runtime/nodejs-apis) + - [Single-file executable](https://bun.com/docs/bundler/executables) + - [Plugins](https://bun.com/docs/runtime/plugins) + - [Watch mode / Hot Reloading](https://bun.com/docs/runtime/hot) + - [Module resolution](https://bun.com/docs/runtime/modules) + - [Auto-install](https://bun.com/docs/runtime/autoimport) + - [bunfig.toml](https://bun.com/docs/runtime/bunfig) + - [Debugger](https://bun.com/docs/runtime/debugger) + - [$ Shell](https://bun.com/docs/runtime/shell) - Package manager - - - [`bun install`](https://bun.sh/docs/cli/install) - - [`bun add`](https://bun.sh/docs/cli/add) - - [`bun remove`](https://bun.sh/docs/cli/remove) - - [`bun update`](https://bun.sh/docs/cli/update) - - [`bun link`](https://bun.sh/docs/cli/link) - - [`bun unlink`](https://bun.sh/docs/cli/unlink) - - [`bun pm`](https://bun.sh/docs/cli/pm) - - [`bun outdated`](https://bun.sh/docs/cli/outdated) - - [`bun publish`](https://bun.sh/docs/cli/publish) - - [`bun patch`](https://bun.sh/docs/install/patch) - - [`bun patch-commit`](https://bun.sh/docs/cli/patch-commit) - - [Global cache](https://bun.sh/docs/install/cache) - - [Workspaces](https://bun.sh/docs/install/workspaces) - - [Lifecycle scripts](https://bun.sh/docs/install/lifecycle) - - [Filter](https://bun.sh/docs/cli/filter) - - [Lockfile](https://bun.sh/docs/install/lockfile) - - [Scopes and registries](https://bun.sh/docs/install/registries) - - [Overrides and resolutions](https://bun.sh/docs/install/overrides) - - [`.npmrc`](https://bun.sh/docs/install/npmrc) + - [`bun install`](https://bun.com/docs/cli/install) + - [`bun add`](https://bun.com/docs/cli/add) + - [`bun remove`](https://bun.com/docs/cli/remove) + - [`bun update`](https://bun.com/docs/cli/update) + - [`bun link`](https://bun.com/docs/cli/link) + - [`bun unlink`](https://bun.com/docs/cli/unlink) + - [`bun pm`](https://bun.com/docs/cli/pm) + - [`bun outdated`](https://bun.com/docs/cli/outdated) + - [`bun publish`](https://bun.com/docs/cli/publish) + - [`bun patch`](https://bun.com/docs/install/patch) + - [`bun patch-commit`](https://bun.com/docs/cli/patch-commit) + - [Global cache](https://bun.com/docs/install/cache) + - [Workspaces](https://bun.com/docs/install/workspaces) + - [Lifecycle scripts](https://bun.com/docs/install/lifecycle) + - [Filter](https://bun.com/docs/cli/filter) + - [Lockfile](https://bun.com/docs/install/lockfile) + - [Scopes and registries](https://bun.com/docs/install/registries) + - [Overrides and resolutions](https://bun.com/docs/install/overrides) + - [`.npmrc`](https://bun.com/docs/install/npmrc) - Bundler - - - [`Bun.build`](https://bun.sh/docs/bundler) - - [Loaders](https://bun.sh/docs/bundler/loaders) - - [Plugins](https://bun.sh/docs/bundler/plugins) - - [Macros](https://bun.sh/docs/bundler/macros) - - [vs esbuild](https://bun.sh/docs/bundler/vs-esbuild) - - [Single-file executable](https://bun.sh/docs/bundler/executables) - - [CSS](https://bun.sh/docs/bundler/css) - - [HTML](https://bun.sh/docs/bundler/html) - - [Hot Module Replacement (HMR)](https://bun.sh/docs/bundler/hmr) - - [Full-stack with HTML imports](https://bun.sh/docs/bundler/fullstack) + - [`Bun.build`](https://bun.com/docs/bundler) + - [Loaders](https://bun.com/docs/bundler/loaders) + - [Plugins](https://bun.com/docs/bundler/plugins) + - [Macros](https://bun.com/docs/bundler/macros) + - [vs esbuild](https://bun.com/docs/bundler/vs-esbuild) + - [Single-file executable](https://bun.com/docs/bundler/executables) + - [CSS](https://bun.com/docs/bundler/css) + - [HTML](https://bun.com/docs/bundler/html) + - [Hot Module Replacement (HMR)](https://bun.com/docs/bundler/hmr) + - [Full-stack with HTML imports](https://bun.com/docs/bundler/fullstack) - Test runner - - - [`bun test`](https://bun.sh/docs/cli/test) - - [Writing tests](https://bun.sh/docs/test/writing) - - [Watch mode](https://bun.sh/docs/test/hot) - - [Lifecycle hooks](https://bun.sh/docs/test/lifecycle) - - [Mocks](https://bun.sh/docs/test/mocks) - - [Snapshots](https://bun.sh/docs/test/snapshots) - - [Dates and times](https://bun.sh/docs/test/time) - - [DOM testing](https://bun.sh/docs/test/dom) - - [Code coverage](https://bun.sh/docs/test/coverage) - - [Configuration](https://bun.sh/docs/test/configuration) - - [Discovery](https://bun.sh/docs/test/discovery) - - [Reporters](https://bun.sh/docs/test/reporters) - - [Runtime Behavior](https://bun.sh/docs/test/runtime-behavior) + - [`bun test`](https://bun.com/docs/cli/test) + - [Writing tests](https://bun.com/docs/test/writing) + - [Watch mode](https://bun.com/docs/test/hot) + - [Lifecycle hooks](https://bun.com/docs/test/lifecycle) + - [Mocks](https://bun.com/docs/test/mocks) + - [Snapshots](https://bun.com/docs/test/snapshots) + - [Dates and times](https://bun.com/docs/test/time) + - [DOM testing](https://bun.com/docs/test/dom) + - [Code coverage](https://bun.com/docs/test/coverage) + - [Configuration](https://bun.com/docs/test/configuration) + - [Discovery](https://bun.com/docs/test/discovery) + - [Reporters](https://bun.com/docs/test/reporters) + - [Runtime Behavior](https://bun.com/docs/test/runtime-behavior) - Package runner - - - [`bunx`](https://bun.sh/docs/cli/bunx) + - [`bunx`](https://bun.com/docs/cli/bunx) - API - - - [HTTP server (`Bun.serve`)](https://bun.sh/docs/api/http) - - [WebSockets](https://bun.sh/docs/api/websockets) - - [Workers](https://bun.sh/docs/api/workers) - - [Binary data](https://bun.sh/docs/api/binary-data) - - [Streams](https://bun.sh/docs/api/streams) - - [File I/O (`Bun.file`)](https://bun.sh/docs/api/file-io) - - [import.meta](https://bun.sh/docs/api/import-meta) - - [SQLite (`bun:sqlite`)](https://bun.sh/docs/api/sqlite) - - [PostgreSQL (`Bun.sql`)](https://bun.sh/docs/api/sql) - - [Redis (`Bun.redis`)](https://bun.sh/docs/api/redis) - - [S3 Client (`Bun.s3`)](https://bun.sh/docs/api/s3) - - [FileSystemRouter](https://bun.sh/docs/api/file-system-router) - - [TCP sockets](https://bun.sh/docs/api/tcp) - - [UDP sockets](https://bun.sh/docs/api/udp) - - [Globals](https://bun.sh/docs/api/globals) - - [$ Shell](https://bun.sh/docs/runtime/shell) - - [Child processes (spawn)](https://bun.sh/docs/api/spawn) - - [Transpiler (`Bun.Transpiler`)](https://bun.sh/docs/api/transpiler) - - [Hashing](https://bun.sh/docs/api/hashing) - - [Colors (`Bun.color`)](https://bun.sh/docs/api/color) - - [Console](https://bun.sh/docs/api/console) - - [FFI (`bun:ffi`)](https://bun.sh/docs/api/ffi) - - [C Compiler (`bun:ffi` cc)](https://bun.sh/docs/api/cc) - - [HTMLRewriter](https://bun.sh/docs/api/html-rewriter) - - [Testing (`bun:test`)](https://bun.sh/docs/api/test) - - [Cookies (`Bun.Cookie`)](https://bun.sh/docs/api/cookie) - - [Utils](https://bun.sh/docs/api/utils) - - [Node-API](https://bun.sh/docs/api/node-api) - - [Glob (`Bun.Glob`)](https://bun.sh/docs/api/glob) - - [Semver (`Bun.semver`)](https://bun.sh/docs/api/semver) - - [DNS](https://bun.sh/docs/api/dns) - - [fetch API extensions](https://bun.sh/docs/api/fetch) + - [HTTP server (`Bun.serve`)](https://bun.com/docs/api/http) + - [WebSockets](https://bun.com/docs/api/websockets) + - [Workers](https://bun.com/docs/api/workers) + - [Binary data](https://bun.com/docs/api/binary-data) + - [Streams](https://bun.com/docs/api/streams) + - [File I/O (`Bun.file`)](https://bun.com/docs/api/file-io) + - [import.meta](https://bun.com/docs/api/import-meta) + - [SQLite (`bun:sqlite`)](https://bun.com/docs/api/sqlite) + - [PostgreSQL (`Bun.sql`)](https://bun.com/docs/api/sql) + - [Redis (`Bun.redis`)](https://bun.com/docs/api/redis) + - [S3 Client (`Bun.s3`)](https://bun.com/docs/api/s3) + - [FileSystemRouter](https://bun.com/docs/api/file-system-router) + - [TCP sockets](https://bun.com/docs/api/tcp) + - [UDP sockets](https://bun.com/docs/api/udp) + - [Globals](https://bun.com/docs/api/globals) + - [$ Shell](https://bun.com/docs/runtime/shell) + - [Child processes (spawn)](https://bun.com/docs/api/spawn) + - [Transpiler (`Bun.Transpiler`)](https://bun.com/docs/api/transpiler) + - [Hashing](https://bun.com/docs/api/hashing) + - [Colors (`Bun.color`)](https://bun.com/docs/api/color) + - [Console](https://bun.com/docs/api/console) + - [FFI (`bun:ffi`)](https://bun.com/docs/api/ffi) + - [C Compiler (`bun:ffi` cc)](https://bun.com/docs/api/cc) + - [HTMLRewriter](https://bun.com/docs/api/html-rewriter) + - [Testing (`bun:test`)](https://bun.com/docs/api/test) + - [Cookies (`Bun.Cookie`)](https://bun.com/docs/api/cookie) + - [Utils](https://bun.com/docs/api/utils) + - [Node-API](https://bun.com/docs/api/node-api) + - [Glob (`Bun.Glob`)](https://bun.com/docs/api/glob) + - [Semver (`Bun.semver`)](https://bun.com/docs/api/semver) + - [DNS](https://bun.com/docs/api/dns) + - [fetch API extensions](https://bun.com/docs/api/fetch) ## Guides - Binary - - - [Convert a Blob to a string](https://bun.sh/guides/binary/blob-to-string) - - [Convert a Buffer to a blob](https://bun.sh/guides/binary/buffer-to-blob) - - [Convert a Blob to a DataView](https://bun.sh/guides/binary/blob-to-dataview) - - [Convert a Buffer to a string](https://bun.sh/guides/binary/buffer-to-string) - - [Convert a Blob to a ReadableStream](https://bun.sh/guides/binary/blob-to-stream) - - [Convert a Blob to a Uint8Array](https://bun.sh/guides/binary/blob-to-typedarray) - - [Convert a DataView to a string](https://bun.sh/guides/binary/dataview-to-string) - - [Convert a Uint8Array to a Blob](https://bun.sh/guides/binary/typedarray-to-blob) - - [Convert a Blob to an ArrayBuffer](https://bun.sh/guides/binary/blob-to-arraybuffer) - - [Convert an ArrayBuffer to a Blob](https://bun.sh/guides/binary/arraybuffer-to-blob) - - [Convert a Buffer to a Uint8Array](https://bun.sh/guides/binary/buffer-to-typedarray) - - [Convert a Uint8Array to a Buffer](https://bun.sh/guides/binary/typedarray-to-buffer) - - [Convert a Uint8Array to a string](https://bun.sh/guides/binary/typedarray-to-string) - - [Convert a Buffer to an ArrayBuffer](https://bun.sh/guides/binary/buffer-to-arraybuffer) - - [Convert an ArrayBuffer to a Buffer](https://bun.sh/guides/binary/arraybuffer-to-buffer) - - [Convert an ArrayBuffer to a string](https://bun.sh/guides/binary/arraybuffer-to-string) - - [Convert a Uint8Array to a DataView](https://bun.sh/guides/binary/typedarray-to-dataview) - - [Convert a Buffer to a ReadableStream](https://bun.sh/guides/binary/buffer-to-readablestream) - - [Convert a Uint8Array to an ArrayBuffer](https://bun.sh/guides/binary/typedarray-to-arraybuffer) - - [Convert an ArrayBuffer to a Uint8Array](https://bun.sh/guides/binary/arraybuffer-to-typedarray) - - [Convert an ArrayBuffer to an array of numbers](https://bun.sh/guides/binary/arraybuffer-to-array) - - [Convert a Uint8Array to a ReadableStream](https://bun.sh/guides/binary/typedarray-to-readablestream) + - [Convert a Blob to a string](https://bun.com/guides/binary/blob-to-string) + - [Convert a Buffer to a blob](https://bun.com/guides/binary/buffer-to-blob) + - [Convert a Blob to a DataView](https://bun.com/guides/binary/blob-to-dataview) + - [Convert a Buffer to a string](https://bun.com/guides/binary/buffer-to-string) + - [Convert a Blob to a ReadableStream](https://bun.com/guides/binary/blob-to-stream) + - [Convert a Blob to a Uint8Array](https://bun.com/guides/binary/blob-to-typedarray) + - [Convert a DataView to a string](https://bun.com/guides/binary/dataview-to-string) + - [Convert a Uint8Array to a Blob](https://bun.com/guides/binary/typedarray-to-blob) + - [Convert a Blob to an ArrayBuffer](https://bun.com/guides/binary/blob-to-arraybuffer) + - [Convert an ArrayBuffer to a Blob](https://bun.com/guides/binary/arraybuffer-to-blob) + - [Convert a Buffer to a Uint8Array](https://bun.com/guides/binary/buffer-to-typedarray) + - [Convert a Uint8Array to a Buffer](https://bun.com/guides/binary/typedarray-to-buffer) + - [Convert a Uint8Array to a string](https://bun.com/guides/binary/typedarray-to-string) + - [Convert a Buffer to an ArrayBuffer](https://bun.com/guides/binary/buffer-to-arraybuffer) + - [Convert an ArrayBuffer to a Buffer](https://bun.com/guides/binary/arraybuffer-to-buffer) + - [Convert an ArrayBuffer to a string](https://bun.com/guides/binary/arraybuffer-to-string) + - [Convert a Uint8Array to a DataView](https://bun.com/guides/binary/typedarray-to-dataview) + - [Convert a Buffer to a ReadableStream](https://bun.com/guides/binary/buffer-to-readablestream) + - [Convert a Uint8Array to an ArrayBuffer](https://bun.com/guides/binary/typedarray-to-arraybuffer) + - [Convert an ArrayBuffer to a Uint8Array](https://bun.com/guides/binary/arraybuffer-to-typedarray) + - [Convert an ArrayBuffer to an array of numbers](https://bun.com/guides/binary/arraybuffer-to-array) + - [Convert a Uint8Array to a ReadableStream](https://bun.com/guides/binary/typedarray-to-readablestream) - Ecosystem - - - [Use React and JSX](https://bun.sh/guides/ecosystem/react) - - [Use EdgeDB with Bun](https://bun.sh/guides/ecosystem/edgedb) - - [Use Prisma with Bun](https://bun.sh/guides/ecosystem/prisma) - - [Add Sentry to a Bun app](https://bun.sh/guides/ecosystem/sentry) - - [Create a Discord bot](https://bun.sh/guides/ecosystem/discordjs) - - [Run Bun as a daemon with PM2](https://bun.sh/guides/ecosystem/pm2) - - [Use Drizzle ORM with Bun](https://bun.sh/guides/ecosystem/drizzle) - - [Build an app with Nuxt and Bun](https://bun.sh/guides/ecosystem/nuxt) - - [Build an app with Qwik and Bun](https://bun.sh/guides/ecosystem/qwik) - - [Build an app with Astro and Bun](https://bun.sh/guides/ecosystem/astro) - - [Build an app with Remix and Bun](https://bun.sh/guides/ecosystem/remix) - - [Build a frontend using Vite and Bun](https://bun.sh/guides/ecosystem/vite) - - [Build an app with Next.js and Bun](https://bun.sh/guides/ecosystem/nextjs) - - [Run Bun as a daemon with systemd](https://bun.sh/guides/ecosystem/systemd) - - [Deploy a Bun application on Render](https://bun.sh/guides/ecosystem/render) - - [Build an HTTP server using Hono and Bun](https://bun.sh/guides/ecosystem/hono) - - [Build an app with SvelteKit and Bun](https://bun.sh/guides/ecosystem/sveltekit) - - [Build an app with SolidStart and Bun](https://bun.sh/guides/ecosystem/solidstart) - - [Build an HTTP server using Elysia and Bun](https://bun.sh/guides/ecosystem/elysia) - - [Build an HTTP server using StricJS and Bun](https://bun.sh/guides/ecosystem/stric) - - [Containerize a Bun application with Docker](https://bun.sh/guides/ecosystem/docker) - - [Build an HTTP server using Express and Bun](https://bun.sh/guides/ecosystem/express) - - [Use Neon Postgres through Drizzle ORM](https://bun.sh/guides/ecosystem/neon-drizzle) - - [Server-side render (SSR) a React component](https://bun.sh/guides/ecosystem/ssr-react) - - [Read and write data to MongoDB using Mongoose and Bun](https://bun.sh/guides/ecosystem/mongoose) - - [Use Neon's Serverless Postgres with Bun](https://bun.sh/guides/ecosystem/neon-serverless-postgres) + - [Use React and JSX](https://bun.com/guides/ecosystem/react) + - [Use EdgeDB with Bun](https://bun.com/guides/ecosystem/edgedb) + - [Use Prisma with Bun](https://bun.com/guides/ecosystem/prisma) + - [Add Sentry to a Bun app](https://bun.com/guides/ecosystem/sentry) + - [Create a Discord bot](https://bun.com/guides/ecosystem/discordjs) + - [Run Bun as a daemon with PM2](https://bun.com/guides/ecosystem/pm2) + - [Use Drizzle ORM with Bun](https://bun.com/guides/ecosystem/drizzle) + - [Build an app with Nuxt and Bun](https://bun.com/guides/ecosystem/nuxt) + - [Build an app with Qwik and Bun](https://bun.com/guides/ecosystem/qwik) + - [Build an app with Astro and Bun](https://bun.com/guides/ecosystem/astro) + - [Build an app with Remix and Bun](https://bun.com/guides/ecosystem/remix) + - [Build a frontend using Vite and Bun](https://bun.com/guides/ecosystem/vite) + - [Build an app with Next.js and Bun](https://bun.com/guides/ecosystem/nextjs) + - [Run Bun as a daemon with systemd](https://bun.com/guides/ecosystem/systemd) + - [Deploy a Bun application on Render](https://bun.com/guides/ecosystem/render) + - [Build an HTTP server using Hono and Bun](https://bun.com/guides/ecosystem/hono) + - [Build an app with SvelteKit and Bun](https://bun.com/guides/ecosystem/sveltekit) + - [Build an app with SolidStart and Bun](https://bun.com/guides/ecosystem/solidstart) + - [Build an HTTP server using Elysia and Bun](https://bun.com/guides/ecosystem/elysia) + - [Build an HTTP server using StricJS and Bun](https://bun.com/guides/ecosystem/stric) + - [Containerize a Bun application with Docker](https://bun.com/guides/ecosystem/docker) + - [Build an HTTP server using Express and Bun](https://bun.com/guides/ecosystem/express) + - [Use Neon Postgres through Drizzle ORM](https://bun.com/guides/ecosystem/neon-drizzle) + - [Server-side render (SSR) a React component](https://bun.com/guides/ecosystem/ssr-react) + - [Read and write data to MongoDB using Mongoose and Bun](https://bun.com/guides/ecosystem/mongoose) + - [Use Neon's Serverless Postgres with Bun](https://bun.com/guides/ecosystem/neon-serverless-postgres) - HTMLRewriter - - - [Extract links from a webpage using HTMLRewriter](https://bun.sh/guides/html-rewriter/extract-links) - - [Extract social share images and Open Graph tags](https://bun.sh/guides/html-rewriter/extract-social-meta) + - [Extract links from a webpage using HTMLRewriter](https://bun.com/guides/html-rewriter/extract-links) + - [Extract social share images and Open Graph tags](https://bun.com/guides/html-rewriter/extract-social-meta) - HTTP - - - [Hot reload an HTTP server](https://bun.sh/guides/http/hot) - - [Common HTTP server usage](https://bun.sh/guides/http/server) - - [Write a simple HTTP server](https://bun.sh/guides/http/simple) - - [Configure TLS on an HTTP server](https://bun.sh/guides/http/tls) - - [Send an HTTP request using fetch](https://bun.sh/guides/http/fetch) - - [Proxy HTTP requests using fetch()](https://bun.sh/guides/http/proxy) - - [Start a cluster of HTTP servers](https://bun.sh/guides/http/cluster) - - [Stream a file as an HTTP Response](https://bun.sh/guides/http/stream-file) - - [fetch with unix domain sockets in Bun](https://bun.sh/guides/http/fetch-unix) - - [Upload files via HTTP using FormData](https://bun.sh/guides/http/file-uploads) - - [Streaming HTTP Server with Async Iterators](https://bun.sh/guides/http/stream-iterator) - - [Streaming HTTP Server with Node.js Streams](https://bun.sh/guides/http/stream-node-streams-in-bun) + - [Hot reload an HTTP server](https://bun.com/guides/http/hot) + - [Common HTTP server usage](https://bun.com/guides/http/server) + - [Write a simple HTTP server](https://bun.com/guides/http/simple) + - [Configure TLS on an HTTP server](https://bun.com/guides/http/tls) + - [Send an HTTP request using fetch](https://bun.com/guides/http/fetch) + - [Proxy HTTP requests using fetch()](https://bun.com/guides/http/proxy) + - [Start a cluster of HTTP servers](https://bun.com/guides/http/cluster) + - [Stream a file as an HTTP Response](https://bun.com/guides/http/stream-file) + - [fetch with unix domain sockets in Bun](https://bun.com/guides/http/fetch-unix) + - [Upload files via HTTP using FormData](https://bun.com/guides/http/file-uploads) + - [Streaming HTTP Server with Async Iterators](https://bun.com/guides/http/stream-iterator) + - [Streaming HTTP Server with Node.js Streams](https://bun.com/guides/http/stream-node-streams-in-bun) - Install - - - [Add a dependency](https://bun.sh/guides/install/add) - - [Add a Git dependency](https://bun.sh/guides/install/add-git) - - [Add a peer dependency](https://bun.sh/guides/install/add-peer) - - [Add a trusted dependency](https://bun.sh/guides/install/trusted) - - [Add a development dependency](https://bun.sh/guides/install/add-dev) - - [Add a tarball dependency](https://bun.sh/guides/install/add-tarball) - - [Add an optional dependency](https://bun.sh/guides/install/add-optional) - - [Generate a yarn-compatible lockfile](https://bun.sh/guides/install/yarnlock) - - [Configuring a monorepo using workspaces](https://bun.sh/guides/install/workspaces) - - [Install a package under a different name](https://bun.sh/guides/install/npm-alias) - - [Install dependencies with Bun in GitHub Actions](https://bun.sh/guides/install/cicd) - - [Using bun install with Artifactory](https://bun.sh/guides/install/jfrog-artifactory) - - [Configure git to diff Bun's lockb lockfile](https://bun.sh/guides/install/git-diff-bun-lockfile) - - [Override the default npm registry for bun install](https://bun.sh/guides/install/custom-registry) - - [Using bun install with an Azure Artifacts npm registry](https://bun.sh/guides/install/azure-artifacts) - - [Migrate from npm install to bun install](https://bun.sh/guides/install/from-npm-install-to-bun-install) - - [Configure a private registry for an organization scope with bun install](https://bun.sh/guides/install/registry-scope) + - [Add a dependency](https://bun.com/guides/install/add) + - [Add a Git dependency](https://bun.com/guides/install/add-git) + - [Add a peer dependency](https://bun.com/guides/install/add-peer) + - [Add a trusted dependency](https://bun.com/guides/install/trusted) + - [Add a development dependency](https://bun.com/guides/install/add-dev) + - [Add a tarball dependency](https://bun.com/guides/install/add-tarball) + - [Add an optional dependency](https://bun.com/guides/install/add-optional) + - [Generate a yarn-compatible lockfile](https://bun.com/guides/install/yarnlock) + - [Configuring a monorepo using workspaces](https://bun.com/guides/install/workspaces) + - [Install a package under a different name](https://bun.com/guides/install/npm-alias) + - [Install dependencies with Bun in GitHub Actions](https://bun.com/guides/install/cicd) + - [Using bun install with Artifactory](https://bun.com/guides/install/jfrog-artifactory) + - [Configure git to diff Bun's lockb lockfile](https://bun.com/guides/install/git-diff-bun-lockfile) + - [Override the default npm registry for bun install](https://bun.com/guides/install/custom-registry) + - [Using bun install with an Azure Artifacts npm registry](https://bun.com/guides/install/azure-artifacts) + - [Migrate from npm install to bun install](https://bun.com/guides/install/from-npm-install-to-bun-install) + - [Configure a private registry for an organization scope with bun install](https://bun.com/guides/install/registry-scope) - Process - - - [Read from stdin](https://bun.sh/guides/process/stdin) - - [Listen for CTRL+C](https://bun.sh/guides/process/ctrl-c) - - [Spawn a child process](https://bun.sh/guides/process/spawn) - - [Listen to OS signals](https://bun.sh/guides/process/os-signals) - - [Parse command-line arguments](https://bun.sh/guides/process/argv) - - [Read stderr from a child process](https://bun.sh/guides/process/spawn-stderr) - - [Read stdout from a child process](https://bun.sh/guides/process/spawn-stdout) - - [Get the process uptime in nanoseconds](https://bun.sh/guides/process/nanoseconds) - - [Spawn a child process and communicate using IPC](https://bun.sh/guides/process/ipc) + - [Read from stdin](https://bun.com/guides/process/stdin) + - [Listen for CTRL+C](https://bun.com/guides/process/ctrl-c) + - [Spawn a child process](https://bun.com/guides/process/spawn) + - [Listen to OS signals](https://bun.com/guides/process/os-signals) + - [Parse command-line arguments](https://bun.com/guides/process/argv) + - [Read stderr from a child process](https://bun.com/guides/process/spawn-stderr) + - [Read stdout from a child process](https://bun.com/guides/process/spawn-stdout) + - [Get the process uptime in nanoseconds](https://bun.com/guides/process/nanoseconds) + - [Spawn a child process and communicate using IPC](https://bun.com/guides/process/ipc) - Read file - - - [Read a JSON file](https://bun.sh/guides/read-file/json) - - [Check if a file exists](https://bun.sh/guides/read-file/exists) - - [Read a file as a string](https://bun.sh/guides/read-file/string) - - [Read a file to a Buffer](https://bun.sh/guides/read-file/buffer) - - [Get the MIME type of a file](https://bun.sh/guides/read-file/mime) - - [Watch a directory for changes](https://bun.sh/guides/read-file/watch) - - [Read a file as a ReadableStream](https://bun.sh/guides/read-file/stream) - - [Read a file to a Uint8Array](https://bun.sh/guides/read-file/uint8array) - - [Read a file to an ArrayBuffer](https://bun.sh/guides/read-file/arraybuffer) + - [Read a JSON file](https://bun.com/guides/read-file/json) + - [Check if a file exists](https://bun.com/guides/read-file/exists) + - [Read a file as a string](https://bun.com/guides/read-file/string) + - [Read a file to a Buffer](https://bun.com/guides/read-file/buffer) + - [Get the MIME type of a file](https://bun.com/guides/read-file/mime) + - [Watch a directory for changes](https://bun.com/guides/read-file/watch) + - [Read a file as a ReadableStream](https://bun.com/guides/read-file/stream) + - [Read a file to a Uint8Array](https://bun.com/guides/read-file/uint8array) + - [Read a file to an ArrayBuffer](https://bun.com/guides/read-file/arraybuffer) - Runtime - - - [Delete files](https://bun.sh/guides/runtime/delete-file) - - [Run a Shell Command](https://bun.sh/guides/runtime/shell) - - [Import a JSON file](https://bun.sh/guides/runtime/import-json) - - [Import a TOML file](https://bun.sh/guides/runtime/import-toml) - - [Set a time zone in Bun](https://bun.sh/guides/runtime/timezone) - - [Set environment variables](https://bun.sh/guides/runtime/set-env) - - [Re-map import paths](https://bun.sh/guides/runtime/tsconfig-paths) - - [Delete directories](https://bun.sh/guides/runtime/delete-directory) - - [Read environment variables](https://bun.sh/guides/runtime/read-env) - - [Import a HTML file as text](https://bun.sh/guides/runtime/import-html) - - [Install and run Bun in GitHub Actions](https://bun.sh/guides/runtime/cicd) - - [Debugging Bun with the web debugger](https://bun.sh/guides/runtime/web-debugger) - - [Install TypeScript declarations for Bun](https://bun.sh/guides/runtime/typescript) - - [Debugging Bun with the VS Code extension](https://bun.sh/guides/runtime/vscode-debugger) - - [Inspect memory usage using V8 heap snapshots](https://bun.sh/guides/runtime/heap-snapshot) - - [Define and replace static globals & constants](https://bun.sh/guides/runtime/define-constant) - - [Codesign a single-file JavaScript executable on macOS](https://bun.sh/guides/runtime/codesign-macos-executable) + - [Delete files](https://bun.com/guides/runtime/delete-file) + - [Run a Shell Command](https://bun.com/guides/runtime/shell) + - [Import a JSON file](https://bun.com/guides/runtime/import-json) + - [Import a TOML file](https://bun.com/guides/runtime/import-toml) + - [Set a time zone in Bun](https://bun.com/guides/runtime/timezone) + - [Set environment variables](https://bun.com/guides/runtime/set-env) + - [Re-map import paths](https://bun.com/guides/runtime/tsconfig-paths) + - [Delete directories](https://bun.com/guides/runtime/delete-directory) + - [Read environment variables](https://bun.com/guides/runtime/read-env) + - [Import a HTML file as text](https://bun.com/guides/runtime/import-html) + - [Install and run Bun in GitHub Actions](https://bun.com/guides/runtime/cicd) + - [Debugging Bun with the web debugger](https://bun.com/guides/runtime/web-debugger) + - [Install TypeScript declarations for Bun](https://bun.com/guides/runtime/typescript) + - [Debugging Bun with the VS Code extension](https://bun.com/guides/runtime/vscode-debugger) + - [Inspect memory usage using V8 heap snapshots](https://bun.com/guides/runtime/heap-snapshot) + - [Define and replace static globals & constants](https://bun.com/guides/runtime/define-constant) + - [Codesign a single-file JavaScript executable on macOS](https://bun.com/guides/runtime/codesign-macos-executable) - Streams - - - [Convert a ReadableStream to JSON](https://bun.sh/guides/streams/to-json) - - [Convert a ReadableStream to a Blob](https://bun.sh/guides/streams/to-blob) - - [Convert a ReadableStream to a Buffer](https://bun.sh/guides/streams/to-buffer) - - [Convert a ReadableStream to a string](https://bun.sh/guides/streams/to-string) - - [Convert a ReadableStream to a Uint8Array](https://bun.sh/guides/streams/to-typedarray) - - [Convert a ReadableStream to an array of chunks](https://bun.sh/guides/streams/to-array) - - [Convert a Node.js Readable to JSON](https://bun.sh/guides/streams/node-readable-to-json) - - [Convert a ReadableStream to an ArrayBuffer](https://bun.sh/guides/streams/to-arraybuffer) - - [Convert a Node.js Readable to a Blob](https://bun.sh/guides/streams/node-readable-to-blob) - - [Convert a Node.js Readable to a string](https://bun.sh/guides/streams/node-readable-to-string) - - [Convert a Node.js Readable to an Uint8Array](https://bun.sh/guides/streams/node-readable-to-uint8array) - - [Convert a Node.js Readable to an ArrayBuffer](https://bun.sh/guides/streams/node-readable-to-arraybuffer) + - [Convert a ReadableStream to JSON](https://bun.com/guides/streams/to-json) + - [Convert a ReadableStream to a Blob](https://bun.com/guides/streams/to-blob) + - [Convert a ReadableStream to a Buffer](https://bun.com/guides/streams/to-buffer) + - [Convert a ReadableStream to a string](https://bun.com/guides/streams/to-string) + - [Convert a ReadableStream to a Uint8Array](https://bun.com/guides/streams/to-typedarray) + - [Convert a ReadableStream to an array of chunks](https://bun.com/guides/streams/to-array) + - [Convert a Node.js Readable to JSON](https://bun.com/guides/streams/node-readable-to-json) + - [Convert a ReadableStream to an ArrayBuffer](https://bun.com/guides/streams/to-arraybuffer) + - [Convert a Node.js Readable to a Blob](https://bun.com/guides/streams/node-readable-to-blob) + - [Convert a Node.js Readable to a string](https://bun.com/guides/streams/node-readable-to-string) + - [Convert a Node.js Readable to an Uint8Array](https://bun.com/guides/streams/node-readable-to-uint8array) + - [Convert a Node.js Readable to an ArrayBuffer](https://bun.com/guides/streams/node-readable-to-arraybuffer) - Test - - - [Spy on methods in `bun test`](https://bun.sh/guides/test/spy-on) - - [Bail early with the Bun test runner](https://bun.sh/guides/test/bail) - - [Mock functions in `bun test`](https://bun.sh/guides/test/mock-functions) - - [Run tests in watch mode with Bun](https://bun.sh/guides/test/watch-mode) - - [Use snapshot testing in `bun test`](https://bun.sh/guides/test/snapshot) - - [Skip tests with the Bun test runner](https://bun.sh/guides/test/skip-tests) - - [Using Testing Library with Bun](https://bun.sh/guides/test/testing-library) - - [Update snapshots in `bun test`](https://bun.sh/guides/test/update-snapshots) - - [Run your tests with the Bun test runner](https://bun.sh/guides/test/run-tests) - - [Set the system time in Bun's test runner](https://bun.sh/guides/test/mock-clock) - - [Set a per-test timeout with the Bun test runner](https://bun.sh/guides/test/timeout) - - [Migrate from Jest to Bun's test runner](https://bun.sh/guides/test/migrate-from-jest) - - [Write browser DOM tests with Bun and happy-dom](https://bun.sh/guides/test/happy-dom) - - [Mark a test as a "todo" with the Bun test runner](https://bun.sh/guides/test/todo-tests) - - [Re-run tests multiple times with the Bun test runner](https://bun.sh/guides/test/rerun-each) - - [Generate code coverage reports with the Bun test runner](https://bun.sh/guides/test/coverage) - - [import, require, and test Svelte components with bun test](https://bun.sh/guides/test/svelte-test) - - [Set a code coverage threshold with the Bun test runner](https://bun.sh/guides/test/coverage-threshold) + - [Spy on methods in `bun test`](https://bun.com/guides/test/spy-on) + - [Bail early with the Bun test runner](https://bun.com/guides/test/bail) + - [Mock functions in `bun test`](https://bun.com/guides/test/mock-functions) + - [Run tests in watch mode with Bun](https://bun.com/guides/test/watch-mode) + - [Use snapshot testing in `bun test`](https://bun.com/guides/test/snapshot) + - [Skip tests with the Bun test runner](https://bun.com/guides/test/skip-tests) + - [Using Testing Library with Bun](https://bun.com/guides/test/testing-library) + - [Update snapshots in `bun test`](https://bun.com/guides/test/update-snapshots) + - [Run your tests with the Bun test runner](https://bun.com/guides/test/run-tests) + - [Set the system time in Bun's test runner](https://bun.com/guides/test/mock-clock) + - [Set a per-test timeout with the Bun test runner](https://bun.com/guides/test/timeout) + - [Migrate from Jest to Bun's test runner](https://bun.com/guides/test/migrate-from-jest) + - [Write browser DOM tests with Bun and happy-dom](https://bun.com/guides/test/happy-dom) + - [Mark a test as a "todo" with the Bun test runner](https://bun.com/guides/test/todo-tests) + - [Re-run tests multiple times with the Bun test runner](https://bun.com/guides/test/rerun-each) + - [Generate code coverage reports with the Bun test runner](https://bun.com/guides/test/coverage) + - [import, require, and test Svelte components with bun test](https://bun.com/guides/test/svelte-test) + - [Set a code coverage threshold with the Bun test runner](https://bun.com/guides/test/coverage-threshold) - Util - - - [Generate a UUID](https://bun.sh/guides/util/javascript-uuid) - - [Hash a password](https://bun.sh/guides/util/hash-a-password) - - [Escape an HTML string](https://bun.sh/guides/util/escape-html) - - [Get the current Bun version](https://bun.sh/guides/util/version) - - [Encode and decode base64 strings](https://bun.sh/guides/util/base64) - - [Compress and decompress data with gzip](https://bun.sh/guides/util/gzip) - - [Sleep for a fixed number of milliseconds](https://bun.sh/guides/util/sleep) - - [Detect when code is executed with Bun](https://bun.sh/guides/util/detect-bun) - - [Check if two objects are deeply equal](https://bun.sh/guides/util/deep-equals) - - [Compress and decompress data with DEFLATE](https://bun.sh/guides/util/deflate) - - [Get the absolute path to the current entrypoint](https://bun.sh/guides/util/main) - - [Get the directory of the current file](https://bun.sh/guides/util/import-meta-dir) - - [Check if the current file is the entrypoint](https://bun.sh/guides/util/entrypoint) - - [Get the file name of the current file](https://bun.sh/guides/util/import-meta-file) - - [Convert a file URL to an absolute path](https://bun.sh/guides/util/file-url-to-path) - - [Convert an absolute path to a file URL](https://bun.sh/guides/util/path-to-file-url) - - [Get the absolute path of the current file](https://bun.sh/guides/util/import-meta-path) - - [Get the path to an executable bin file](https://bun.sh/guides/util/which-path-to-executable-bin) + - [Generate a UUID](https://bun.com/guides/util/javascript-uuid) + - [Hash a password](https://bun.com/guides/util/hash-a-password) + - [Escape an HTML string](https://bun.com/guides/util/escape-html) + - [Get the current Bun version](https://bun.com/guides/util/version) + - [Encode and decode base64 strings](https://bun.com/guides/util/base64) + - [Compress and decompress data with gzip](https://bun.com/guides/util/gzip) + - [Sleep for a fixed number of milliseconds](https://bun.com/guides/util/sleep) + - [Detect when code is executed with Bun](https://bun.com/guides/util/detect-bun) + - [Check if two objects are deeply equal](https://bun.com/guides/util/deep-equals) + - [Compress and decompress data with DEFLATE](https://bun.com/guides/util/deflate) + - [Get the absolute path to the current entrypoint](https://bun.com/guides/util/main) + - [Get the directory of the current file](https://bun.com/guides/util/import-meta-dir) + - [Check if the current file is the entrypoint](https://bun.com/guides/util/entrypoint) + - [Get the file name of the current file](https://bun.com/guides/util/import-meta-file) + - [Convert a file URL to an absolute path](https://bun.com/guides/util/file-url-to-path) + - [Convert an absolute path to a file URL](https://bun.com/guides/util/path-to-file-url) + - [Get the absolute path of the current file](https://bun.com/guides/util/import-meta-path) + - [Get the path to an executable bin file](https://bun.com/guides/util/which-path-to-executable-bin) - WebSocket - - - [Build a publish-subscribe WebSocket server](https://bun.sh/guides/websocket/pubsub) - - [Build a simple WebSocket server](https://bun.sh/guides/websocket/simple) - - [Enable compression for WebSocket messages](https://bun.sh/guides/websocket/compression) - - [Set per-socket contextual data on a WebSocket](https://bun.sh/guides/websocket/context) + - [Build a publish-subscribe WebSocket server](https://bun.com/guides/websocket/pubsub) + - [Build a simple WebSocket server](https://bun.com/guides/websocket/simple) + - [Enable compression for WebSocket messages](https://bun.com/guides/websocket/compression) + - [Set per-socket contextual data on a WebSocket](https://bun.com/guides/websocket/context) - Write file - - - [Delete a file](https://bun.sh/guides/write-file/unlink) - - [Write to stdout](https://bun.sh/guides/write-file/stdout) - - [Write a file to stdout](https://bun.sh/guides/write-file/cat) - - [Write a Blob to a file](https://bun.sh/guides/write-file/blob) - - [Write a string to a file](https://bun.sh/guides/write-file/basic) - - [Append content to a file](https://bun.sh/guides/write-file/append) - - [Write a file incrementally](https://bun.sh/guides/write-file/filesink) - - [Write a Response to a file](https://bun.sh/guides/write-file/response) - - [Copy a file to another location](https://bun.sh/guides/write-file/file-cp) - - [Write a ReadableStream to a file](https://bun.sh/guides/write-file/stream) + - [Delete a file](https://bun.com/guides/write-file/unlink) + - [Write to stdout](https://bun.com/guides/write-file/stdout) + - [Write a file to stdout](https://bun.com/guides/write-file/cat) + - [Write a Blob to a file](https://bun.com/guides/write-file/blob) + - [Write a string to a file](https://bun.com/guides/write-file/basic) + - [Append content to a file](https://bun.com/guides/write-file/append) + - [Write a file incrementally](https://bun.com/guides/write-file/filesink) + - [Write a Response to a file](https://bun.com/guides/write-file/response) + - [Copy a file to another location](https://bun.com/guides/write-file/file-cp) + - [Write a ReadableStream to a file](https://bun.com/guides/write-file/stream) ## Contributing -Refer to the [Project > Contributing](https://bun.sh/docs/project/contributing) guide to start contributing to Bun. +Refer to the [Project > Contributing](https://bun.com/docs/project/contributing) guide to start contributing to Bun. ## License -Refer to the [Project > License](https://bun.sh/docs/project/licensing) page for information about Bun's licensing. +Refer to the [Project > License](https://bun.com/docs/project/licensing) page for information about Bun's licensing. diff --git a/SECURITY.md b/SECURITY.md index 5179a1ec70..f9402ed970 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,5 +8,4 @@ ## Reporting a Vulnerability -Report any discovered vulnerabilities to the Bun team by emailing `security@bun.sh`. Your report will acknowledged within 5 days, and a team member will be assigned as the primary handler. To the greatest extent possible, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement, and may ask for additional information or guidance surrounding the reported issue. - +Report any discovered vulnerabilities to the Bun team by emailing `security@bun.com`. Your report will acknowledged within 5 days, and a team member will be assigned as the primary handler. To the greatest extent possible, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement, and may ask for additional information or guidance surrounding the reported issue. diff --git a/bench/expect-to-equal/README.md b/bench/expect-to-equal/README.md index 3e7e3594b7..7f79d91198 100644 --- a/bench/expect-to-equal/README.md +++ b/bench/expect-to-equal/README.md @@ -40,4 +40,4 @@ vitest (node v18.11.0) > expect().toEqual() x 10000: 401.08ms -This project was created using `bun init` in bun v0.3.0. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. +This project was created using `bun init` in bun v0.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bench/snippets/dns-prefetch.mjs b/bench/snippets/dns-prefetch.mjs index 885be66100..5ea5e24098 100644 --- a/bench/snippets/dns-prefetch.mjs +++ b/bench/snippets/dns-prefetch.mjs @@ -9,7 +9,7 @@ // To clear your DNS cache on Windows: // ipconfig /flushdns // -const url = new URL(process.argv.length > 2 ? process.argv.at(-1) : "https://bun.sh"); +const url = new URL(process.argv.length > 2 ? process.argv.at(-1) : "https://bun.com"); const hostname = url.hostname; const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80; diff --git a/bench/websocket-server/README.md b/bench/websocket-server/README.md index c583e54bab..c3a8ef3575 100644 --- a/bench/websocket-server/README.md +++ b/bench/websocket-server/README.md @@ -34,4 +34,4 @@ For example, when the client sends `"foo"`, the server sends back `"John: foo"` The client script waits until it receives all the messages for each client before sending the next batch of messages. -This project was created using `bun init` in bun v0.2.1. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. +This project was created using `bun init` in bun v0.2.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/docs/api/binary-data.md b/docs/api/binary-data.md index 8803765040..bd9bed578b 100644 --- a/docs/api/binary-data.md +++ b/docs/api/binary-data.md @@ -522,7 +522,7 @@ for await (const chunk of stream) { } ``` -For a more complete discussion of streams in Bun, see [API > Streams](https://bun.sh/docs/api/streams). +For a more complete discussion of streams in Bun, see [API > Streams](https://bun.com/docs/api/streams). ## Conversion diff --git a/docs/api/cc.md b/docs/api/cc.md index 0cdf0b0a75..383cdfaba0 100644 --- a/docs/api/cc.md +++ b/docs/api/cc.md @@ -2,7 +2,7 @@ ## Usage (cc in `bun:ffi`) -See the [introduction blog post](https://bun.sh/blog/compile-and-run-c-in-js) for more information. +See the [introduction blog post](https://bun.com/blog/compile-and-run-c-in-js) for more information. JavaScript: diff --git a/docs/api/dns.md b/docs/api/dns.md index 5cb50ee549..68a90631d2 100644 --- a/docs/api/dns.md +++ b/docs/api/dns.md @@ -3,7 +3,7 @@ Bun implements the `node:dns` module. ```ts import * as dns from "node:dns"; -const addrs = await dns.promises.resolve4("bun.sh", { ttl: true }); +const addrs = await dns.promises.resolve4("bun.com", { ttl: true }); console.log(addrs); // => [{ address: "172.67.161.226", family: 4, ttl: 0 }, ...] ``` @@ -54,10 +54,10 @@ Here's an example: ```ts import { dns } from "bun"; -dns.prefetch("bun.sh", 443); +dns.prefetch("bun.com", 443); // // ... sometime later ... -await fetch("https://bun.sh"); +await fetch("https://bun.com"); ``` ### `dns.getCacheStats()` diff --git a/docs/api/fetch.md b/docs/api/fetch.md index 80aaecc46c..3a69755488 100644 --- a/docs/api/fetch.md +++ b/docs/api/fetch.md @@ -267,7 +267,7 @@ const response = await fetch("s3://my-bucket/path/to/object", { Note: Only PUT and POST methods support request bodies when using S3. For uploads, Bun automatically uses multipart upload for streaming bodies. -You can read more about Bun's S3 support in the [S3](https://bun.sh/docs/api/s3) documentation. +You can read more about Bun's S3 support in the [S3](https://bun.com/docs/api/s3) documentation. #### File URLs - `file://` @@ -376,14 +376,14 @@ To prefetch a DNS entry, you can use the `dns.prefetch` API. This API is useful ```ts import { dns } from "bun"; -dns.prefetch("bun.sh"); +dns.prefetch("bun.com"); ``` #### DNS caching By default, Bun caches and deduplicates DNS queries in-memory for up to 30 seconds. You can see the cache stats by calling `dns.getCacheStats()`: -To learn more about DNS caching in Bun, see the [DNS caching](https://bun.sh/docs/api/dns) documentation. +To learn more about DNS caching in Bun, see the [DNS caching](https://bun.com/docs/api/dns) documentation. ### Preconnect to a host @@ -392,7 +392,7 @@ To preconnect to a host, you can use the `fetch.preconnect` API. This API is use ```ts import { fetch } from "bun"; -fetch.preconnect("https://bun.sh"); +fetch.preconnect("https://bun.com"); ``` Note: calling `fetch` immediately after `fetch.preconnect` will not make your request faster. Preconnecting only helps if you know you'll need to connect to a host soon, but you're not ready to make the request yet. @@ -402,7 +402,7 @@ Note: calling `fetch` immediately after `fetch.preconnect` will not make your re To preconnect to a host at startup, you can pass `--fetch-preconnect`: ```sh -$ bun --fetch-preconnect https://bun.sh ./my-script.ts +$ bun --fetch-preconnect https://bun.com ./my-script.ts ``` This is sort of like `` in HTML. diff --git a/docs/api/file-io.md b/docs/api/file-io.md index f8e3102783..506ec9b051 100644 --- a/docs/api/file-io.md +++ b/docs/api/file-io.md @@ -1,8 +1,8 @@ {% callout %} - + -**Note** — The `Bun.file` and `Bun.write` APIs documented on this page are heavily optimized and represent the recommended way to perform file-system tasks using Bun. For operations that are not yet available with `Bun.file`, such as `mkdir` or `readdir`, you can use Bun's [nearly complete](https://bun.sh/docs/runtime/nodejs-apis#node-fs) implementation of the [`node:fs`](https://nodejs.org/api/fs.html) module. +**Note** — The `Bun.file` and `Bun.write` APIs documented on this page are heavily optimized and represent the recommended way to perform file-system tasks using Bun. For operations that are not yet available with `Bun.file`, such as `mkdir` or `readdir`, you can use Bun's [nearly complete](https://bun.com/docs/runtime/nodejs-apis#node-fs) implementation of the [`node:fs`](https://nodejs.org/api/fs.html) module. {% /callout %} @@ -208,7 +208,7 @@ await Bun.write(Bun.stdout, input); To write the body of an HTTP response to disk: ```ts -const response = await fetch("https://bun.sh"); +const response = await fetch("https://bun.com"); await Bun.write("index.html", response); ``` diff --git a/docs/api/globals.md b/docs/api/globals.md index 8e5a89651a..1a98bb0899 100644 --- a/docs/api/globals.md +++ b/docs/api/globals.md @@ -34,7 +34,7 @@ Bun implements the following globals. - [`Buffer`](https://nodejs.org/api/buffer.html#class-buffer) - Node.js -- See [Node.js > `Buffer`](https://bun.sh/docs/runtime/nodejs-apis#node-buffer) +- See [Node.js > `Buffer`](https://bun.com/docs/runtime/nodejs-apis#node-buffer) --- @@ -172,7 +172,7 @@ Bun implements the following globals. - [`global`](https://nodejs.org/api/globals.html#global) - Node.js -- See [Node.js > `global`](https://bun.sh/docs/runtime/nodejs-apis#global). +- See [Node.js > `global`](https://bun.com/docs/runtime/nodejs-apis#global). --- @@ -188,7 +188,7 @@ Bun implements the following globals. --- -- [`HTMLRewriter`](https://bun.sh/docs/api/html-rewriter) +- [`HTMLRewriter`](https://bun.com/docs/api/html-rewriter) - Cloudflare -   @@ -220,7 +220,7 @@ Bun implements the following globals. - [`process`](https://nodejs.org/api/process.html) - Node.js -- See [Node.js > `process`](https://bun.sh/docs/runtime/nodejs-apis#node-process) +- See [Node.js > `process`](https://bun.com/docs/runtime/nodejs-apis#node-process) --- diff --git a/docs/api/http.md b/docs/api/http.md index 0a0c959e48..e5b052fed9 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -1,7 +1,7 @@ The page primarily documents the Bun-native `Bun.serve` API. Bun also implements [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and the Node.js [`http`](https://nodejs.org/api/http.html) and [`https`](https://nodejs.org/api/https.html) modules. {% callout %} -These modules have been re-implemented to use Bun's fast internal HTTP infrastructure. Feel free to use these modules directly; frameworks like [Express](https://expressjs.com/) that depend on these modules should work out of the box. For granular compatibility information, see [Runtime > Node.js APIs](https://bun.sh/docs/runtime/nodejs-apis). +These modules have been re-implemented to use Bun's fast internal HTTP infrastructure. Feel free to use these modules directly; frameworks like [Express](https://expressjs.com/) that depend on these modules should work out of the box. For granular compatibility information, see [Runtime > Node.js APIs](https://bun.com/docs/runtime/nodejs-apis). {% /callout %} To start a high-performance HTTP server with a clean API, the recommended approach is [`Bun.serve`](#start-a-server-bun-serve). @@ -149,7 +149,7 @@ Bun.serve({ }), // Redirects - "/blog": Response.redirect("https://bun.sh/blog"), + "/blog": Response.redirect("https://bun.com/blog"), // API responses "/api/config": Response.json({ @@ -342,9 +342,9 @@ Bun.serve({ }); ``` -HTML imports don't just serve HTML — it's a full-featured frontend bundler, transpiler, and toolkit built using Bun's [bundler](https://bun.sh/docs/bundler), JavaScript transpiler and CSS parser. You can use this to build full-featured frontends with React, TypeScript, Tailwind CSS, and more. +HTML imports don't just serve HTML — it's a full-featured frontend bundler, transpiler, and toolkit built using Bun's [bundler](https://bun.com/docs/bundler), JavaScript transpiler and CSS parser. You can use this to build full-featured frontends with React, TypeScript, Tailwind CSS, and more. -For a complete guide on building full-stack applications with HTML imports, including detailed examples and best practices, see [/docs/bundler/fullstack](https://bun.sh/docs/bundler/fullstack). +For a complete guide on building full-stack applications with HTML imports, including detailed examples and best practices, see [/docs/bundler/fullstack](https://bun.com/docs/bundler/fullstack). ### Practical example: REST API @@ -605,7 +605,7 @@ Bun.serve({ ``` {% callout %} -[Learn more about debugging in Bun](https://bun.sh/docs/runtime/debugger) +[Learn more about debugging in Bun](https://bun.com/docs/runtime/debugger) {% /callout %} The call to `Bun.serve` returns a `Server` object. To stop the server, call the `.stop()` method. @@ -772,7 +772,7 @@ Instead of passing the server options into `Bun.serve`, `export default` it. Thi $ bun --hot server.ts ``` --> - + ## Streaming files diff --git a/docs/api/s3.md b/docs/api/s3.md index 3cde81fc7d..68dae77a39 100644 --- a/docs/api/s3.md +++ b/docs/api/s3.md @@ -4,7 +4,7 @@ Production servers often read, upload, and write files to S3-compatible object s ### Bun's S3 API is fast -{% image src="https://bun.sh/bun-s3-node.gif" alt="Bun's S3 API is fast" caption="Left: Bun v1.1.44. Right: Node.js v23.6.0" /%} +{% image src="https://bun.com/bun-s3-node.gif" alt="Bun's S3 API is fast" caption="Left: Bun v1.1.44. Right: Node.js v23.6.0" /%} {% /callout %} diff --git a/docs/api/utils.md b/docs/api/utils.md index 2d04163c72..76571fd97d 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -234,7 +234,7 @@ const currentFile = import.meta.url; Bun.openInEditor(currentFile); ``` -You can override this via the `debug.editor` setting in your [`bunfig.toml`](https://bun.sh/docs/runtime/bunfig). +You can override this via the `debug.editor` setting in your [`bunfig.toml`](https://bun.com/docs/runtime/bunfig). ```toml-diff#bunfig.toml + [debug] @@ -704,7 +704,7 @@ Bun.nanoseconds(); Bun implements a set of convenience functions for asynchronously consuming the body of a `ReadableStream` and converting it to various binary formats. ```ts -const stream = (await fetch("https://bun.sh")).body; +const stream = (await fetch("https://bun.com")).body; stream; // => ReadableStream await Bun.readableStreamToArrayBuffer(stream); @@ -787,7 +787,7 @@ const buffer = Buffer.alloc(1024 * 1024); estimateShallowMemoryUsageOf(buffer); // => 1048624 -const req = new Request("https://bun.sh"); +const req = new Request("https://bun.com"); estimateShallowMemoryUsageOf(req); // => 167 diff --git a/docs/bundler/fullstack.md b/docs/bundler/fullstack.md index 587afe8c60..16ed1d8402 100644 --- a/docs/bundler/fullstack.md +++ b/docs/bundler/fullstack.md @@ -325,7 +325,7 @@ When adding a build step is too complicated, you can set `development: false` in ## Plugins -Bun's [bundler plugins](https://bun.sh/docs/bundler/plugins) are also supported when bundling static routes. +Bun's [bundler plugins](https://bun.com/docs/bundler/plugins) are also supported when bundling static routes. To configure plugins for `Bun.serve`, add a `plugins` array in the `[serve.static]` section of your `bunfig.toml`. @@ -365,7 +365,7 @@ Or in your CSS: ### Custom plugins -Any JS file or module which exports a [valid bundler plugin object](https://bun.sh/docs/bundler/plugins#usage) (essentially an object with a `name` and `setup` field) can be placed inside the `plugins` array: +Any JS file or module which exports a [valid bundler plugin object](https://bun.com/docs/bundler/plugins#usage) (essentially an object with a `name` and `setup` field) can be placed inside the `plugins` array: ```toml#bunfig.toml [serve.static] diff --git a/docs/bundler/index.md b/docs/bundler/index.md index a889343eee..9442ae8680 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -147,7 +147,7 @@ $ bun build ./index.tsx --outdir ./out --watch ## Content types -Like the Bun runtime, the bundler supports an array of file types out of the box. The following table breaks down the bundler's set of standard "loaders". Refer to [Bundler > File types](https://bun.sh/docs/runtime/loaders) for full documentation. +Like the Bun runtime, the bundler supports an array of file types out of the box. The following table breaks down the bundler's set of standard "loaders". Refer to [Bundler > File types](https://bun.com/docs/runtime/loaders) for full documentation. {% table %} @@ -220,11 +220,11 @@ console.log(logo); The exact behavior of the file loader is also impacted by [`naming`](#naming) and [`publicPath`](#publicpath). {% /callout %} -Refer to the [Bundler > Loaders](https://bun.sh/docs/bundler/loaders#file) page for more complete documentation on the file loader. +Refer to the [Bundler > Loaders](https://bun.com/docs/bundler/loaders#file) page for more complete documentation on the file loader. ### Plugins -The behavior described in this table can be overridden or extended with [plugins](https://bun.sh/docs/bundler/plugins). Refer to the [Bundler > Loaders](https://bun.sh/docs/bundler/plugins) page for complete documentation. +The behavior described in this table can be overridden or extended with [plugins](https://bun.com/docs/bundler/plugins). Refer to the [Bundler > Loaders](https://bun.com/docs/bundler/plugins) page for complete documentation. ## API @@ -484,7 +484,7 @@ n/a {% /codetabs %} -Bun implements a universal plugin system for both Bun's runtime and bundler. Refer to the [plugin documentation](https://bun.sh/docs/bundler/plugins) for complete documentation. +Bun implements a universal plugin system for both Bun's runtime and bundler. Refer to the [plugin documentation](https://bun.com/docs/bundler/plugins) for complete documentation. + Performance sensitive APIs like `Buffer`, `fetch`, and `Response` are heavily profiled and optimized. Under the hood Bun uses the [JavaScriptCore engine](https://developer.apple.com/documentation/javascriptcore), which is developed by Apple for Safari. It starts and runs faster than V8, the engine used by Node.js and Chromium-based browsers. @@ -21,7 +21,7 @@ $ bun index.ts $ bun index.tsx ``` -Some aspects of Bun's runtime behavior are affected by the contents of your `tsconfig.json` file. Refer to [Runtime > TypeScript](https://bun.sh/docs/runtime/typescript) page for details. +Some aspects of Bun's runtime behavior are affected by the contents of your `tsconfig.json` file. Refer to [Runtime > TypeScript](https://bun.com/docs/runtime/typescript) page for details. @@ -122,17 +122,17 @@ $ bun run ./my-wasm-app.whatever ## Node.js compatibility -Long-term, Bun aims for complete Node.js compatibility. Most Node.js packages already work with Bun out of the box, but certain low-level APIs like `dgram` are still unimplemented. Track the current compatibility status at [Ecosystem > Node.js](https://bun.sh/docs/runtime/nodejs-apis). +Long-term, Bun aims for complete Node.js compatibility. Most Node.js packages already work with Bun out of the box, but certain low-level APIs like `dgram` are still unimplemented. Track the current compatibility status at [Ecosystem > Node.js](https://bun.com/docs/runtime/nodejs-apis). Bun implements the Node.js module resolution algorithm, so dependencies can still be managed with `package.json`, `node_modules`, and CommonJS-style imports. {% callout %} -**Note** — We recommend using Bun's [built-in package manager](https://bun.sh/docs/cli/install) for a performance boost over other npm clients. +**Note** — We recommend using Bun's [built-in package manager](https://bun.com/docs/cli/install) for a performance boost over other npm clients. {% /callout %} ## Web APIs - + Some Web APIs aren't relevant in the context of a server-first runtime like Bun, such as the [DOM API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API#html_dom_api_interfaces) or [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API). Many others, though, are broadly useful outside of the browser context; when possible, Bun implements these Web-standard APIs instead of introducing new APIs. @@ -237,67 +237,67 @@ Bun exposes a set of Bun-specific APIs on the `Bun` global object and through a --- -- [HTTP](https://bun.sh/docs/api/http) +- [HTTP](https://bun.com/docs/api/http) - `Bun.serve` --- -- [File I/O](https://bun.sh/docs/api/file-io) +- [File I/O](https://bun.com/docs/api/file-io) - `Bun.file` `Bun.write` --- -- [Processes](https://bun.sh/docs/api/spawn) +- [Processes](https://bun.com/docs/api/spawn) - `Bun.spawn` `Bun.spawnSync` --- -- [TCP](https://bun.sh/docs/api/tcp) +- [TCP](https://bun.com/docs/api/tcp) - `Bun.listen` `Bun.connect` --- -- [Transpiler](https://bun.sh/docs/api/transpiler) +- [Transpiler](https://bun.com/docs/api/transpiler) - `Bun.Transpiler` --- -- [Routing](https://bun.sh/docs/api/file-system-router) +- [Routing](https://bun.com/docs/api/file-system-router) - `Bun.FileSystemRouter` --- -- [HTMLRewriter](https://bun.sh/docs/api/html-rewriter) +- [HTMLRewriter](https://bun.com/docs/api/html-rewriter) - `HTMLRewriter` --- -- [Utils](https://bun.sh/docs/api/utils) +- [Utils](https://bun.com/docs/api/utils) - `Bun.peek` `Bun.which` --- -- [SQLite](https://bun.sh/docs/api/sqlite) +- [SQLite](https://bun.com/docs/api/sqlite) - `bun:sqlite` --- -- [FFI](https://bun.sh/docs/api/ffi) +- [FFI](https://bun.com/docs/api/ffi) - `bun:ffi` --- -- [DNS](https://bun.sh/docs/api/dns) +- [DNS](https://bun.com/docs/api/dns) - `bun:dns` --- -- [Testing](https://bun.sh/docs/api/test) +- [Testing](https://bun.com/docs/api/test) - `bun:test` --- -- [Node-API](https://bun.sh/docs/api/node-api) +- [Node-API](https://bun.com/docs/api/node-api) - `Node-API` --- @@ -306,4 +306,4 @@ Bun exposes a set of Bun-specific APIs on the `Bun` global object and through a ## Plugins -Support for additional file types can be implemented with plugins. Refer to [Runtime > Plugins](https://bun.sh/docs/bundler/plugins) for full documentation. +Support for additional file types can be implemented with plugins. Refer to [Runtime > Plugins](https://bun.com/docs/bundler/plugins) for full documentation. diff --git a/docs/runtime/jsx.md b/docs/runtime/jsx.md index ab08255599..f5ad0dc271 100644 --- a/docs/runtime/jsx.md +++ b/docs/runtime/jsx.md @@ -14,7 +14,7 @@ console.log(); ## Configuration -Bun reads your `tsconfig.json` or `jsconfig.json` configuration files to determines how to perform the JSX transform internally. To avoid using either of these, the following options can also be defined in [`bunfig.toml`](https://bun.sh/docs/runtime/bunfig). +Bun reads your `tsconfig.json` or `jsconfig.json` configuration files to determines how to perform the JSX transform internally. To avoid using either of these, the following options can also be defined in [`bunfig.toml`](https://bun.com/docs/runtime/bunfig). The following compiler options are respected. diff --git a/docs/runtime/loaders.md b/docs/runtime/loaders.md index 3909a1de90..18608f3020 100644 --- a/docs/runtime/loaders.md +++ b/docs/runtime/loaders.md @@ -9,7 +9,7 @@ $ bun index.ts $ bun index.tsx ``` -Some aspects of Bun's runtime behavior are affected by the contents of your `tsconfig.json` file. Refer to [Runtime > TypeScript](https://bun.sh/docs/runtime/typescript) page for details. +Some aspects of Bun's runtime behavior are affected by the contents of your `tsconfig.json` file. Refer to [Runtime > TypeScript](https://bun.com/docs/runtime/typescript) page for details. ## JSX @@ -89,11 +89,11 @@ import db from "./my.db" with { type: "sqlite" }; console.log(db.query("select * from users LIMIT 1").get()); ``` -This uses [`bun:sqlite`](https://bun.sh/docs/api/sqlite). +This uses [`bun:sqlite`](https://bun.com/docs/api/sqlite). ## Custom loaders -Support for additional file types can be implemented with plugins. Refer to [Runtime > Plugins](https://bun.sh/docs/bundler/plugins) for full documentation. +Support for additional file types can be implemented with plugins. Refer to [Runtime > Plugins](https://bun.com/docs/bundler/plugins) for full documentation.