From 5fa13625a1ca0ea1a3a1c5bb86d0880dcfac349f Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 21 Jun 2023 23:38:18 -0700 Subject: upgrade zig to `v0.11.0-dev.3737+9eb008717` (#3374) * progress * finish `@memset/@memcpy` update * Update build.zig * change `@enumToInt` to `@intFromEnum` and friends * update zig versions * it was 1 * add link to issue * add `compileError` reminder * fix merge * format * upgrade to llvm 16 * Revert "upgrade to llvm 16" This reverts commit cc930ceb1c5b4db9614a7638596948f704544ab8. --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/javascript.zig | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d458b6e7e..545f41f19 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -196,7 +196,7 @@ pub const SavedSourceMap = struct { pub const SourceMapHandler = js_printer.SourceMapHandler.For(SavedSourceMap, onSourceMapChunk); pub fn putMappings(this: *SavedSourceMap, source: logger.Source, mappings: MutableString) !void { - var entry = try this.map.getOrPut(std.hash.Wyhash.hash(0, source.path.text)); + var entry = try this.map.getOrPut(bun.hash(source.path.text)); if (entry.found_existing) { var value = Value.from(entry.value_ptr.*); if (value.get(MappingList)) |source_map_| { @@ -213,7 +213,7 @@ pub const SavedSourceMap = struct { } pub fn get(this: *SavedSourceMap, path: string) ?MappingList { - var mapping = this.map.getEntry(std.hash.Wyhash.hash(0, path)) orelse return null; + var mapping = this.map.getEntry(bun.hash(path)) orelse return null; switch (Value.from(mapping.value_ptr.*).tag()) { (@field(Value.Tag, @typeName(MappingList))) => { return Value.from(mapping.value_ptr.*).as(MappingList).*; @@ -264,7 +264,7 @@ export fn Bun__readOriginTimer(vm: *JSC.VirtualMachine) u64 { export fn Bun__readOriginTimerStart(vm: *JSC.VirtualMachine) f64 { // timespce to milliseconds - return @floatCast(f64, (@intToFloat(f64, vm.origin_timestamp) + JSC.VirtualMachine.origin_relative_epoch) / 1_000_000.0); + return @floatCast(f64, (@floatFromInt(f64, vm.origin_timestamp) + JSC.VirtualMachine.origin_relative_epoch) / 1_000_000.0); } // comptime { @@ -1382,7 +1382,7 @@ pub const VirtualMachine = struct { // // This double prints // pub fn promiseRejectionTracker(global: *JSGlobalObject, promise: *JSPromise, _: JSPromiseRejectionOperation) callconv(.C) JSValue { // const result = promise.result(global.vm()); - // if (@enumToInt(VirtualMachine.get().last_error_jsvalue) != @enumToInt(result)) { + // if (@intFromEnum(VirtualMachine.get().last_error_jsvalue) != @intFromEnum(result)) { // VirtualMachine.get().runErrorHandler(result, null); // } @@ -1823,7 +1823,7 @@ pub const VirtualMachine = struct { iterator(_vm, globalObject, nextValue, ctx.?, false); } inline fn iterator(_: [*c]VM, _: [*c]JSGlobalObject, nextValue: JSValue, ctx: ?*anyopaque, comptime color: bool) void { - var this_ = @intToPtr(*@This(), @ptrToInt(ctx)); + var this_ = @ptrFromInt(*@This(), @intFromPtr(ctx)); VirtualMachine.get().printErrorlikeObject(nextValue, null, this_.current_exception_list, Writer, this_.writer, color, allow_side_effects); } }; @@ -2066,8 +2066,8 @@ pub const VirtualMachine = struct { )) |lines| { var source_lines = exception.stack.source_lines_ptr[0..JSC.ZigException.Holder.source_lines_count]; var source_line_numbers = exception.stack.source_lines_numbers[0..JSC.ZigException.Holder.source_lines_count]; - std.mem.set(ZigString, source_lines, ZigString.Empty); - std.mem.set(i32, source_line_numbers, 0); + @memset(source_lines, ZigString.Empty); + @memset(source_line_numbers, 0); var lines_ = lines[0..@min(lines.len, source_lines.len)]; for (lines_, 0..) |line, j| { @@ -2374,7 +2374,7 @@ pub const EventListenerMixin = struct { const FetchEventRejectionHandler = struct { pub fn onRejection(_ctx: *anyopaque, err: anyerror, fetch_event: *FetchEvent, value: JSValue) void { onError( - @intToPtr(*CtxType, @ptrToInt(_ctx)), + @ptrFromInt(*CtxType, @intFromPtr(_ctx)), err, value, fetch_event.request_context.?, @@ -2763,10 +2763,10 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime break :brk path_string.slice(); } else { var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); - @memcpy(&_on_file_update_path_buf, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); + @memcpy(_on_file_update_path_buf[0..file_path_without_trailing_slash.len], file_path_without_trailing_slash); _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; - @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len..][0..changed_name.len], changed_name); const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; file_hash = @This().Watcher.getHash(path_slice); break :brk path_slice; -- cgit v1.2.3 From 217501e180eadd1999f30733e0f13580cd1f0abf Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Thu, 22 Jun 2023 22:27:00 -0700 Subject: `expect().resolves` and `expect().rejects` (#3318) * Move expect and snapshots to their own files * expect().resolves and expect().rejects * Fix promise being added to unhandled rejection list * Handle timeouts in expect() * wip merge * Fix merge issue --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- packages/bun-types/bun-test.d.ts | 14 + src/bun.js/bindings/bindings.cpp | 12 + src/bun.js/bindings/bindings.zig | 13 + src/bun.js/bindings/exports.zig | 14 +- src/bun.js/bindings/generated_classes_list.zig | 10 +- src/bun.js/bindings/headers.h | 2 + src/bun.js/bindings/headers.zig | 2 + src/bun.js/event_loop.zig | 24 + src/bun.js/javascript.zig | 5 + src/bun.js/test/expect.zig | 3335 ++++++++++++++++++ src/bun.js/test/jest.zig | 4264 ++---------------------- src/bun.js/test/pretty_format.zig | 15 +- src/bun.js/test/snapshot.zig | 284 ++ src/cli/test_command.zig | 2 +- src/jsc.zig | 2 + test/cli/test/bun-test.test.ts | 22 +- 16 files changed, 3970 insertions(+), 4050 deletions(-) create mode 100644 src/bun.js/test/expect.zig create mode 100644 src/bun.js/test/snapshot.zig (limited to 'src/bun.js/javascript.zig') diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index ba59966ad..156585766 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -424,6 +424,20 @@ declare module "bun:test" { * expect(null).not.toBeNull(); */ not: Expect; + /** + * Expects the value to be a promise that resolves. + * + * @example + * expect(Promise.resolve(1)).resolves.toBe(1); + */ + resolves: Expect; + /** + * Expects the value to be a promise that rejects. + * + * @example + * expect(Promise.reject("error")).rejects.toBe("error"); + */ + rejects: Expect; /** * Asserts that a value equals what is expected. * diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4eee81f4d..2c2f5c2ea 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2593,6 +2593,12 @@ bool JSC__JSPromise__isHandled(const JSC__JSPromise* arg0, JSC__VM* arg1) { return arg0->isHandled(reinterpret_cast(arg1)); } +void JSC__JSPromise__setHandled(JSC__JSPromise* promise, JSC__VM* arg1) +{ + auto& vm = *arg1; + auto flags = promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32(); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isHandledFlag)); +} #pragma mark - JSC::JSInternalPromise @@ -2666,6 +2672,12 @@ bool JSC__JSInternalPromise__isHandled(const JSC__JSInternalPromise* arg0, JSC__ { return arg0->isHandled(reinterpret_cast(arg1)); } +void JSC__JSInternalPromise__setHandled(JSC__JSInternalPromise* promise, JSC__VM* arg1) +{ + auto& vm = *arg1; + auto flags = promise->internalField(JSC::JSPromise::Field::Flags).get().asUInt32(); + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isHandledFlag)); +} #pragma mark - JSC::JSGlobalObject diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 581fc6f85..00833c71d 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2045,6 +2045,9 @@ pub const JSPromise = extern struct { pub fn isHandled(this: *const JSPromise, vm: *VM) bool { return cppFn("isHandled", .{ this, vm }); } + pub fn setHandled(this: *JSPromise, vm: *VM) void { + cppFn("setHandled", .{ this, vm }); + } pub fn rejectWithCaughtException(this: *JSPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { return cppFn("rejectWithCaughtException", .{ this, globalObject, scope }); @@ -2115,6 +2118,7 @@ pub const JSPromise = extern struct { "asValue", "create", "isHandled", + "setHandled", "reject", "rejectAsHandled", "rejectAsHandledException", @@ -2149,6 +2153,9 @@ pub const JSInternalPromise = extern struct { pub fn isHandled(this: *const JSInternalPromise, vm: *VM) bool { return cppFn("isHandled", .{ this, vm }); } + pub fn setHandled(this: *JSInternalPromise, vm: *VM) void { + cppFn("setHandled", .{ this, vm }); + } pub fn rejectWithCaughtException(this: *JSInternalPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { return cppFn("rejectWithCaughtException", .{ this, globalObject, scope }); @@ -2332,6 +2339,7 @@ pub const JSInternalPromise = extern struct { "status", "result", "isHandled", + "setHandled", "resolvedPromise", "rejectedPromise", "resolve", @@ -2363,6 +2371,11 @@ pub const AnyPromise = union(enum) { inline else => |promise| promise.isHandled(vm), }; } + pub fn setHandled(this: AnyPromise, vm: *VM) void { + switch (this) { + inline else => |promise| promise.setHandled(vm), + } + } pub fn rejectWithCaughtException(this: AnyPromise, globalObject: *JSGlobalObject, scope: ThrowScope) void { switch (this) { diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index de3f819b7..f77b57216 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -2220,11 +2220,11 @@ pub const ZigConsoleClient = struct { } else if (value.as(JSC.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (value.as(JSC.Jest.ExpectAnything) != null) { + } else if (value.as(JSC.Expect.ExpectAnything) != null) { writer.writeAll("Anything"); return; - } else if (value.as(JSC.Jest.ExpectAny) != null) { - const constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectAny) != null) { + const constructor_value = JSC.Expect.ExpectAny.constructorValueGetCached(value) orelse return; this.addForNewLine("Any<".len); writer.writeAll("Any<"); @@ -2237,16 +2237,16 @@ pub const ZigConsoleClient = struct { writer.writeAll(">"); return; - } else if (value.as(JSC.Jest.ExpectStringContaining) != null) { - const substring_value = JSC.Jest.ExpectStringContaining.stringValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectStringContaining) != null) { + const substring_value = JSC.Expect.ExpectStringContaining.stringValueGetCached(value) orelse return; this.addForNewLine("StringContaining ".len); writer.writeAll("StringContaining "); this.printAs(.String, Writer, writer_, substring_value, .String, enable_ansi_colors); return; - } else if (value.as(JSC.Jest.ExpectStringMatching) != null) { - const test_value = JSC.Jest.ExpectStringMatching.testValueGetCached(value) orelse return; + } else if (value.as(JSC.Expect.ExpectStringMatching) != null) { + const test_value = JSC.Expect.ExpectStringMatching.testValueGetCached(value) orelse return; this.addForNewLine("StringMatching ".len); writer.writeAll("StringMatching "); diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index d5d987dce..c54965093 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -4,11 +4,11 @@ pub const Classes = struct { pub const Blob = JSC.WebCore.Blob; pub const CryptoHasher = JSC.API.Bun.Crypto.CryptoHasher; pub const Dirent = JSC.Node.Dirent; - pub const Expect = JSC.Jest.Expect; - pub const ExpectAny = JSC.Jest.ExpectAny; - pub const ExpectAnything = JSC.Jest.ExpectAnything; - pub const ExpectStringContaining = JSC.Jest.ExpectStringContaining; - pub const ExpectStringMatching = JSC.Jest.ExpectStringMatching; + pub const Expect = JSC.Expect.Expect; + pub const ExpectAny = JSC.Expect.ExpectAny; + pub const ExpectAnything = JSC.Expect.ExpectAnything; + pub const ExpectStringContaining = JSC.Expect.ExpectStringContaining; + pub const ExpectStringMatching = JSC.Expect.ExpectStringMatching; pub const FileSystemRouter = JSC.API.FileSystemRouter; pub const Bundler = JSC.API.JSBundler; pub const JSBundler = Bundler; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index cdf7e05f4..f507121f8 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -253,6 +253,7 @@ CPP_DECL JSC__JSPromise* JSC__JSPromise__resolvedPromise(JSC__JSGlobalObject* ar CPP_DECL JSC__JSValue JSC__JSPromise__resolvedPromiseValue(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void JSC__JSPromise__resolveOnNextTick(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL JSC__JSValue JSC__JSPromise__result(JSC__JSPromise* arg0, JSC__VM* arg1); +CPP_DECL void JSC__JSPromise__setHandled(JSC__JSPromise* arg0, JSC__VM* arg1); CPP_DECL uint32_t JSC__JSPromise__status(const JSC__JSPromise* arg0, JSC__VM* arg1); #pragma mark - JSC::JSInternalPromise @@ -267,6 +268,7 @@ CPP_DECL void JSC__JSInternalPromise__rejectWithCaughtException(JSC__JSInternalP CPP_DECL void JSC__JSInternalPromise__resolve(JSC__JSInternalPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL JSC__JSInternalPromise* JSC__JSInternalPromise__resolvedPromise(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL JSC__JSValue JSC__JSInternalPromise__result(const JSC__JSInternalPromise* arg0, JSC__VM* arg1); +CPP_DECL void JSC__JSInternalPromise__setHandled(JSC__JSInternalPromise* arg0, JSC__VM* arg1); CPP_DECL uint32_t JSC__JSInternalPromise__status(const JSC__JSInternalPromise* arg0, JSC__VM* arg1); #pragma mark - JSC::JSFunction diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 4dda5f30b..666369b21 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -168,6 +168,7 @@ pub extern fn JSC__JSPromise__resolvedPromise(arg0: *bindings.JSGlobalObject, JS pub extern fn JSC__JSPromise__resolvedPromiseValue(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) JSC__JSValue; pub extern fn JSC__JSPromise__resolveOnNextTick(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__result(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) JSC__JSValue; +pub extern fn JSC__JSPromise__setHandled(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) void; pub extern fn JSC__JSPromise__status(arg0: [*c]const JSC__JSPromise, arg1: *bindings.VM) u32; pub extern fn JSC__JSInternalPromise__create(arg0: *bindings.JSGlobalObject) [*c]bindings.JSInternalPromise; pub extern fn JSC__JSInternalPromise__isHandled(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) bool; @@ -179,6 +180,7 @@ pub extern fn JSC__JSInternalPromise__rejectWithCaughtException(arg0: [*c]bindin pub extern fn JSC__JSInternalPromise__resolve(arg0: [*c]bindings.JSInternalPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSInternalPromise__resolvedPromise(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) [*c]bindings.JSInternalPromise; pub extern fn JSC__JSInternalPromise__result(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) JSC__JSValue; +pub extern fn JSC__JSInternalPromise__setHandled(arg0: [*c]bindings.JSInternalPromise, arg1: *bindings.VM) void; pub extern fn JSC__JSInternalPromise__status(arg0: [*c]const JSC__JSInternalPromise, arg1: *bindings.VM) u32; pub extern fn JSC__JSFunction__optimizeSoon(JSValue0: JSC__JSValue) void; pub extern fn JSC__JSGlobalObject__bunVM(arg0: *bindings.JSGlobalObject) ?*bindings.VirtualMachine; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 8441bd064..0a3459d64 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -666,6 +666,30 @@ pub const EventLoop = struct { } } + pub fn waitForPromiseWithTimeout(this: *EventLoop, promise: JSC.AnyPromise, timeout: u32) bool { + return switch (promise.status(this.global.vm())) { + JSC.JSPromise.Status.Pending => { + if (timeout == 0) { + return false; + } + var start_time = std.time.milliTimestamp(); + while (promise.status(this.global.vm()) == .Pending) { + this.tick(); + + if (std.time.milliTimestamp() - start_time > timeout) { + return false; + } + + if (promise.status(this.global.vm()) == .Pending) { + this.autoTick(); + } + } + return true; + }, + else => true, + }; + } + pub fn waitForTasks(this: *EventLoop) void { this.tick(); while (this.tasks.count > 0) { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 545f41f19..bebfbeb18 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -653,6 +653,10 @@ pub const VirtualMachine = struct { this.eventLoop().waitForPromise(promise); } + pub fn waitForPromiseWithTimeout(this: *VirtualMachine, promise: JSC.AnyPromise, timeout: u32) bool { + return this.eventLoop().waitForPromiseWithTimeout(promise, timeout); + } + pub fn waitForTasks(this: *VirtualMachine) void { this.eventLoop().waitForTasks(); } @@ -957,6 +961,7 @@ pub const VirtualMachine = struct { } pub fn refCountedStringWithWasNew(this: *VirtualMachine, new: *bool, input_: []const u8, hash_: ?u32, comptime dupe: bool) *JSC.RefString { + JSC.markBinding(@src()); const hash = hash_ orelse JSC.RefString.computeHash(input_); var entry = this.ref_strings.getOrPut(hash) catch unreachable; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig new file mode 100644 index 000000000..90fcb2a73 --- /dev/null +++ b/src/bun.js/test/expect.zig @@ -0,0 +1,3335 @@ +const std = @import("std"); +const bun = @import("root").bun; +const default_allocator = bun.default_allocator; +const string = bun.string; +const MutableString = bun.MutableString; +const strings = bun.strings; +const Output = bun.Output; +const jest = bun.JSC.Jest; +const Jest = jest.Jest; +const TestRunner = jest.TestRunner; +const DescribeScope = jest.DescribeScope; +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; +const JSInternalPromise = JSC.JSInternalPromise; +const JSPromise = JSC.JSPromise; +const JSType = JSValue.JSType; +const JSError = JSC.JSError; +const JSObject = JSC.JSObject; +const CallFrame = JSC.CallFrame; +const ZigString = JSC.ZigString; +const Environment = bun.Environment; +const DiffFormatter = @import("./diff_format.zig").DiffFormatter; + +pub const Counter = struct { + expected: u32 = 0, + actual: u32 = 0, +}; + +pub var active_test_expectation_counter: Counter = .{}; + +/// https://jestjs.io/docs/expect +// To support async tests, we need to track the test ID +pub const Expect = struct { + pub usingnamespace JSC.Codegen.JSExpect; + + test_id: TestRunner.Test.ID, + scope: *DescribeScope, + flags: Flags = .{}, + + pub const Flags = packed struct { + promise: enum(u2) { + resolves, + rejects, + none, + } = .none, + not: bool = false, + }; + + pub fn getSignature(comptime matcher_name: string, comptime args: string, comptime not: bool) string { + const received = "expect(received)."; + comptime if (not) { + return received ++ "not." ++ matcher_name ++ "(" ++ args ++ ")"; + }; + return received ++ matcher_name ++ "(" ++ args ++ ")"; + } + + pub fn getFullSignature(comptime matcher: string, comptime args: string, comptime flags: Flags) string { + const fmt = "expect(received)." ++ if (flags.promise != .none) + switch (flags.promise) { + .resolves => if (flags.not) "resolves.not." else "resolves.", + .rejects => if (flags.not) "rejects.not." else "rejects.", + else => unreachable, + } + else if (flags.not) "not." else ""; + return fmt ++ matcher ++ "(" ++ args ++ ")"; + } + + pub fn getNot(this: *Expect, thisValue: JSValue, _: *JSGlobalObject) callconv(.C) JSValue { + this.flags.not = !this.flags.not; + return thisValue; + } + + pub fn getResolves(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { + switch (this.flags.promise) { + .rejects => { + globalThis.throw("Cannot chain .resolves() after .rejects()", .{}); + return .zero; + }, + .resolves => {}, + .none => { + this.flags.promise = .resolves; + }, + } + return thisValue; + } + + pub fn getRejects(this: *Expect, thisValue: JSValue, globalThis: *JSGlobalObject) callconv(.C) JSValue { + switch (this.flags.promise) { + .rejects => { + this.flags.promise = .rejects; + }, + .resolves => { + globalThis.throw("Cannot chain .rejects() after .resolves()", .{}); + return .zero; + }, + .none => { + this.flags.promise = .resolves; + }, + } + + return thisValue; + } + + pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, comptime matcher_name: string, comptime matcher_args: string) ?JSValue { + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("{s}() must be called in a test", .{matcher_name}); + return null; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal error: the expect(value) was garbage collected but it should not have been!", .{}); + return null; + }; + value.ensureStillAlive(); + + switch (this.flags.promise) { + inline .resolves, .rejects => |resolution| { + if (value.asAnyPromise()) |promise| { + var vm = globalThis.vm(); + promise.setHandled(vm); + + const now = std.time.Instant.now() catch unreachable; + const pending_test = Jest.runner.?.pending_test.?; + const elapsed = @divFloor(now.since(pending_test.started_at), std.time.ns_per_ms); + const remaining = @truncate(u32, Jest.runner.?.last_test_timeout_timer_duration -| elapsed); + + if (!globalThis.bunVM().waitForPromiseWithTimeout(promise, remaining)) { + pending_test.timeout(); + return null; + } + + const newValue = promise.result(vm); + switch (promise.status(vm)) { + .Fulfilled => switch (comptime resolution) { + .resolves => {}, + .rejects => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise that rejects\nReceived promise that resolved: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise that rejects\nReceived promise that resolved: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + }, + .Rejected => switch (comptime resolution) { + .rejects => {}, + .resolves => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise that resolves\nReceived promise that rejected: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise that resolves\nReceived promise that rejected: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{newValue.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + }, + .Pending => unreachable, + } + + newValue.ensureStillAlive(); + return newValue; + } else { + switch (this.flags.promise) { + .resolves => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .resolves }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + }, + .rejects => { + if (this.flags.not) { + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = true, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + } + const signature = comptime getFullSignature(matcher_name, matcher_args, .{ .not = false, .promise = .rejects }); + const fmt = signature ++ "\n\nExpected promise\nReceived: {any}\n"; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + globalThis.throwPretty(fmt, .{value.toFmt(globalThis, &formatter)}); + return null; + }, + .none => unreachable, + } + } + }, + else => {}, + } + + return value; + } + + pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { + const test_name = this.scope.tests.items[this.test_id].label; + + var length: usize = 0; + var curr_scope: ?*DescribeScope = this.scope; + while (curr_scope) |scope| { + if (scope.label.len > 0) { + length += scope.label.len + 1; + } + curr_scope = scope.parent; + } + length += test_name.len; + if (hint.len > 0) { + length += hint.len + 2; + } + + var buf = try allocator.alloc(u8, length); + + var index = buf.len; + if (hint.len > 0) { + index -= hint.len; + bun.copy(u8, buf[index..], hint); + index -= test_name.len + 2; + bun.copy(u8, buf[index..], test_name); + bun.copy(u8, buf[index + test_name.len ..], ": "); + } else { + index -= test_name.len; + bun.copy(u8, buf[index..], test_name); + } + // copy describe scopes in reverse order + curr_scope = this.scope; + while (curr_scope) |scope| { + if (scope.label.len > 0) { + index -= scope.label.len + 1; + bun.copy(u8, buf[index..], scope.label); + buf[index + scope.label.len] = ' '; + } + curr_scope = scope.parent; + } + + return buf; + } + + pub fn finalize( + this: *Expect, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const arguments = callframe.arguments(1); + const value = if (arguments.len < 1) JSC.JSValue.jsUndefined() else arguments.ptr[0]; + + var expect = globalObject.bunVM().allocator.create(Expect) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + expect.* = .{ + .scope = Jest.runner.?.pending_test.?.describe, + .test_id = Jest.runner.?.pending_test.?.test_id, + }; + const expect_js_value = expect.toJS(globalObject); + expect_js_value.ensureStillAlive(); + Expect.capturedValueSetCached(expect_js_value, globalObject, value); + expect_js_value.ensureStillAlive(); + expect.postMatch(globalObject); + return expect_js_value; + } + + pub fn constructor( + globalObject: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) ?*Expect { + globalObject.throw("expect() cannot be called with new", .{}); + return null; + } + + /// Object.is() + pub fn toBe( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callframe.this(); + const arguments_ = callframe.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBe() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + const right = arguments[0]; + right.ensureStillAlive(); + const left = this.getValue(globalObject, thisValue, "toBe", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = right.isSameValue(left, globalObject); + if (comptime Environment.allow_assert) { + std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, right.asObjectRef(), left.asObjectRef())); + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + if (not) { + const signature = comptime getSignature("toBe", "expected", true); + const fmt = signature ++ "\n\nExpected: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + + const signature = comptime getSignature("toBe", "expected", false); + if (left.deepEquals(right, globalObject) or left.strictDeepEquals(right, globalObject)) { + const fmt = signature ++ + "\n\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" ++ + "\n\nExpected: {any}\n" ++ + "Received: serializes to the same string\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (right.isString() and left.isString()) { + const diff_format = DiffFormatter{ + .expected = right, + .received = left, + .globalObject = globalObject, + .not = not, + }; + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); + return .zero; + } + + const fmt = signature ++ "\n\nExpected: {any}\nReceived: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + right.toFmt(globalObject, &formatter), + left.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + right.toFmt(globalObject, &formatter), + left.toFmt(globalObject, &formatter), + }); + return .zero; + } + + pub fn toHaveLength( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callframe.this(); + const arguments_ = callframe.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toHaveLength() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected: JSValue = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveLength", "expected") orelse return .zero; + + if (!value.isObject() and !value.isString()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } + + if (!expected.isNumber()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); + return .zero; + } + + const expected_length: f64 = expected.asNumber(); + if (@round(expected_length) != expected_length or std.math.isInf(expected_length) or std.math.isNan(expected_length) or expected_length < 0) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); + + if (actual_length == std.math.inf(f64)) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } else if (std.math.isNan(actual_length)) { + globalObject.throw("Received value has non-number length property: {}", .{actual_length}); + return .zero; + } + + if (actual_length == expected_length) { + pass = true; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + if (not) { + const expected_line = "Expected length: not {d}\n"; + const fmt = comptime getSignature("toHaveLength", "expected", true) ++ "\n\n" ++ expected_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_length}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_length}); + return .zero; + } + + const expected_line = "Expected length: {d}\n"; + const received_line = "Received length: {d}\n"; + const fmt = comptime getSignature("toHaveLength", "expected", false) ++ "\n\n" ++ + expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_length, actual_length }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_length, actual_length }); + return .zero; + } + + pub fn toContain( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callFrame: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toContain() takes 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + expected.ensureStillAlive(); + const value: JSValue = this.getValue(globalObject, thisValue, "toContain", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = false; + + if (value.isIterable(globalObject)) { + var itr = value.arrayIterator(globalObject); + while (itr.next()) |item| { + if (item.isSameValue(expected, globalObject)) { + pass = true; + break; + } + } + } else if (value.isString() and expected.isString()) { + const value_string = value.toString(globalObject).toSlice(globalObject, default_allocator).slice(); + const expected_string = expected.toString(globalObject).toSlice(globalObject, default_allocator).slice(); + if (strings.contains(value_string, expected_string)) { + pass = true; + } else if (value_string.len == 0 and expected_string.len == 0) { // edge case two empty strings are true + pass = true; + } + } else { + globalObject.throw("Received value must be an array type, or both received and expected values must be strings.", .{}); + return .zero; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = expected.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected to contain: not {any}\n"; + const fmt = comptime getSignature("toContain", "expected", true) ++ "\n\n" ++ expected_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_fmt}); + return .zero; + } + + const expected_line = "Expected to contain: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toContain", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeTruthy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeTruthy", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + const truthy = value.toBooleanSlow(globalObject); + if (truthy) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeTruthy", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeTruthy", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeUndefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeUndefined", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + if (value.isUndefined()) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeUndefined", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeUndefined", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeNaN(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeNaN", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + if (value.isNumber()) { + const number = value.asNumber(); + if (number != number) pass = true; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNaN", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNaN", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeNull(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeNull", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = value.isNull(); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNull", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeNull", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeDefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeDefined", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = !value.isUndefined(); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeDefined", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeDefined", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeFalsy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeFalsy", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + const truthy = value.toBooleanSlow(globalObject); + if (!truthy) pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeFalsy", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeFalsy", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toEqual", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.jestDeepEquals(expected, globalObject); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ + .received = value, + .expected = expected, + .globalObject = globalObject, + .not = not, + }; + + if (not) { + const signature = comptime getSignature("toEqual", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toEqual", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + pub fn toStrictEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toStrictEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value: JSValue = this.getValue(globalObject, thisValue, "toStrictEqual", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.jestStrictDeepEquals(expected, globalObject); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not }; + + if (not) { + const signature = comptime getSignature("toStrictEqual", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toStrictEqual", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); + return .zero; + } + + pub fn toHaveProperty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toHaveProperty() requires at least 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected_property_path = arguments[0]; + expected_property_path.ensureStillAlive(); + const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; + if (expected_property) |ev| ev.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveProperty", "path, value") orelse return .zero; + + if (!expected_property_path.isString() and !expected_property_path.isIterable(globalObject)) { + globalObject.throw("Expected path must be a string or an array", .{}); + return .zero; + } + + const not = this.flags.not; + var path_string = ZigString.Empty; + expected_property_path.toZigString(&path_string, globalObject); + + var pass = !value.isUndefinedOrNull(); + var received_property: JSValue = .zero; + + if (pass) { + received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); + pass = !received_property.isEmpty(); + } + + if (pass and expected_property != null) { + pass = received_property.jestDeepEquals(expected_property.?, globalObject); + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + if (not) { + if (expected_property != null) { + const signature = comptime getSignature("toHaveProperty", "path, value", true); + if (!received_property.isEmpty()) { + const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + } + + const signature = comptime getSignature("toHaveProperty", "path", true); + const fmt = signature ++ "\n\nExpected path: not {any}\n\nReceived value: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + received_property.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + expected_property_path.toFmt(globalObject, &formatter), + received_property.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_property != null) { + const signature = comptime getSignature("toHaveProperty", "path, value", false); + if (!received_property.isEmpty()) { + // deep equal case + const fmt = signature ++ "\n\n{any}\n"; + const diff_format = DiffFormatter{ + .received = received_property, + .expected = expected_property.?, + .globalObject = globalObject, + }; + + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); + return .zero; + } + + const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: {any}\n\n" ++ + "Unable to find property\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ + expected_property_path.toFmt(globalObject, &formatter), + expected_property.?.toFmt(globalObject, &formatter), + }); + return .zero; + } + + const signature = comptime getSignature("toHaveProperty", "path", false); + const fmt = signature ++ "\n\nExpected path: {any}\n\nUnable to find property\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_property_path.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_property_path.toFmt(globalObject, &formatter)}); + return .zero; + } + + pub fn toBeEven(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeEven", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + if (value.isAnyInt()) { + const _value = value.toInt64(); + pass = @mod(_value, 2) == 0; + if (_value == -0) { // negative zero is even + pass = true; + } + } else if (value.isBigInt() or value.isBigInt32()) { + const _value = value.toInt64(); + pass = switch (_value == -0) { // negative zero is even + true => true, + else => _value & 1 == 0, + }; + } else if (value.isNumber()) { + const _value = JSValue.asNumber(value); + if (@mod(_value, 1) == 0 and @mod(_value, 2) == 0) { // if the fraction is all zeros and even + pass = true; + } else { + pass = false; + } + } else { + pass = false; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeEven", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeEven", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toBeGreaterThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeGreaterThan() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeGreaterThan", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() > other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .greater_than => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .less_than => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\> {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\> {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThan", "expected", false) ++ "\n\n" ++ + expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeGreaterThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeGreaterThanOrEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeGreaterThanOrEqual", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() >= other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .greater_than, .equal => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .less_than, .equal => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\>= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\>= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeLessThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeLessThan() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeLessThan", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() < other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .less_than => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .greater_than => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\< {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\< {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThan", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeLessThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeLessThanOrEqual() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const other_value = arguments[0]; + other_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeLessThanOrEqual", "expected") orelse return .zero; + + if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { + globalObject.throw("Expected and actual values must be numbers or bigints", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + if (!value.isBigInt() and !other_value.isBigInt()) { + pass = value.asNumber() <= other_value.asNumber(); + } else if (value.isBigInt()) { + pass = switch (value.asBigIntCompare(globalObject, other_value)) { + .less_than, .equal => true, + else => pass, + }; + } else { + pass = switch (other_value.asBigIntCompare(globalObject, value)) { + .greater_than, .equal => true, + else => pass, + }; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + const expected_fmt = other_value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected: not \\<= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected: \\<= {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + return .zero; + } + + pub fn toBeCloseTo(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const thisArguments = callFrame.arguments(2); + const arguments = thisArguments.ptr[0..thisArguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); + return .zero; + } + + const expected_ = arguments[0]; + if (!expected_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "expected", "number"); + return .zero; + } + + var precision: f64 = 2.0; + if (arguments.len > 1) { + const precision_ = arguments[1]; + if (!precision_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "precision", "number"); + return .zero; + } + + precision = precision_.asNumber(); + } + + const received_: JSValue = this.getValue(globalObject, thisValue, "toBeCloseTo", "expected, precision") orelse return .zero; + if (!received_.isNumber()) { + globalObject.throwInvalidArgumentType("expect", "received", "number"); + return .zero; + } + + var expected = expected_.asNumber(); + var received = received_.asNumber(); + + if (std.math.isNegativeInf(expected)) { + expected = -expected; + } + + if (std.math.isNegativeInf(received)) { + received = -received; + } + + if (std.math.isPositiveInf(expected) and std.math.isPositiveInf(received)) { + return thisValue; + } + + const expected_diff = std.math.pow(f64, 10, -precision) / 2; + const actual_diff = std.math.fabs(received - expected); + var pass = actual_diff < expected_diff; + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_fmt = expected_.toFmt(globalObject, &formatter); + const received_fmt = received_.toFmt(globalObject, &formatter); + + const expected_line = "Expected: {any}\n"; + const received_line = "Received: {any}\n"; + const expected_precision = "Expected precision: {d}\n"; + const expected_difference = "Expected difference: \\< {d}\n"; + const received_difference = "Received difference: {d}\n"; + + const suffix_fmt = "\n\n" ++ expected_line ++ received_line ++ "\n" ++ expected_precision ++ expected_difference ++ received_difference; + + if (not) { + const fmt = comptime getSignature("toBeCloseTo", "expected, precision", true) ++ suffix_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + const fmt = comptime getSignature("toBeCloseTo", "expected, precision", false) ++ suffix_fmt; + + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + pub fn toBeOdd(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeOdd", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + if (value.isBigInt32()) { + pass = value.toInt32() & 1 == 1; + } else if (value.isBigInt()) { + pass = value.toInt64() & 1 == 1; + } else if (value.isInt32()) { + const _value = value.toInt32(); + pass = @mod(_value, 2) == 1; + } else if (value.isAnyInt()) { + const _value = value.toInt64(); + pass = @mod(_value, 2) == 1; + } else if (value.isNumber()) { + const _value = JSValue.asNumber(value); + if (@mod(_value, 1) == 0 and @mod(_value, 2) == 1) { // if the fraction is all zeros and odd + pass = true; + } else { + pass = false; + } + } else { + pass = false; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeOdd", "", true) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeOdd", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); + return .zero; + } + + pub fn toThrow(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + active_test_expectation_counter.actual += 1; + + const expected_value: JSValue = if (arguments.len > 0) brk: { + const value = arguments[0]; + if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) { + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + globalObject.throw("Expected value must be string or Error: {any}", .{value.toFmt(globalObject, &fmt)}); + return .zero; + } + break :brk value; + } else .zero; + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toThrow", "expected") orelse return .zero; + + if (!value.jsType().isFunction()) { + globalObject.throw("Expected value must be a function", .{}); + return .zero; + } + + const not = this.flags.not; + + const result_: ?JSValue = brk: { + var vm = globalObject.bunVM(); + var return_value: JSValue = .zero; + var scope = vm.unhandledRejectionScope(); + var prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; + vm.unhandled_pending_rejection_to_capture = &return_value; + vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; + const return_value_from_fucntion: JSValue = value.call(globalObject, &.{}); + vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; + + if (return_value == .zero) { + return_value = return_value_from_fucntion; + } + + if (return_value.asAnyPromise()) |promise| { + globalObject.bunVM().waitForPromise(promise); + scope.apply(vm); + const promise_result = promise.result(globalObject.vm()); + + switch (promise.status(globalObject.vm())) { + .Fulfilled => { + break :brk null; + }, + .Rejected => { + // since we know for sure it rejected, we should always return the error + break :brk promise_result.toError() orelse promise_result; + }, + .Pending => unreachable, + } + } + scope.apply(vm); + + break :brk return_value.toError(); + }; + + const did_throw = result_ != null; + + if (not) { + const signature = comptime getSignature("toThrow", "expected", true); + + if (!did_throw) return thisValue; + + const result: JSValue = result_.?; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (expected_value.isEmpty()) { + const signature_no_args = comptime getSignature("toThrow", "", true); + if (result.toError()) |err| { + const name = err.get(globalObject, "name") orelse JSValue.undefined; + const message = err.get(globalObject, "message") orelse JSValue.undefined; + const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; + globalObject.throwPretty(fmt, .{ + name.toFmt(globalObject, &formatter), + message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + // non error thrown + const fmt = signature_no_args ++ "\n\nThrown value: {any}\n"; + globalObject.throwPretty(fmt, .{result.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.isString()) { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + + // TODO: remove this allocation + // partial match + { + const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; + defer expected_slice.deinit(); + const received_slice = received_message.toSliceOrNull(globalObject) orelse return .zero; + defer received_slice.deinit(); + if (!strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; + } + + const fmt = signature ++ "\n\nExpected substring: not {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ + expected_value.toFmt(globalObject, &formatter), + received_message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_value.isRegExp()) { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + + // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. + if (expected_value.get(globalObject, "test")) |test_fn| { + const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); + if (!matches.toBooleanSlow(globalObject)) return thisValue; + } + + const fmt = signature ++ "\n\nExpected pattern: not {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ + expected_value.toFmt(globalObject, &formatter), + received_message.toFmt(globalObject, &formatter), + }); + return .zero; + } + + if (expected_value.get(globalObject, "message")) |expected_message| { + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + // no partial match for this case + if (!expected_message.isSameValue(received_message, globalObject)) return thisValue; + + const fmt = signature ++ "\n\nExpected message: not {any}\n"; + globalObject.throwPretty(fmt, .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (!result.isInstanceOf(globalObject, expected_value)) return thisValue; + + var expected_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); + const fmt = signature ++ "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); + return .zero; + } + + const signature = comptime getSignature("toThrow", "expected", false); + if (did_throw) { + if (expected_value.isEmpty()) return thisValue; + + const result: JSValue = if (result_.?.toError()) |r| + r + else + result_.?; + + const _received_message: ?JSValue = if (result.isObject()) + result.get(globalObject, "message") + else if (result.toStringOrNull(globalObject)) |js_str| + JSC.JSValue.fromCell(js_str) + else + null; + + if (expected_value.isString()) { + if (_received_message) |received_message| { + // TODO: remove this allocation + // partial match + const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; + defer expected_slice.deinit(); + const received_slice = received_message.toSlice(globalObject, globalObject.allocator()); + defer received_slice.deinit(); + if (strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; + } + + // error: message from received error does not match expected string + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); + return .zero; + } + + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived value: {any}"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + + return .zero; + } + + if (expected_value.isRegExp()) { + if (_received_message) |received_message| { + // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. + if (expected_value.get(globalObject, "test")) |test_fn| { + const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); + if (matches.toBooleanSlow(globalObject)) return thisValue; + } + } + + // error: message from received error does not match expected pattern + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); + + return .zero; + } + + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + // If it's not an object, we are going to crash here. + std.debug.assert(expected_value.isObject()); + + if (expected_value.get(globalObject, "message")) |expected_message| { + if (_received_message) |received_message| { + if (received_message.isSameValue(expected_message, globalObject)) return thisValue; + } + + // error: message from received error does not match expected error message. + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + if (_received_message) |received_message| { + const expected_fmt = expected_message.toFmt(globalObject, &formatter); + const received_fmt = received_message.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\nExpected message: {any}\nReceived message: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + const expected_fmt = expected_message.toFmt(globalObject, &formatter); + const received_fmt = result.toFmt(globalObject, &formatter); + const fmt = signature ++ "\n\nExpected message: {any}\nReceived value: {any}\n"; + globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); + return .zero; + } + + if (result.isInstanceOf(globalObject, expected_value)) return thisValue; + + // error: received error not instance of received error constructor + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + var expected_class = ZigString.Empty; + var received_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + result.getClassName(globalObject, &received_class); + const fmt = signature ++ "\n\nExpected constructor: {s}\nReceived constructor: {s}\n\n"; + + if (_received_message) |received_message| { + const message_fmt = fmt ++ "Received message: {any}\n"; + const received_message_fmt = received_message.toFmt(globalObject, &formatter); + + globalObject.throwPretty(message_fmt, .{ + expected_class, + received_class, + received_message_fmt, + }); + return .zero; + } + + const received_fmt = result.toFmt(globalObject, &formatter); + const value_fmt = fmt ++ "Received value: {any}\n"; + + globalObject.throwPretty(value_fmt, .{ + expected_class, + received_class, + received_fmt, + }); + return .zero; + } + + // did not throw + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + const received_line = "Received function did not throw\n"; + + if (expected_value.isEmpty()) { + const fmt = comptime getSignature("toThrow", "", false) ++ "\n\n" ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{}); + return .zero; + } + + if (expected_value.isString()) { + const expected_fmt = "\n\nExpected substring: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.isRegExp()) { + const expected_fmt = "\n\nExpected pattern: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (expected_value.get(globalObject, "message")) |expected_message| { + const expected_fmt = "\n\nExpected message: {any}\n\n" ++ received_line; + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)}); + return .zero; + } + + const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; + var expected_class = ZigString.Empty; + expected_value.getClassName(globalObject, &expected_class); + const fmt = signature ++ expected_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); + return .zero; + } + + pub fn toMatchSnapshot(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + if (not) { + const signature = comptime getSignature("toMatchSnapshot", "", true); + const fmt = signature ++ "\n\nMatcher error: Snapshot matchers cannot be used with not\n"; + globalObject.throwPretty(fmt, .{}); + } + + var hint_string: ZigString = ZigString.Empty; + var property_matchers: ?JSValue = null; + switch (arguments.len) { + 0 => {}, + 1 => { + if (arguments[0].isString()) { + arguments[0].toZigString(&hint_string, globalObject); + } else if (arguments[0].isObject()) { + property_matchers = arguments[0]; + } + }, + else => { + if (!arguments[0].isObject()) { + const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); + const fmt = signature ++ "\n\nMatcher error: Expected properties must be an object\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + property_matchers = arguments[0]; + + if (arguments[1].isString()) { + arguments[1].toZigString(&hint_string, globalObject); + } + }, + } + + var hint = hint_string.toSlice(default_allocator); + defer hint.deinit(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toMatchSnapshot", "properties, hint") orelse return .zero; + + if (!value.isObject() and property_matchers != null) { + const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); + const fmt = signature ++ "\n\nMatcher error: received values must be an object when the matcher has properties\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (property_matchers) |_prop_matchers| { + var prop_matchers = _prop_matchers; + + if (!value.jestDeepMatch(prop_matchers, globalObject, true)) { + // TODO: print diff with properties from propertyMatchers + const signature = comptime getSignature("toMatchSnapshot", "propertyMatchers", false); + const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ + "\n\nReceived: {any}\n"; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + } + + const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalObject) catch |err| { + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + const test_file_path = Jest.runner.?.files.get(this.scope.file_id).source.path.text; + switch (err) { + error.FailedToOpenSnapshotFile => globalObject.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), + error.FailedToMakeSnapshotDirectory => globalObject.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}), + error.FailedToWriteSnapshotFile => globalObject.throw("Failed write to snapshot file: {s}", .{test_file_path}), + error.ParseError => globalObject.throw("Failed to parse snapshot file for: {s}", .{test_file_path}), + else => globalObject.throw("Failed to snapshot value: {any}", .{value.toFmt(globalObject, &formatter)}), + } + return .zero; + }; + + if (result) |saved_value| { + var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable; + value.jestSnapshotPrettyFormat(&pretty_value, globalObject) catch { + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + globalObject.throw("Failed to pretty format value: {s}", .{value.toFmt(globalObject, &formatter)}); + return .zero; + }; + defer pretty_value.deinit(); + + if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) { + Jest.runner.?.snapshots.passed += 1; + return thisValue; + } + + Jest.runner.?.snapshots.failed += 1; + const signature = comptime getSignature("toMatchSnapshot", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + const diff_format = DiffFormatter{ + .received_string = pretty_value.toOwnedSliceLeaky(), + .expected_string = saved_value, + .globalObject = globalObject, + }; + + globalObject.throwPretty(fmt, .{diff_format}); + return .zero; + } + + return thisValue; + } + + pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalObject, thisValue, "toBeEmpty", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); + + if (actual_length == std.math.inf(f64)) { + if (value.jsTypeLoose().isObject()) { + if (value.isIterable(globalObject)) { + var any_properties_in_iterator = false; + value.forEach(globalObject, &any_properties_in_iterator, struct { + pub fn anythingInIterator( + _: *JSC.VM, + _: *JSGlobalObject, + any_: ?*anyopaque, + _: JSValue, + ) callconv(.C) void { + bun.cast(*bool, any_.?).* = true; + } + }.anythingInIterator); + pass = !any_properties_in_iterator; + } else { + var props_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + + .include_value = true, + }).init(globalObject, value.asObjectRef()); + defer props_iter.deinit(); + pass = props_iter.len == 0; + } + } else { + const signature = comptime getSignature("toBeEmpty", "", false); + const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + } else if (std.math.isNan(actual_length)) { + globalObject.throw("Received value has non-number length property: {}", .{actual_length}); + return .zero; + } else { + pass = actual_length == 0; + } + + if (not and pass) { + const signature = comptime getSignature("toBeEmpty", "", true); + const fmt = signature ++ "\n\nExpected value not to be a string, object, or iterable" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + if (not) pass = !pass; + if (pass) return thisValue; + + if (not) { + const signature = comptime getSignature("toBeEmpty", "", true); + const fmt = signature ++ "\n\nExpected value not to be empty" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + const signature = comptime getSignature("toBeEmpty", "", false); + const fmt = signature ++ "\n\nExpected value to be empty" ++ + "\n\nReceived: {any}\n"; + globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNil", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isUndefinedOrNull() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeArray", "") orelse return .zero; + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeArray() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.jsType().isArray() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeArray", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeArray", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeArrayOfSize", "") orelse return .zero; + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeArrayOfSize() must be called in a test", .{}); + return .zero; + } + + const size = arguments[0]; + size.ensureStillAlive(); + + if (!size.isAnyInt()) { + globalThis.throw("toBeArrayOfSize() requires the first argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = value.jsType().isArray() and @intCast(i32, value.getLength(globalThis)) == size.toInt32(); + + if (not) pass = !pass; + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeArrayOfSize", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeArrayOfSize", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeBoolean", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isBoolean() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toBeTypeOf() must be called in a test", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeTypeOf", "") orelse return .zero; + + const expected = arguments[0]; + expected.ensureStillAlive(); + + const expectedAsStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + active_test_expectation_counter.actual += 1; + + if (!expected.isString()) { + globalThis.throwInvalidArguments("toBeTypeOf() requires a string argument", .{}); + return .zero; + } + + if (!std.mem.eql(u8, expectedAsStr, "function") and + !std.mem.eql(u8, expectedAsStr, "object") and + !std.mem.eql(u8, expectedAsStr, "bigint") and + !std.mem.eql(u8, expectedAsStr, "boolean") and + !std.mem.eql(u8, expectedAsStr, "number") and + !std.mem.eql(u8, expectedAsStr, "string") and + !std.mem.eql(u8, expectedAsStr, "symbol") and + !std.mem.eql(u8, expectedAsStr, "undefined")) + { + globalThis.throwInvalidArguments("toBeTypeOf() requires a valid type string argument ('function', 'object', 'bigint', 'boolean', 'number', 'string', 'symbol', 'undefined')", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + var whatIsTheType: []const u8 = ""; + + // Checking for function/class should be done before everything else, or it will fail. + if (value.isCallable(globalThis.vm())) { + whatIsTheType = "function"; + } else if (value.isObject() or value.jsType().isArray() or value.isNull()) { + whatIsTheType = "object"; + } else if (value.isBigInt()) { + whatIsTheType = "bigint"; + } else if (value.isBoolean()) { + whatIsTheType = "boolean"; + } else if (value.isNumber()) { + whatIsTheType = "number"; + } else if (value.jsType().isString()) { + whatIsTheType = "string"; + } else if (value.isSymbol()) { + whatIsTheType = "symbol"; + } else if (value.isUndefined()) { + whatIsTheType = "undefined"; + } else { + globalThis.throw("Internal consistency error: unknown JSValue type", .{}); + return .zero; + } + + pass = std.mem.eql(u8, expectedAsStr, whatIsTheType); + + if (not) pass = !pass; + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + const expected_str = expected.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTypeOf", "", true) ++ "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); + return .zero; + } + + const fmt = comptime getSignature("toBeTypeOf", "", false) ++ "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); + return .zero; + } + + pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeTrue", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = (value.isBoolean() and value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFalse", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = (value.isBoolean() and !value.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNumber", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isNumber() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeInteger", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isAnyInt() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFinite", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = std.math.isFinite(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBePositive", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeNegative", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num: f64 = value.asNumber(); + pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(2); + const arguments = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toBeWithin", "start, end") orelse return .zero; + + const startValue = arguments[0]; + startValue.ensureStillAlive(); + + if (!startValue.isNumber()) { + globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); + return .zero; + } + + const endValue = arguments[1]; + endValue.ensureStillAlive(); + + if (!endValue.isNumber()) { + globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var pass = value.isNumber(); + if (pass) { + const num = value.asNumber(); + pass = num >= startValue.asNumber() and num < endValue.asNumber(); + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const start_fmt = startValue.toFmt(globalThis, &formatter); + const end_fmt = endValue.toFmt(globalThis, &formatter); + const received_fmt = value.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected: not between {any} (inclusive) and {any} (exclusive)\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeWithin", "start, end", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + const expected_line = "Expected: between {any} (inclusive) and {any} (exclusive)\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toBeWithin", "start, end", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); + return .zero; + } + + pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeSymbol", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isSymbol() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeFunction", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isCallable(globalThis.vm()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeDate", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isDate() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const value: JSValue = this.getValue(globalThis, thisValue, "toBeString", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + const pass = value.isString() != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const received = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{received}); + return .zero; + } + + pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toInclude() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toInclude", "") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.contains(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not include: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toInclude", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to include: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toInclude", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toStartWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toStartWith", "expected") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not start with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toStartWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to start with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toStartWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); + return .zero; + } + + const expected = arguments[0]; + expected.ensureStillAlive(); + + if (!expected.isString()) { + globalThis.throw("toEndWith() requires the first argument to be a string", .{}); + return .zero; + } + + const value: JSValue = this.getValue(globalThis, thisValue, "toEndWith", "expected") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + var pass = value.isString(); + if (pass) { + const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; + } + + const not = this.flags.not; + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const value_fmt = value.toFmt(globalThis, &formatter); + const expected_fmt = expected.toFmt(globalThis, &formatter); + + if (not) { + const expected_line = "Expected to not end with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toEndWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected to end with: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toEndWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_value = arguments[0]; + if (!expected_value.isConstructor()) { + globalObject.throw("Expected value must be a function: {any}", .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toBeInstanceOf", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.isInstanceOf(globalObject, expected_value); + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const value_fmt = value.toFmt(globalObject, &formatter); + if (not) { + const expected_line = "Expected constructor: not {any}\n"; + const received_line = "Received value: {any}\n"; + const fmt = comptime getSignature("toBeInstanceOf", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected constructor: {any}\n"; + const received_line = "Received value: {any}\n"; + const fmt = comptime getSignature("toBeInstanceOf", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toMatch(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_value = arguments[0]; + if (!expected_value.isString() and !expected_value.isRegExp()) { + globalObject.throw("Expected value must be a string or regular expression: {any}", .{expected_value.toFmt(globalObject, &formatter)}); + return .zero; + } + expected_value.ensureStillAlive(); + + const value: JSValue = this.getValue(globalObject, thisValue, "toMatch", "expected") orelse return .zero; + + if (!value.isString()) { + globalObject.throw("Received value must be a string: {any}", .{value.toFmt(globalObject, &formatter)}); + return .zero; + } + + const not = this.flags.not; + var pass: bool = brk: { + if (expected_value.isString()) { + break :brk value.stringIncludes(globalObject, expected_value); + } else if (expected_value.isRegExp()) { + break :brk expected_value.toMatch(globalObject, value); + } + unreachable; + }; + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const expected_fmt = expected_value.toFmt(globalObject, &formatter); + const value_fmt = value.toFmt(globalObject, &formatter); + + if (not) { + const expected_line = "Expected substring or pattern: not {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toMatch", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const expected_line = "Expected substring or pattern: {any}\n"; + const received_line = "Received: {any}\n"; + const fmt = comptime getSignature("toMatch", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; + globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + pub fn toHaveBeenCalled(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + JSC.markBinding(@src()); + const thisValue = callframe.this(); + defer this.postMatch(globalObject); + + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveBeenCalled", "") orelse return .zero; + + const calls = JSMockFunction__getCalls(value); + active_test_expectation_counter.actual += 1; + + if (calls == .zero or !calls.jsType().isArray()) { + globalObject.throw("Expected value must be a mock function: {}", .{value}); + return .zero; + } + + var pass = calls.getLength(globalObject) > 0; + + const not = this.flags.not; + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + if (not) { + const signature = comptime getSignature("toHaveBeenCalled", "", true); + const fmt = signature ++ "\n\nExpected: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } else { + const signature = comptime getSignature("toHaveBeenCalled", "", true); + const fmt = signature ++ "\n\nExpected {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + + unreachable; + } + pub fn toHaveBeenCalledTimes(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + JSC.markBinding(@src()); + + const thisValue = callframe.this(); + const arguments_ = callframe.arguments(1); + const arguments: []const JSValue = arguments_.ptr[0..arguments_.len]; + defer this.postMatch(globalObject); + const value: JSValue = this.getValue(globalObject, thisValue, "toHaveBeenCalledTimes", "expected") orelse return .zero; + + active_test_expectation_counter.actual += 1; + + const calls = JSMockFunction__getCalls(value); + + if (calls == .zero or !calls.jsType().isArray()) { + globalObject.throw("Expected value must be a mock function: {}", .{value}); + return .zero; + } + + if (arguments.len < 1 or !arguments[0].isAnyInt()) { + globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 integer argument", .{}); + return .zero; + } + + const times = arguments[0].coerce(i32, globalObject); + + var pass = @intCast(i32, calls.getLength(globalObject)) == times; + + const not = this.flags.not; + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + if (not) { + const signature = comptime getSignature("toHaveBeenCalledTimes", "expected", true); + const fmt = signature ++ "\n\nExpected: not {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } else { + const signature = comptime getSignature("toHaveBeenCalledTimes", "expected", true); + const fmt = signature ++ "\n\nExpected {any}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); + return .zero; + } + + unreachable; + } + + pub fn toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + + defer this.postMatch(globalObject); + const thisValue = callFrame.this(); + const args = callFrame.arguments(1).slice(); + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + + const received_object: JSValue = this.getValue(globalObject, thisValue, "toMatchObject", "expected") orelse return .zero; + + if (!received_object.isObject()) { + const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; + if (not) { + const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (args.len < 1 or !args[0].isObject()) { + const matcher_error = "\n\nMatcher error: expected value must be a non-null object\n"; + if (not) { + const fmt = comptime getSignature("toMatchObject", "", true) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + const fmt = comptime getSignature("toMatchObject", "", false) ++ matcher_error; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const property_matchers = args[0]; + + var pass = received_object.jestDeepMatch(property_matchers, globalObject, true); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + const diff_formatter = DiffFormatter{ + .received = received_object, + .expected = property_matchers, + .globalObject = globalObject, + .not = not, + }; + + if (not) { + const signature = comptime getSignature("toMatchObject", "expected", true); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + const signature = comptime getSignature("toMatchObject", "expected", false); + const fmt = signature ++ "\n\n{any}\n"; + globalObject.throwPretty(fmt, .{diff_formatter}); + return .zero; + } + + pub const toHaveBeenCalledWith = notImplementedJSCFn; + pub const toHaveBeenLastCalledWith = notImplementedJSCFn; + pub const toHaveBeenNthCalledWith = notImplementedJSCFn; + pub const toHaveReturnedTimes = notImplementedJSCFn; + pub const toHaveReturnedWith = notImplementedJSCFn; + pub const toHaveLastReturnedWith = notImplementedJSCFn; + pub const toHaveNthReturnedWith = notImplementedJSCFn; + pub const toContainEqual = notImplementedJSCFn; + pub const toMatchInlineSnapshot = notImplementedJSCFn; + pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; + pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; + + pub const getStaticNot = notImplementedStaticProp; + pub const getStaticResolves = notImplementedStaticProp; + pub const getStaticRejects = notImplementedStaticProp; + + pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAny.call(globalObject, callFrame); + } + + pub fn anything(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAnything.call(globalObject, callFrame); + } + + pub fn stringContaining(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectStringContaining.call(globalObject, callFrame); + } + + pub fn stringMatching(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectStringMatching.call(globalObject, callFrame); + } + + pub const extend = notImplementedStaticFn; + pub const arrayContaining = notImplementedStaticFn; + pub const assertions = notImplementedStaticFn; + pub const hasAssertions = notImplementedStaticFn; + pub const objectContaining = notImplementedStaticFn; + pub const addSnapshotSerializer = notImplementedStaticFn; + + pub fn notImplementedJSCFn(_: *Expect, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedStaticFn(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedJSCProp(_: *Expect, _: JSC.JSValue, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn notImplementedStaticProp(globalObject: *JSC.JSGlobalObject, _: JSC.JSValue, _: JSC.JSValue) callconv(.C) JSC.JSValue { + globalObject.throw("Not implemented", .{}); + return .zero; + } + + pub fn postMatch(_: *Expect, globalObject: *JSC.JSGlobalObject) void { + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + } +}; + +pub const ExpectAnything = struct { + pub usingnamespace JSC.Codegen.JSExpectAnything; + + pub fn finalize( + this: *ExpectAnything, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + const anything = globalObject.bunVM().allocator.create(ExpectAnything) catch unreachable; + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.anything() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const anything_js_value = anything.toJS(globalObject); + anything_js_value.ensureStillAlive(); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + + return anything_js_value; + } +}; + +pub const ExpectStringMatching = struct { + pub usingnamespace JSC.Codegen.JSExpectStringMatching; + + pub fn finalize( + this: *ExpectStringMatching, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + const args = callFrame.arguments(1).slice(); + + if (args.len == 0 or (!args[0].isString() and !args[0].isRegExp())) { + const fmt = "expect.stringContaining(string)\n\nExpected a string or regular expression\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const test_value = args[0]; + const string_matching = globalObject.bunVM().allocator.create(ExpectStringMatching) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const string_matching_js_value = string_matching.toJS(globalObject); + ExpectStringMatching.testValueSetCached(string_matching_js_value, globalObject, test_value); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + return string_matching_js_value; + } +}; + +pub const ExpectStringContaining = struct { + pub usingnamespace JSC.Codegen.JSExpectStringContaining; + + pub fn finalize( + this: *ExpectStringContaining, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + const args = callFrame.arguments(1).slice(); + + if (args.len == 0 or !args[0].isString()) { + const fmt = "expect.stringContaining(string)\n\nExpected a string\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + const string_value = args[0]; + + const string_containing = globalObject.bunVM().allocator.create(ExpectStringContaining) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + const string_containing_js_value = string_containing.toJS(globalObject); + ExpectStringContaining.stringValueSetCached(string_containing_js_value, globalObject, string_value); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + return string_containing_js_value; + } +}; + +pub const ExpectAny = struct { + pub usingnamespace JSC.Codegen.JSExpectAny; + + pub fn finalize( + this: *ExpectAny, + ) callconv(.C) void { + VirtualMachine.get().allocator.destroy(this); + } + + pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len == 0) { + globalObject.throw("any() expects to be passed a constructor function.", .{}); + return .zero; + } + + const constructor = arguments[0]; + constructor.ensureStillAlive(); + if (!constructor.isConstructor()) { + const fmt = "expect.any(constructor)\n\nExpected a constructor\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + var any = globalObject.bunVM().allocator.create(ExpectAny) catch unreachable; + + if (Jest.runner.?.pending_test == null) { + const err = globalObject.createErrorInstance("expect.any() must be called in a test", .{}); + err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); + globalObject.throwValue(err); + return .zero; + } + + any.* = .{}; + const any_js_value = any.toJS(globalObject); + any_js_value.ensureStillAlive(); + ExpectAny.constructorValueSetCached(any_js_value, globalObject, constructor); + any_js_value.ensureStillAlive(); + + var vm = globalObject.bunVM(); + vm.autoGarbageCollect(); + + return any_js_value; + } +}; + +/// JSValue.zero is used to indicate it was not a JSMockFunction +/// If there were no calls, it returns an empty JSArray* +extern fn JSMockFunction__getCalls(JSValue) JSValue; + +/// JSValue.zero is used to indicate it was not a JSMockFunction +/// If there were no calls, it returns an empty JSArray* +extern fn JSMockFunction__getReturns(JSValue) JSValue; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 2dc4ae1c4..a1a4f2af8 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -8,7 +8,12 @@ const MimeType = @import("../../http.zig").MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = @import("root").bun.HTTP; const NetworkThread = HTTPClient.NetworkThread; -const Environment = @import("../../env.zig"); +const Environment = bun.Environment; + +const Snapshots = @import("./snapshot.zig").Snapshots; +const expect = @import("./expect.zig"); +const Counter = expect.Counter; +const Expect = expect.Expect; const DiffFormatter = @import("./diff_format.zig").DiffFormatter; @@ -28,8 +33,6 @@ const default_allocator = @import("root").bun.default_allocator; const FeatureFlags = @import("root").bun.FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; -const d = @import("../base.zig").d; -const castObj = @import("../base.zig").castObj; const getAllocator = @import("../base.zig").getAllocator; const ZigString = JSC.ZigString; @@ -43,12 +46,10 @@ const JSObject = JSC.JSObject; const CallFrame = JSC.CallFrame; const VirtualMachine = JSC.VirtualMachine; -const Task = @import("../javascript.zig").Task; - -const Fs = @import("../../fs.zig"); +const Fs = bun.fs; const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; -const ArrayIdentityContext = @import("../../identity_context.zig").ArrayIdentityContext; +const ArrayIdentityContext = bun.ArrayIdentityContext; pub var test_elapsed_timer: ?*std.time.Timer = null; pub const Tag = enum(u3) { @@ -252,4041 +253,256 @@ pub const TestRunner = struct { pass, fail, skip, - todo, - fail_because_todo_passed, - }; - }; -}; - -pub const Snapshots = struct { - const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; - pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); - - allocator: std.mem.Allocator, - update_snapshots: bool, - total: usize = 0, - added: usize = 0, - passed: usize = 0, - failed: usize = 0, - - file_buf: *std.ArrayList(u8), - values: *ValuesHashMap, - counts: *bun.StringHashMap(usize), - _current_file: ?File = null, - snapshot_dir_path: ?string = null, - - const File = struct { - id: TestRunner.File.ID, - file: std.fs.File, - }; - - pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { - switch (try this.getSnapshotFile(expect.scope.file_id)) { - .result => {}, - .err => |err| { - return switch (err.syscall) { - .mkdir => error.FailedToMakeSnapshotDirectory, - .open => error.FailedToOpenSnapshotFile, - else => error.SnapshotFailed, - }; - }, - } - - const snapshot_name = try expect.getSnapshotName(this.allocator, hint); - this.total += 1; - - var count_entry = try this.counts.getOrPut(snapshot_name); - const counter = brk: { - if (count_entry.found_existing) { - this.allocator.free(snapshot_name); - count_entry.value_ptr.* += 1; - break :brk count_entry.value_ptr.*; - } - count_entry.value_ptr.* = 1; - break :brk count_entry.value_ptr.*; - }; - - const name = count_entry.key_ptr.*; - - var counter_string_buf = [_]u8{0} ** 32; - var counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); - - var name_with_counter = try this.allocator.alloc(u8, name.len + 1 + counter_string.len); - defer this.allocator.free(name_with_counter); - bun.copy(u8, name_with_counter[0..name.len], name); - name_with_counter[name.len] = ' '; - bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); - - const name_hash = bun.hash(name_with_counter); - if (this.values.get(name_hash)) |expected| { - return expected; - } - - // doesn't exist. append to file bytes and add to hashmap. - var pretty_value = try MutableString.init(this.allocator, 0); - try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); - - const serialized_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; - try this.file_buf.ensureUnusedCapacity(serialized_length); - this.file_buf.appendSliceAssumeCapacity("\nexports[`"); - this.file_buf.appendSliceAssumeCapacity(name_with_counter); - this.file_buf.appendSliceAssumeCapacity("`] = `"); - this.file_buf.appendSliceAssumeCapacity(pretty_value.list.items); - this.file_buf.appendSliceAssumeCapacity("`;\n"); - - this.added += 1; - try this.values.put(name_hash, pretty_value.toOwnedSlice()); - return null; - } - - pub fn parseFile(this: *Snapshots) !void { - if (this.file_buf.items.len == 0) return; - - const vm = VirtualMachine.get(); - var opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); - var temp_log = logger.Log.init(this.allocator); - - const test_file = Jest.runner.?.files.get(this._current_file.?.id); - const test_filename = test_file.source.path.name.filename; - const dir_path = test_file.source.path.name.dirWithTrailingSlash(); - - var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; - bun.copy(u8, remain, dir_path); - remain = remain[dir_path.len..]; - bun.copy(u8, remain, "__snapshots__/"); - remain = remain["__snapshots__/".len..]; - bun.copy(u8, remain, test_filename); - remain = remain[test_filename.len..]; - bun.copy(u8, remain, ".snap"); - remain = remain[".snap".len..]; - remain[0] = 0; - const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - - const source = logger.Source.initPathString(snapshot_file_path, this.file_buf.items); - - var parser = try js_parser.Parser.init( - opts, - &temp_log, - &source, - vm.bundler.options.define, - this.allocator, - ); - - var parse_result = try parser.parse(); - var ast = if (parse_result == .ast) parse_result.ast else return error.ParseError; - defer ast.deinit(); - - if (ast.exports_ref.isNull()) return; - const exports_ref = ast.exports_ref; - - // TODO: when common js transform changes, keep this updated or add flag to support this version - - const export_default = brk: { - for (ast.parts.slice()) |part| { - for (part.stmts) |stmt| { - if (stmt.data == .s_export_default and stmt.data.s_export_default.value == .expr) { - break :brk stmt.data.s_export_default.value.expr; - } - } - } - - return; - }; - - if (export_default.data == .e_call) { - const function_call = export_default.data.e_call; - if (function_call.args.len == 2 and function_call.args.ptr[0].data == .e_function) { - const arg_function_stmts = function_call.args.ptr[0].data.e_function.func.body.stmts; - for (arg_function_stmts) |stmt| { - switch (stmt.data) { - .s_expr => |expr| { - if (expr.value.data == .e_binary and expr.value.data.e_binary.op == .bin_assign) { - const left = expr.value.data.e_binary.left; - if (left.data == .e_index and left.data.e_index.index.data == .e_string and left.data.e_index.target.data == .e_identifier) { - const target: js_ast.E.Identifier = left.data.e_index.target.data.e_identifier; - var index: *js_ast.E.String = left.data.e_index.index.data.e_string; - if (target.ref.eql(exports_ref) and expr.value.data.e_binary.right.data == .e_string) { - const key = index.slice(this.allocator); - var value_string = expr.value.data.e_binary.right.data.e_string; - const value = value_string.slice(this.allocator); - defer { - if (!index.isUTF8()) this.allocator.free(key); - if (!value_string.isUTF8()) this.allocator.free(value); - } - const value_clone = try this.allocator.alloc(u8, value.len); - bun.copy(u8, value_clone, value); - const name_hash = bun.hash(key); - try this.values.put(name_hash, value_clone); - } - } - } - }, - else => {}, - } - } - } - } - } - - pub fn writeSnapshotFile(this: *Snapshots) !void { - if (this._current_file) |_file| { - var file = _file; - file.file.writeAll(this.file_buf.items) catch { - return error.FailedToWriteSnapshotFile; - }; - file.file.close(); - this.file_buf.clearAndFree(); - - var value_itr = this.values.valueIterator(); - while (value_itr.next()) |value| { - this.allocator.free(value.*); - } - this.values.clearAndFree(); - - var count_key_itr = this.counts.keyIterator(); - while (count_key_itr.next()) |key| { - this.allocator.free(key.*); - } - this.counts.clearAndFree(); - } - } - - fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { - if (this._current_file == null or this._current_file.?.id != file_id) { - try this.writeSnapshotFile(); - - const test_file = Jest.runner.?.files.get(file_id); - const test_filename = test_file.source.path.name.filename; - const dir_path = test_file.source.path.name.dirWithTrailingSlash(); - - var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; - bun.copy(u8, remain, dir_path); - remain = remain[dir_path.len..]; - bun.copy(u8, remain, "__snapshots__/"); - remain = remain["__snapshots__/".len..]; - - if (this.snapshot_dir_path == null or !strings.eqlLong(dir_path, this.snapshot_dir_path.?, true)) { - remain[0] = 0; - const snapshot_dir_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - switch (JSC.Node.Syscall.mkdir(snapshot_dir_path, 0o777)) { - .result => this.snapshot_dir_path = dir_path, - .err => |err| { - switch (err.getErrno()) { - std.os.E.EXIST => this.snapshot_dir_path = dir_path, - else => return JSC.Maybe(void){ - .err = err, - }, - } - }, - } - } - - bun.copy(u8, remain, test_filename); - remain = remain[test_filename.len..]; - bun.copy(u8, remain, ".snap"); - remain = remain[".snap".len..]; - remain[0] = 0; - const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; - - var flags: JSC.Node.Mode = std.os.O.CREAT | std.os.O.RDWR; - if (this.update_snapshots) flags |= std.os.O.TRUNC; - const fd = switch (JSC.Node.Syscall.open(snapshot_file_path, flags, 0o644)) { - .result => |_fd| _fd, - .err => |err| return JSC.Maybe(void){ - .err = err, - }, - }; - - var file: File = .{ - .id = file_id, - .file = .{ .handle = fd }, - }; - - if (this.update_snapshots) { - try this.file_buf.appendSlice(file_header); - } else { - const length = try file.file.getEndPos(); - if (length == 0) { - try this.file_buf.appendSlice(file_header); - } else { - const buf = try this.allocator.alloc(u8, length); - _ = try file.file.preadAll(buf, 0); - try this.file_buf.appendSlice(buf); - this.allocator.free(buf); - } - } - - this._current_file = file; - try this.parseFile(); - } - - return JSC.Maybe(void).success; - } -}; - -pub const Jest = struct { - pub var runner: ?*TestRunner = null; - - fn globalHook(comptime name: string) JSC.JSHostFunctionType { - return struct { - pub fn appendGlobalFunctionCallback( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSValue { - const arguments = callframe.arguments(2); - if (arguments.len < 1) { - globalThis.throwNotEnoughArguments("callback", 1, arguments.len); - return .zero; - } - - const function = arguments.ptr[0]; - if (function.isEmptyOrUndefinedOrNull() or !function.isCallable(globalThis.vm())) { - globalThis.throwInvalidArgumentType(name, "callback", "function"); - return .zero; - } - - if (function.getLength(globalThis) > 0) { - globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); - return .zero; - } - - function.protect(); - @field(Jest.runner.?.global_callbacks, name).append( - bun.default_allocator, - function, - ) catch unreachable; - return JSC.JSValue.jsUndefined(); - } - }.appendGlobalFunctionCallback; - } - - pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - var global_hooks_object = JSC.JSValue.createEmptyObject(globalObject, 8); - global_hooks_object.ensureStillAlive(); - - const notSupportedHereFn = struct { - pub fn notSupportedHere( - globalThis: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) callconv(.C) JSValue { - globalThis.throw("This function can only be used in a test.", .{}); - return .zero; - } - }.notSupportedHere; - const notSupportedHere = JSC.NewFunction(globalObject, null, 0, notSupportedHereFn, false); - notSupportedHere.ensureStillAlive(); - - inline for (.{ - "expect", - "describe", - "it", - "test", - }) |name| { - global_hooks_object.put(globalObject, ZigString.static(name), notSupportedHere); - } - - inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { - const function = JSC.NewFunction(globalObject, null, 1, globalHook(name), false); - function.ensureStillAlive(); - global_hooks_object.put(globalObject, ZigString.static(name), function); - } - return global_hooks_object; - } - - pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - const module = JSC.JSValue.createEmptyObject(globalObject, 11); - - const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); - module.put( - globalObject, - ZigString.static("test"), - test_fn, - ); - test_fn.put( - globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), - ); - test_fn.put( - globalObject, - ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, TestScope.skip, false), - ); - test_fn.put( - globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), - ); - test_fn.put( - globalObject, - ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), - ); - test_fn.put( - globalObject, - ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), - ); - - module.put( - globalObject, - ZigString.static("it"), - test_fn, - ); - const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); - describe.put( - globalObject, - ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), - ); - describe.put( - globalObject, - ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), - ); - describe.put( - globalObject, - ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), - ); - describe.put( - globalObject, - ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), - ); - describe.put( - globalObject, - ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), - ); - - module.put( - globalObject, - ZigString.static("describe"), - describe, - ); - - module.put( - globalObject, - ZigString.static("beforeAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeAll"), 1, DescribeScope.beforeAll, false), - ); - module.put( - globalObject, - ZigString.static("beforeEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeEach"), 1, DescribeScope.beforeEach, false), - ); - module.put( - globalObject, - ZigString.static("afterAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterAll"), 1, DescribeScope.afterAll, false), - ); - module.put( - globalObject, - ZigString.static("afterEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterEach"), 1, DescribeScope.afterEach, false), - ); - module.put( - globalObject, - ZigString.static("expect"), - Expect.getConstructor(globalObject), - ); - - const mock_fn = JSMockFunction__createObject(globalObject); - const spyOn = JSC.NewFunction(globalObject, ZigString.static("spyOn"), 2, JSMock__spyOn, false); - const restoreAllMocks = JSC.NewFunction(globalObject, ZigString.static("restoreAllMocks"), 2, jsFunctionResetSpies, false); - module.put(globalObject, ZigString.static("mock"), mock_fn); - - const jest = JSValue.createEmptyObject(globalObject, 3); - jest.put(globalObject, ZigString.static("fn"), mock_fn); - jest.put(globalObject, ZigString.static("spyOn"), spyOn); - jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks); - module.put(globalObject, ZigString.static("jest"), jest); - module.put(globalObject, ZigString.static("spyOn"), spyOn); - - const vi = JSValue.createEmptyObject(globalObject, 1); - vi.put(globalObject, ZigString.static("fn"), mock_fn); - module.put(globalObject, ZigString.static("vi"), vi); - - return module; - } - - extern fn JSMockFunction__createObject(*JSC.JSGlobalObject) JSC.JSValue; - - extern fn Bun__Jest__testPreloadObject(*JSC.JSGlobalObject) JSC.JSValue; - extern fn Bun__Jest__testModuleObject(*JSC.JSGlobalObject) JSC.JSValue; - extern fn jsFunctionResetSpies(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; - extern fn JSMock__spyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; - - pub fn call( - _: void, - ctx: js.JSContextRef, - _: js.JSObjectRef, - _: js.JSObjectRef, - arguments_: []const js.JSValueRef, - exception: js.ExceptionRef, - ) js.JSValueRef { - JSC.markBinding(@src()); - var runner_ = runner orelse { - JSError(getAllocator(ctx), "Run \"bun test\" to run a test", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - }; - const arguments = @ptrCast([]const JSC.JSValue, arguments_); - - if (arguments.len < 1 or !arguments[0].isString()) { - JSError(getAllocator(ctx), "Bun.jest() expects a string filename", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - } - var str = arguments[0].toSlice(ctx, bun.default_allocator); - defer str.deinit(); - var slice = str.slice(); - - if (str.len == 0 or slice[0] != '/') { - JSError(getAllocator(ctx), "Bun.jest() expects an absolute file path", .{}, ctx, exception); - return js.JSValueMakeUndefined(ctx); - } - var vm = ctx.bunVM(); - if (vm.is_in_preload) { - return Bun__Jest__testPreloadObject(ctx).asObjectRef(); - } - - var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - - var scope = runner_.getOrPutFile(filepath); - DescribeScope.active = scope; - DescribeScope.module = scope; - - return Bun__Jest__testModuleObject(ctx).asObjectRef(); - } - - comptime { - if (!JSC.is_bindgen) { - @export(Bun__Jest__createTestModuleObject, .{ .name = "Bun__Jest__createTestModuleObject" }); - @export(Bun__Jest__createTestPreloadObject, .{ .name = "Bun__Jest__createTestPreloadObject" }); - } - } -}; - -pub const ExpectAnything = struct { - pub usingnamespace JSC.Codegen.JSExpectAnything; - - pub fn finalize( - this: *ExpectAnything, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { - const anything = globalObject.bunVM().allocator.create(ExpectAnything) catch unreachable; - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.anything() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const anything_js_value = anything.toJS(globalObject); - anything_js_value.ensureStillAlive(); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - - return anything_js_value; - } -}; - -pub const ExpectStringMatching = struct { - pub usingnamespace JSC.Codegen.JSExpectStringMatching; - - pub fn finalize( - this: *ExpectStringMatching, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - const args = callFrame.arguments(1).slice(); - - if (args.len == 0 or (!args[0].isString() and !args[0].isRegExp())) { - const fmt = "expect.stringContaining(string)\n\nExpected a string or regular expression\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - const test_value = args[0]; - const string_matching = globalObject.bunVM().allocator.create(ExpectStringMatching) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const string_matching_js_value = string_matching.toJS(globalObject); - ExpectStringMatching.testValueSetCached(string_matching_js_value, globalObject, test_value); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - return string_matching_js_value; - } -}; - -pub const ExpectStringContaining = struct { - pub usingnamespace JSC.Codegen.JSExpectStringContaining; - - pub fn finalize( - this: *ExpectStringContaining, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - const args = callFrame.arguments(1).slice(); - - if (args.len == 0 or !args[0].isString()) { - const fmt = "expect.stringContaining(string)\n\nExpected a string\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - const string_value = args[0]; - - const string_containing = globalObject.bunVM().allocator.create(ExpectStringContaining) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.stringContaining() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - const string_containing_js_value = string_containing.toJS(globalObject); - ExpectStringContaining.stringValueSetCached(string_containing_js_value, globalObject, string_value); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - return string_containing_js_value; - } -}; -pub const ExpectAny = struct { - pub usingnamespace JSC.Codegen.JSExpectAny; - - pub fn finalize( - this: *ExpectAny, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len == 0) { - globalObject.throw("any() expects to be passed a constructor function.", .{}); - return .zero; - } - - const constructor = arguments[0]; - constructor.ensureStillAlive(); - if (!constructor.isConstructor()) { - const fmt = "expect.any(constructor)\n\nExpected a constructor\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - var any = globalObject.bunVM().allocator.create(ExpectAny) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect.any() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - any.* = .{}; - const any_js_value = any.toJS(globalObject); - any_js_value.ensureStillAlive(); - ExpectAny.constructorValueSetCached(any_js_value, globalObject, constructor); - any_js_value.ensureStillAlive(); - - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); - - return any_js_value; - } -}; - -/// https://jestjs.io/docs/expect -// To support async tests, we need to track the test ID -pub const Expect = struct { - test_id: TestRunner.Test.ID, - scope: *DescribeScope, - op: Op.Set = Op.Set.init(.{}), - - pub usingnamespace JSC.Codegen.JSExpect; - - pub const Op = enum(u3) { - resolves, - rejects, - not, - pub const Set = std.EnumSet(Op); - }; - - pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { - const test_name = this.scope.tests.items[this.test_id].label; - - var length: usize = 0; - var curr_scope: ?*DescribeScope = this.scope; - while (curr_scope) |scope| { - if (scope.label.len > 0) { - length += scope.label.len + 1; - } - curr_scope = scope.parent; - } - length += test_name.len; - if (hint.len > 0) { - length += hint.len + 2; - } - - var buf = try allocator.alloc(u8, length); - - var index = buf.len; - if (hint.len > 0) { - index -= hint.len; - bun.copy(u8, buf[index..], hint); - index -= test_name.len + 2; - bun.copy(u8, buf[index..], test_name); - bun.copy(u8, buf[index + test_name.len ..], ": "); - } else { - index -= test_name.len; - bun.copy(u8, buf[index..], test_name); - } - // copy describe scopes in reverse order - curr_scope = this.scope; - while (curr_scope) |scope| { - if (scope.label.len > 0) { - index -= scope.label.len + 1; - bun.copy(u8, buf[index..], scope.label); - buf[index + scope.label.len] = ' '; - } - curr_scope = scope.parent; - } - - return buf; - } - - pub fn finalize( - this: *Expect, - ) callconv(.C) void { - VirtualMachine.get().allocator.destroy(this); - } - - pub fn call(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const arguments = callframe.arguments(1); - const value = if (arguments.len < 1) JSC.JSValue.jsUndefined() else arguments.ptr[0]; - - var expect = globalObject.bunVM().allocator.create(Expect) catch unreachable; - - if (Jest.runner.?.pending_test == null) { - const err = globalObject.createErrorInstance("expect() must be called in a test", .{}); - err.put(globalObject, ZigString.static("name"), ZigString.init("TestNotRunningError").toValueGC(globalObject)); - globalObject.throwValue(err); - return .zero; - } - - expect.* = .{ - .scope = Jest.runner.?.pending_test.?.describe, - .test_id = Jest.runner.?.pending_test.?.test_id, - }; - const expect_js_value = expect.toJS(globalObject); - expect_js_value.ensureStillAlive(); - JSC.Jest.Expect.capturedValueSetCached(expect_js_value, globalObject, value); - expect_js_value.ensureStillAlive(); - expect.postMatch(globalObject); - return expect_js_value; - } - - pub fn constructor( - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) ?*Expect { - _ = callframe.arguments(1); - globalObject.throw("expect() cannot be called with new", .{}); - return null; - } - - /// Object.is() - pub fn toBe( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callframe.this(); - const arguments_ = callframe.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBe() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBe() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - const right = arguments[0]; - right.ensureStillAlive(); - const left = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - left.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = right.isSameValue(left, globalObject); - if (comptime Environment.allow_assert) { - std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, right.asObjectRef(), left.asObjectRef())); - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - const signature = comptime getSignature("toBe", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - - const signature = comptime getSignature("toBe", "expected", false); - if (left.deepEquals(right, globalObject) or left.strictDeepEquals(right, globalObject)) { - const fmt = signature ++ - "\n\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" ++ - "\n\nExpected: {any}\n" ++ - "Received: serializes to the same string\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (right.isString() and left.isString()) { - const diff_format = DiffFormatter{ - .expected = right, - .received = left, - .globalObject = globalObject, - .not = not, - }; - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); - return .zero; - } - - const fmt = signature ++ "\n\nExpected: {any}\nReceived: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - right.toFmt(globalObject, &formatter), - left.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - right.toFmt(globalObject, &formatter), - left.toFmt(globalObject, &formatter), - }); - return .zero; - } - - pub fn getSignature(comptime matcher_name: string, comptime args: string, comptime not: bool) string { - const received = "expect(received)."; - comptime if (not) { - return received ++ "not." ++ matcher_name ++ "(" ++ args ++ ")"; - }; - return received ++ matcher_name ++ "(" ++ args ++ ")"; - } - - pub fn toHaveLength( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callframe.this(); - const arguments_ = callframe.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toHaveLength() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toHaveLength() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected: JSValue = arguments[0]; - const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!value.isObject() and !value.isString()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } - - if (!expected.isNumber()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); - return .zero; - } - - const expected_length: f64 = expected.asNumber(); - if (@round(expected_length) != expected_length or std.math.isInf(expected_length) or std.math.isNan(expected_length) or expected_length < 0) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be a non-negative integer: {any}", .{expected.toFmt(globalObject, &fmt)}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); - - if (actual_length == std.math.inf(f64)) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } else if (std.math.isNan(actual_length)) { - globalObject.throw("Received value has non-number length property: {}", .{actual_length}); - return .zero; - } - - if (actual_length == expected_length) { - pass = true; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - if (not) { - const expected_line = "Expected length: not {d}\n"; - const fmt = comptime getSignature("toHaveLength", "expected", true) ++ "\n\n" ++ expected_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_length}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_length}); - return .zero; - } - - const expected_line = "Expected length: {d}\n"; - const received_line = "Received length: {d}\n"; - const fmt = comptime getSignature("toHaveLength", "expected", false) ++ "\n\n" ++ - expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_length, actual_length }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_length, actual_length }); - return .zero; - } - - pub fn toContain( - this: *Expect, - globalObject: *JSC.JSGlobalObject, - callFrame: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toContain() takes 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toContain() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - expected.ensureStillAlive(); - const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = false; - - if (value.isIterable(globalObject)) { - var itr = value.arrayIterator(globalObject); - while (itr.next()) |item| { - if (item.isSameValue(expected, globalObject)) { - pass = true; - break; - } - } - } else if (value.isString() and expected.isString()) { - const value_string = value.toString(globalObject).toSlice(globalObject, default_allocator).slice(); - const expected_string = expected.toString(globalObject).toSlice(globalObject, default_allocator).slice(); - if (strings.contains(value_string, expected_string)) { - pass = true; - } else if (value_string.len == 0 and expected_string.len == 0) { // edge case two empty strings are true - pass = true; - } - } else { - globalObject.throw("Received value must be an array type, or both received and expected values must be strings.", .{}); - return .zero; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = expected.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected to contain: not {any}\n"; - const fmt = comptime getSignature("toContain", "expected", true) ++ "\n\n" ++ expected_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_fmt}); - return .zero; - } - - const expected_line = "Expected to contain: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toContain", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeTruthy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeTruthy() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - const truthy = value.toBooleanSlow(globalObject); - if (truthy) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeTruthy", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeTruthy", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeUndefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - if (value.isUndefined()) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeUndefined", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeUndefined", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeNaN(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - if (value.isNumber()) { - const number = value.asNumber(); - if (number != number) pass = true; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNaN", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNaN", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeNull(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = value.isNull(); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNull", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeNull", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeDefined(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = !value.isUndefined(); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeDefined", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeDefined", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeFalsy(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeFalsy() must be called in a test", .{}); - return .zero; - } - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - const truthy = value.toBooleanSlow(globalObject); - if (!truthy) pass = true; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeFalsy", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeFalsy", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.jestDeepEquals(expected, globalObject); - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const diff_formatter = DiffFormatter{ - .received = value, - .expected = expected, - .globalObject = globalObject, - .not = not, - }; - - if (not) { - const signature = comptime getSignature("toEqual", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } - - const signature = comptime getSignature("toEqual", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } - - pub fn toStrictEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toStrictEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toStrictEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected = arguments[0]; - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.jestStrictDeepEquals(expected, globalObject); - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not }; - - if (not) { - const signature = comptime getSignature("toStrictEqual", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); - return .zero; - } - - const signature = comptime getSignature("toStrictEqual", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter}); - return .zero; - } - - pub fn toHaveProperty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toHaveProperty() requires at least 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toHaveProperty must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected_property_path = arguments[0]; - expected_property_path.ensureStillAlive(); - const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; - if (expected_property) |ev| ev.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!expected_property_path.isString() and !expected_property_path.isIterable(globalObject)) { - globalObject.throw("Expected path must be a string or an array", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var path_string = ZigString.Empty; - expected_property_path.toZigString(&path_string, globalObject); - - var pass = !value.isUndefinedOrNull(); - var received_property: JSValue = .zero; - - if (pass) { - received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path); - pass = !received_property.isEmpty(); - } - - if (pass and expected_property != null) { - pass = received_property.jestDeepEquals(expected_property.?, globalObject); - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - if (expected_property != null) { - const signature = comptime getSignature("toHaveProperty", "path, value", true); - if (!received_property.isEmpty()) { - const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - } - - const signature = comptime getSignature("toHaveProperty", "path", true); - const fmt = signature ++ "\n\nExpected path: not {any}\n\nReceived value: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - received_property.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - expected_property_path.toFmt(globalObject, &formatter), - received_property.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_property != null) { - const signature = comptime getSignature("toHaveProperty", "path, value", false); - if (!received_property.isEmpty()) { - // deep equal case - const fmt = signature ++ "\n\n{any}\n"; - const diff_format = DiffFormatter{ - .received = received_property, - .expected = expected_property.?, - .globalObject = globalObject, - }; - - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); - return .zero; - } - - const fmt = signature ++ "\n\nExpected path: {any}\n\nExpected value: {any}\n\n" ++ - "Unable to find property\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ - expected_property_path.toFmt(globalObject, &formatter), - expected_property.?.toFmt(globalObject, &formatter), - }); - return .zero; - } - - const signature = comptime getSignature("toHaveProperty", "path", false); - const fmt = signature ++ "\n\nExpected path: {any}\n\nUnable to find property\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_property_path.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_property_path.toFmt(globalObject, &formatter)}); - return .zero; - } - - pub fn toBeEven(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeEven() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - if (value.isAnyInt()) { - const _value = value.toInt64(); - pass = @mod(_value, 2) == 0; - if (_value == -0) { // negative zero is even - pass = true; - } - } else if (value.isBigInt() or value.isBigInt32()) { - const _value = value.toInt64(); - pass = switch (_value == -0) { // negative zero is even - true => true, - else => _value & 1 == 0, - }; - } else if (value.isNumber()) { - const _value = JSValue.asNumber(value); - if (@mod(_value, 1) == 0 and @mod(_value, 2) == 0) { // if the fraction is all zeros and even - pass = true; - } else { - pass = false; - } - } else { - pass = false; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeEven", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeEven", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toBeGreaterThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeGreaterThan() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeGreaterThan() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() > other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .greater_than => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .less_than => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\> {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\> {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThan", "expected", false) ++ "\n\n" ++ - expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeGreaterThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeGreaterThanOrEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeGreaterThanOrEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() >= other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .greater_than, .equal => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .less_than, .equal => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\>= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\>= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeGreaterThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeLessThan(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeLessThan() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeLessThan() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() < other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .less_than => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .greater_than => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\< {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThan", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\< {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThan", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeLessThanOrEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeLessThanOrEqual() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeLessThanOrEqual() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const other_value = arguments[0]; - other_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { - globalObject.throw("Expected and actual values must be numbers or bigints", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - - if (!value.isBigInt() and !other_value.isBigInt()) { - pass = value.asNumber() <= other_value.asNumber(); - } else if (value.isBigInt()) { - pass = switch (value.asBigIntCompare(globalObject, other_value)) { - .less_than, .equal => true, - else => pass, - }; - } else { - pass = switch (other_value.asBigIntCompare(globalObject, value)) { - .greater_than, .equal => true, - else => pass, - }; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - const expected_fmt = other_value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected: not \\<= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected: \\<= {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeLessThanOrEqual", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - return .zero; - } - - pub fn toBeCloseTo(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const thisArguments = callFrame.arguments(2); - const arguments = thisArguments.ptr[0..thisArguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); - return .zero; - } - - const expected_ = arguments[0]; - if (!expected_.isNumber()) { - globalObject.throwInvalidArgumentType("toBeCloseTo", "expected", "number"); - return .zero; - } - - var precision: f64 = 2.0; - if (arguments.len > 1) { - const precision_ = arguments[1]; - if (!precision_.isNumber()) { - globalObject.throwInvalidArgumentType("toBeCloseTo", "precision", "number"); - return .zero; - } - - precision = precision_.asNumber(); - } - - const received_: JSC.JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (!received_.isNumber()) { - globalObject.throwInvalidArgumentType("expect", "received", "number"); - return .zero; - } - - var expected = expected_.asNumber(); - var received = received_.asNumber(); - - if (std.math.isNegativeInf(expected)) { - expected = -expected; - } - - if (std.math.isNegativeInf(received)) { - received = -received; - } - - if (std.math.isPositiveInf(expected) and std.math.isPositiveInf(received)) { - return thisValue; - } - - const expected_diff = std.math.pow(f64, 10, -precision) / 2; - const actual_diff = std.math.fabs(received - expected); - var pass = actual_diff < expected_diff; - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_fmt = expected_.toFmt(globalObject, &formatter); - const received_fmt = received_.toFmt(globalObject, &formatter); - - const expected_line = "Expected: {any}\n"; - const received_line = "Received: {any}\n"; - const expected_precision = "Expected precision: {d}\n"; - const expected_difference = "Expected difference: \\< {d}\n"; - const received_difference = "Received difference: {d}\n"; - - const suffix_fmt = "\n\n" ++ expected_line ++ received_line ++ "\n" ++ expected_precision ++ expected_difference ++ received_difference; - - if (not) { - const fmt = comptime getSignature("toBeCloseTo", "expected, precision", true) ++ suffix_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - const fmt = comptime getSignature("toBeCloseTo", "expected, precision", false) ++ suffix_fmt; - - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; - } - - pub fn toBeOdd(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeOdd() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - - if (value.isBigInt32()) { - pass = value.toInt32() & 1 == 1; - } else if (value.isBigInt()) { - pass = value.toInt64() & 1 == 1; - } else if (value.isInt32()) { - const _value = value.toInt32(); - pass = @mod(_value, 2) == 1; - } else if (value.isAnyInt()) { - const _value = value.toInt64(); - pass = @mod(_value, 2) == 1; - } else if (value.isNumber()) { - const _value = JSValue.asNumber(value); - if (@mod(_value, 1) == 0 and @mod(_value, 2) == 1) { // if the fraction is all zeros and odd - pass = true; - } else { - pass = false; - } - } else { - pass = false; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeOdd", "", true) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeOdd", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt}); - return .zero; - } - - pub fn toThrow(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toThrow() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const expected_value: JSValue = if (arguments.len > 0) brk: { - const value = arguments[0]; - if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) { - var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - globalObject.throw("Expected value must be string or Error: {any}", .{value.toFmt(globalObject, &fmt)}); - return .zero; - } - break :brk value; - } else .zero; - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!value.jsType().isFunction()) { - globalObject.throw("Expected value must be a function", .{}); - return .zero; - } - - const not = this.op.contains(.not); - - const result_: ?JSValue = brk: { - var vm = globalObject.bunVM(); - var return_value: JSValue = .zero; - var scope = vm.unhandledRejectionScope(); - var prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; - vm.unhandled_pending_rejection_to_capture = &return_value; - vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue; - const return_value_from_fucntion: JSValue = value.call(globalObject, &.{}); - vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture; - - if (return_value == .zero) { - return_value = return_value_from_fucntion; - } - - if (return_value.asAnyPromise()) |promise| { - globalObject.bunVM().waitForPromise(promise); - scope.apply(vm); - const promise_result = promise.result(globalObject.vm()); - - switch (promise.status(globalObject.vm())) { - .Fulfilled => { - break :brk null; - }, - .Rejected => { - // since we know for sure it rejected, we should always return the error - break :brk promise_result.toError() orelse promise_result; - }, - .Pending => unreachable, - } - } - scope.apply(vm); - - break :brk return_value.toError(); - }; - - const did_throw = result_ != null; - - if (not) { - const signature = comptime getSignature("toThrow", "expected", true); - - if (!did_throw) return thisValue; - - const result: JSValue = result_.?; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (expected_value.isEmpty()) { - const signature_no_args = comptime getSignature("toThrow", "", true); - if (result.toError()) |err| { - const name = err.get(globalObject, "name") orelse JSValue.undefined; - const message = err.get(globalObject, "message") orelse JSValue.undefined; - const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; - globalObject.throwPretty(fmt, .{ - name.toFmt(globalObject, &formatter), - message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - // non error thrown - const fmt = signature_no_args ++ "\n\nThrown value: {any}\n"; - globalObject.throwPretty(fmt, .{result.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.isString()) { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - - // TODO: remove this allocation - // partial match - { - const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; - defer expected_slice.deinit(); - const received_slice = received_message.toSliceOrNull(globalObject) orelse return .zero; - defer received_slice.deinit(); - if (!strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; - } - - const fmt = signature ++ "\n\nExpected substring: not {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ - expected_value.toFmt(globalObject, &formatter), - received_message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_value.isRegExp()) { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - - // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. - if (expected_value.get(globalObject, "test")) |test_fn| { - const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); - if (!matches.toBooleanSlow(globalObject)) return thisValue; - } - - const fmt = signature ++ "\n\nExpected pattern: not {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ - expected_value.toFmt(globalObject, &formatter), - received_message.toFmt(globalObject, &formatter), - }); - return .zero; - } - - if (expected_value.get(globalObject, "message")) |expected_message| { - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - // no partial match for this case - if (!expected_message.isSameValue(received_message, globalObject)) return thisValue; - - const fmt = signature ++ "\n\nExpected message: not {any}\n"; - globalObject.throwPretty(fmt, .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (!result.isInstanceOf(globalObject, expected_value)) return thisValue; - - var expected_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7); - const fmt = signature ++ "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_class, received_message.toFmt(globalObject, &formatter) }); - return .zero; - } - - const signature = comptime getSignature("toThrow", "expected", false); - if (did_throw) { - if (expected_value.isEmpty()) return thisValue; - - const result: JSValue = if (result_.?.toError()) |r| - r - else - result_.?; - - const _received_message: ?JSValue = if (result.isObject()) - result.get(globalObject, "message") - else if (result.toStringOrNull(globalObject)) |js_str| - JSC.JSValue.fromCell(js_str) - else - null; - - if (expected_value.isString()) { - if (_received_message) |received_message| { - // TODO: remove this allocation - // partial match - const expected_slice = expected_value.toSliceOrNull(globalObject) orelse return .zero; - defer expected_slice.deinit(); - const received_slice = received_message.toSlice(globalObject, globalObject.allocator()); - defer received_slice.deinit(); - if (strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue; - } - - // error: message from received error does not match expected string - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); - return .zero; - } - - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected substring: {any}\nReceived value: {any}"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - - return .zero; - } - - if (expected_value.isRegExp()) { - if (_received_message) |received_message| { - // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. - if (expected_value.get(globalObject, "test")) |test_fn| { - const matches = test_fn.callWithThis(globalObject, expected_value, &.{received_message}); - if (matches.toBooleanSlow(globalObject)) return thisValue; - } - } - - // error: message from received error does not match expected pattern - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_value_fmt = expected_value.toFmt(globalObject, &formatter); - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_value_fmt, received_message_fmt }); - - return .zero; - } - - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - // If it's not an object, we are going to crash here. - std.debug.assert(expected_value.isObject()); - - if (expected_value.get(globalObject, "message")) |expected_message| { - if (_received_message) |received_message| { - if (received_message.isSameValue(expected_message, globalObject)) return thisValue; - } - - // error: message from received error does not match expected error message. - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - if (_received_message) |received_message| { - const expected_fmt = expected_message.toFmt(globalObject, &formatter); - const received_fmt = received_message.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\nExpected message: {any}\nReceived message: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - const expected_fmt = expected_message.toFmt(globalObject, &formatter); - const received_fmt = result.toFmt(globalObject, &formatter); - const fmt = signature ++ "\n\nExpected message: {any}\nReceived value: {any}\n"; - globalObject.throwPretty(fmt, .{ expected_fmt, received_fmt }); - return .zero; - } - - if (result.isInstanceOf(globalObject, expected_value)) return thisValue; - - // error: received error not instance of received error constructor - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - var expected_class = ZigString.Empty; - var received_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - result.getClassName(globalObject, &received_class); - const fmt = signature ++ "\n\nExpected constructor: {s}\nReceived constructor: {s}\n\n"; - - if (_received_message) |received_message| { - const message_fmt = fmt ++ "Received message: {any}\n"; - const received_message_fmt = received_message.toFmt(globalObject, &formatter); - - globalObject.throwPretty(message_fmt, .{ - expected_class, - received_class, - received_message_fmt, - }); - return .zero; - } - - const received_fmt = result.toFmt(globalObject, &formatter); - const value_fmt = fmt ++ "Received value: {any}\n"; - - globalObject.throwPretty(value_fmt, .{ - expected_class, - received_class, - received_fmt, - }); - return .zero; - } - - // did not throw - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - const received_line = "Received function did not throw\n"; - - if (expected_value.isEmpty()) { - const fmt = comptime getSignature("toThrow", "", false) ++ "\n\n" ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{}); - return .zero; - } - - if (expected_value.isString()) { - const expected_fmt = "\n\nExpected substring: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.isRegExp()) { - const expected_fmt = "\n\nExpected pattern: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (expected_value.get(globalObject, "message")) |expected_message| { - const expected_fmt = "\n\nExpected message: {any}\n\n" ++ received_line; - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)}); - return .zero; - } - - const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; - var expected_class = ZigString.Empty; - expected_value.getClassName(globalObject, &expected_class); - const fmt = signature ++ expected_fmt; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class}); - return .zero; - } - - pub fn toMatchSnapshot(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatchSnapshot() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - if (not) { - const signature = comptime getSignature("toMatchSnapshot", "", true); - const fmt = signature ++ "\n\nMatcher error: Snapshot matchers cannot be used with not\n"; - globalObject.throwPretty(fmt, .{}); - } - - var hint_string: ZigString = ZigString.Empty; - var property_matchers: ?JSValue = null; - switch (arguments.len) { - 0 => {}, - 1 => { - if (arguments[0].isString()) { - arguments[0].toZigString(&hint_string, globalObject); - } else if (arguments[0].isObject()) { - property_matchers = arguments[0]; - } - }, - else => { - if (!arguments[0].isObject()) { - const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - const fmt = signature ++ "\n\nMatcher error: Expected properties must be an object\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - property_matchers = arguments[0]; - - if (arguments[1].isString()) { - arguments[1].toZigString(&hint_string, globalObject); - } - }, - } - - var hint = hint_string.toSlice(default_allocator); - defer hint.deinit(); - - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (!value.isObject() and property_matchers != null) { - const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - const fmt = signature ++ "\n\nMatcher error: received values must be an object when the matcher has properties\n"; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - - if (property_matchers) |_prop_matchers| { - var prop_matchers = _prop_matchers; - - if (!value.jestDeepMatch(prop_matchers, globalObject, true)) { - // TODO: print diff with properties from propertyMatchers - const signature = comptime getSignature("toMatchSnapshot", "propertyMatchers", false); - const fmt = signature ++ "\n\nExpected propertyMatchers to match properties from received object" ++ - "\n\nReceived: {any}\n"; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - } - - const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalObject) catch |err| { - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - const test_file_path = Jest.runner.?.files.get(this.scope.file_id).source.path.text; - switch (err) { - error.FailedToOpenSnapshotFile => globalObject.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), - error.FailedToMakeSnapshotDirectory => globalObject.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}), - error.FailedToWriteSnapshotFile => globalObject.throw("Failed write to snapshot file: {s}", .{test_file_path}), - error.ParseError => globalObject.throw("Failed to parse snapshot file for: {s}", .{test_file_path}), - else => globalObject.throw("Failed to snapshot value: {any}", .{value.toFmt(globalObject, &formatter)}), - } - return .zero; - }; - - if (result) |saved_value| { - var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable; - value.jestSnapshotPrettyFormat(&pretty_value, globalObject) catch { - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; - globalObject.throw("Failed to pretty format value: {s}", .{value.toFmt(globalObject, &formatter)}); - return .zero; - }; - defer pretty_value.deinit(); - - if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) { - Jest.runner.?.snapshots.passed += 1; - return thisValue; - } - - Jest.runner.?.snapshots.failed += 1; - const signature = comptime getSignature("toMatchSnapshot", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - const diff_format = DiffFormatter{ - .received_string = pretty_value.toOwnedSliceLeaky(), - .expected_string = saved_value, - .globalObject = globalObject, - }; - - globalObject.throwPretty(fmt, .{diff_format}); - return .zero; - } - - return thisValue; - } - - pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const value: JSValue = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeEmpty() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = false; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const actual_length = value.getLengthIfPropertyExistsInternal(globalObject); - - if (actual_length == std.math.inf(f64)) { - if (value.jsTypeLoose().isObject()) { - if (value.isIterable(globalObject)) { - var any_properties_in_iterator = false; - value.forEach(globalObject, &any_properties_in_iterator, struct { - pub fn anythingInIterator( - _: *JSC.VM, - _: *JSGlobalObject, - any_: ?*anyopaque, - _: JSValue, - ) callconv(.C) void { - bun.cast(*bool, any_.?).* = true; - } - }.anythingInIterator); - pass = !any_properties_in_iterator; - } else { - var props_iter = JSC.JSPropertyIterator(.{ - .skip_empty_name = false, - - .include_value = true, - }).init(globalObject, value.asObjectRef()); - defer props_iter.deinit(); - pass = props_iter.len == 0; - } - } else { - const signature = comptime getSignature("toBeEmpty", "", false); - const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - } else if (std.math.isNan(actual_length)) { - globalObject.throw("Received value has non-number length property: {}", .{actual_length}); - return .zero; - } else { - pass = actual_length == 0; - } - - if (not and pass) { - const signature = comptime getSignature("toBeEmpty", "", true); - const fmt = signature ++ "\n\nExpected value not to be a string, object, or iterable" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - if (not) pass = !pass; - if (pass) return thisValue; - - if (not) { - const signature = comptime getSignature("toBeEmpty", "", true); - const fmt = signature ++ "\n\nExpected value not to be empty" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - const signature = comptime getSignature("toBeEmpty", "", false); - const fmt = signature ++ "\n\nExpected value to be empty" ++ - "\n\nReceived: {any}\n"; - globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNil() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isUndefinedOrNull() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNil", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNil", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeArray() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.jsType().isArray() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeArray", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeArray", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeArrayOfSize() must be called in a test", .{}); - return .zero; - } - - const size = arguments[0]; - size.ensureStillAlive(); - - if (!size.isAnyInt()) { - globalThis.throw("toBeArrayOfSize() requires the first argument to be a number", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - var pass = value.jsType().isArray() and @intCast(i32, value.getLength(globalThis)) == size.toInt32(); - - if (not) pass = !pass; - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeArrayOfSize", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeArrayOfSize", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeBoolean() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isBoolean() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeBoolean", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeBoolean", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeTrue() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = (value.isBoolean() and value.toBoolean()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeTrue", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeTrue", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFalse() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = (value.isBoolean() and !value.toBoolean()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFalse", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFalse", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNumber() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isNumber() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNumber", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNumber", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeInteger() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isAnyInt() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeInteger", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeInteger", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFinite() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = std.math.isFinite(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFinite", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFinite", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBePositive() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = @round(num) > 0 and !std.math.isInf(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBePositive", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBePositive", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeNegative() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num: f64 = value.asNumber(); - pass = @round(num) < 0 and !std.math.isInf(num) and !std.math.isNan(num); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeNegative", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeNegative", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeTypeOf() must be called in a test", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const expected = arguments[0]; - expected.ensureStillAlive(); - - const expectedAsStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - active_test_expectation_counter.actual += 1; - - if (!expected.isString()) { - globalThis.throwInvalidArguments("toBeTypeOf() requires a string argument", .{}); - return .zero; - } - - if (!std.mem.eql(u8, expectedAsStr, "function") and - !std.mem.eql(u8, expectedAsStr, "object") and - !std.mem.eql(u8, expectedAsStr, "bigint") and - !std.mem.eql(u8, expectedAsStr, "boolean") and - !std.mem.eql(u8, expectedAsStr, "number") and - !std.mem.eql(u8, expectedAsStr, "string") and - !std.mem.eql(u8, expectedAsStr, "symbol") and - !std.mem.eql(u8, expectedAsStr, "undefined")) - { - globalThis.throwInvalidArguments("toBeTypeOf() requires a valid type string argument ('function', 'object', 'bigint', 'boolean', 'number', 'string', 'symbol', 'undefined')", .{}); - return .zero; - } - - const not = this.op.contains(.not); - var pass = false; - var whatIsTheType: []const u8 = ""; - - // Checking for function/class should be done before everything else, or it will fail. - if (value.isCallable(globalThis.vm())) { - whatIsTheType = "function"; - } else if (value.isObject() or value.jsType().isArray() or value.isNull()) { - whatIsTheType = "object"; - } else if (value.isBigInt()) { - whatIsTheType = "bigint"; - } else if (value.isBoolean()) { - whatIsTheType = "boolean"; - } else if (value.isNumber()) { - whatIsTheType = "number"; - } else if (value.jsType().isString()) { - whatIsTheType = "string"; - } else if (value.isSymbol()) { - whatIsTheType = "symbol"; - } else if (value.isUndefined()) { - whatIsTheType = "undefined"; - } else { - globalThis.throw("Internal consistency error: unknown JSValue type", .{}); - return .zero; - } - - pass = std.mem.eql(u8, expectedAsStr, whatIsTheType); - - if (not) pass = !pass; - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - const expected_str = expected.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeTypeOf", "", true) ++ "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; - globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); - return .zero; - } - - const fmt = comptime getSignature("toBeTypeOf", "", false) ++ "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n"; - globalThis.throwPretty(fmt, .{ expected_str, whatIsTheType, received }); - return .zero; - } - - pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(2); - const arguments = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeWithin() must be called in a test", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const startValue = arguments[0]; - startValue.ensureStillAlive(); - - if (!startValue.isNumber()) { - globalThis.throw("toBeWithin() requires the first argument to be a number", .{}); - return .zero; - } - - const endValue = arguments[1]; - endValue.ensureStillAlive(); - - if (!endValue.isNumber()) { - globalThis.throw("toBeWithin() requires the second argument to be a number", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isNumber(); - if (pass) { - const num = value.asNumber(); - pass = num >= startValue.asNumber() and num < endValue.asNumber(); - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const start_fmt = startValue.toFmt(globalThis, &formatter); - const end_fmt = endValue.toFmt(globalThis, &formatter); - const received_fmt = value.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected: not between {any} (inclusive) and {any} (exclusive)\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeWithin", "start, end", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); - return .zero; - } - - const expected_line = "Expected: between {any} (inclusive) and {any} (exclusive)\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toBeWithin", "start, end", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ start_fmt, end_fmt, received_fmt }); - return .zero; - } - - pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeSymbol() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isSymbol() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeSymbol", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeSymbol", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeFunction() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isCallable(globalThis.vm()) != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeFunction", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeFunction", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeDate() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isDate() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeDate", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeDate", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toBeString() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - const pass = value.isString() != not; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const received = value.toFmt(globalThis, &formatter); - - if (not) { - const fmt = comptime getSignature("toBeString", "", true) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - const fmt = comptime getSignature("toBeString", "", false) ++ "\n\n" ++ "Received: {any}\n"; - globalThis.throwPretty(fmt, .{received}); - return .zero; - } - - pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toInclude() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toInclude() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toInclude() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.contains(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not include: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toInclude", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to include: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toInclude", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toStartWith() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toStartWith() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toStartWith() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.startsWith(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not start with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toStartWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to start with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toStartWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalThis); - - const thisValue = callFrame.this(); - const arguments_ = callFrame.arguments(1); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - globalThis.throwInvalidArguments("toEndWith() requires 1 argument", .{}); - return .zero; - } - - const expected = arguments[0]; - expected.ensureStillAlive(); - - if (!expected.isString()) { - globalThis.throw("toEndWith() requires the first argument to be a string", .{}); - return .zero; - } - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (this.scope.tests.items.len <= this.test_id) { - globalThis.throw("toEndWith() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var pass = value.isString(); - if (pass) { - const value_string = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - const expected_string = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); - pass = strings.endsWith(value_string, expected_string) or expected_string.len == 0; - } - - const not = this.op.contains(.not); - if (not) pass = !pass; - - if (pass) return thisValue; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; - const value_fmt = value.toFmt(globalThis, &formatter); - const expected_fmt = expected.toFmt(globalThis, &formatter); - - if (not) { - const expected_line = "Expected to not end with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toEndWith", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected to end with: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toEndWith", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{}); - return .zero; - } - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toBeInstanceOf() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_value = arguments[0]; - if (!expected_value.isConstructor()) { - globalObject.throw("Expected value must be a function: {any}", .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - const not = this.op.contains(.not); - var pass = value.isInstanceOf(globalObject, expected_value); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const value_fmt = value.toFmt(globalObject, &formatter); - if (not) { - const expected_line = "Expected constructor: not {any}\n"; - const received_line = "Received value: {any}\n"; - const fmt = comptime getSignature("toBeInstanceOf", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt }); - return .zero; - } - - globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected constructor: {any}\n"; - const received_line = "Received value: {any}\n"; - const fmt = comptime getSignature("toBeInstanceOf", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toMatch(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - defer this.postMatch(globalObject); - - const thisValue = callFrame.this(); - const _arguments = callFrame.arguments(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatch() must be called in a test", .{}); - return .zero; - } - - if (arguments.len < 1) { - globalObject.throwInvalidArguments("toMatch() requires 1 argument", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - - const expected_value = arguments[0]; - if (!expected_value.isString() and !expected_value.isRegExp()) { - globalObject.throw("Expected value must be a string or regular expression: {any}", .{expected_value.toFmt(globalObject, &formatter)}); - return .zero; - } - expected_value.ensureStillAlive(); - - const value = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - value.ensureStillAlive(); - - if (!value.isString()) { - globalObject.throw("Received value must be a string: {any}", .{value.toFmt(globalObject, &formatter)}); - return .zero; - } - - const not = this.op.contains(.not); - var pass: bool = brk: { - if (expected_value.isString()) { - break :brk value.stringIncludes(globalObject, expected_value); - } else if (expected_value.isRegExp()) { - break :brk expected_value.toMatch(globalObject, value); - } - unreachable; - }; - - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - const expected_fmt = expected_value.toFmt(globalObject, &formatter); - const value_fmt = value.toFmt(globalObject, &formatter); - - if (not) { - const expected_line = "Expected substring or pattern: not {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toMatch", "expected", true) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - const expected_line = "Expected substring or pattern: {any}\n"; - const received_line = "Received: {any}\n"; - const fmt = comptime getSignature("toMatch", "expected", false) ++ "\n\n" ++ expected_line ++ received_line; - globalObject.throwPretty(fmt, .{ expected_fmt, value_fmt }); - return .zero; - } - - pub fn toHaveBeenCalled(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - const thisValue = callframe.this(); - defer this.postMatch(globalObject); - - const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - const calls = JSMockFunction__getCalls(value); - active_test_expectation_counter.actual += 1; - - if (calls == .zero or !calls.jsType().isArray()) { - globalObject.throw("Expected value must be a mock function: {}", .{value}); - return .zero; - } - - var pass = calls.getLength(globalObject) > 0; - - const not = this.op.contains(.not); - if (not) pass = !pass; - if (pass) return thisValue; - - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - const signature = comptime getSignature("toHaveBeenCalled", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } else { - const signature = comptime getSignature("toHaveBeenCalled", "expected", true); - const fmt = signature ++ "\n\nExpected {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - - unreachable; - } - pub fn toHaveBeenCalledTimes(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - const thisValue = callframe.this(); - const arguments_ = callframe.arguments(1); - const arguments: []const JSValue = arguments_.ptr[0..arguments_.len]; - defer this.postMatch(globalObject); - const value: JSValue = JSC.Jest.Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; - - active_test_expectation_counter.actual += 1; - - const calls = JSMockFunction__getCalls(value); - - if (calls == .zero or !calls.jsType().isArray()) { - globalObject.throw("Expected value must be a mock function: {}", .{value}); - return .zero; - } + todo, + fail_because_todo_passed, + }; + }; +}; - if (arguments.len < 1 or !arguments[0].isAnyInt()) { - globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 integer argument", .{}); - return .zero; - } +pub const Jest = struct { + pub var runner: ?*TestRunner = null; - const times = arguments[0].coerce(i32, globalObject); + fn globalHook(comptime name: string) JSC.JSHostFunctionType { + return struct { + pub fn appendGlobalFunctionCallback( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + const arguments = callframe.arguments(2); + if (arguments.len < 1) { + globalThis.throwNotEnoughArguments("callback", 1, arguments.len); + return .zero; + } - var pass = @intCast(i32, calls.getLength(globalObject)) == times; + const function = arguments.ptr[0]; + if (function.isEmptyOrUndefinedOrNull() or !function.isCallable(globalThis.vm())) { + globalThis.throwInvalidArgumentType(name, "callback", "function"); + return .zero; + } - const not = this.op.contains(.not); - if (not) pass = !pass; - if (pass) return thisValue; + if (function.getLength(globalThis) > 0) { + globalThis.throw("done() callback is not implemented in global hooks yet. Please make your function take no arguments", .{}); + return .zero; + } - // handle failure - var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; - if (not) { - const signature = comptime getSignature("toHaveBeenCalled", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } else { - const signature = comptime getSignature("toHaveBeenCalled", "expected", true); - const fmt = signature ++ "\n\nExpected {any}\n"; - if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)}); - return .zero; + function.protect(); + @field(Jest.runner.?.global_callbacks, name).append( + bun.default_allocator, + function, + ) catch unreachable; + return JSC.JSValue.jsUndefined(); } - globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)}); - return .zero; - } - - unreachable; + }.appendGlobalFunctionCallback; } - pub fn toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - defer this.postMatch(globalObject); - const thisValue = callFrame.this(); - const args = callFrame.arguments(1).slice(); - - if (this.scope.tests.items.len <= this.test_id) { - globalObject.throw("toMatchObject() must be called in a test", .{}); - return .zero; - } - - active_test_expectation_counter.actual += 1; - - const not = this.op.contains(.not); - - const received_object = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; + var global_hooks_object = JSC.JSValue.createEmptyObject(globalObject, 8); + global_hooks_object.ensureStillAlive(); - if (!received_object.isObject()) { - const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; - if (not) { - const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); + const notSupportedHereFn = struct { + pub fn notSupportedHere( + globalThis: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) JSValue { + globalThis.throw("This function can only be used in a test.", .{}); return .zero; } + }.notSupportedHere; + const notSupportedHere = JSC.NewFunction(globalObject, null, 0, notSupportedHereFn, false); + notSupportedHere.ensureStillAlive(); - const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; + inline for (.{ + "expect", + "describe", + "it", + "test", + }) |name| { + global_hooks_object.put(globalObject, ZigString.static(name), notSupportedHere); } - if (args.len < 1 or !args[0].isObject()) { - const matcher_error = "\n\nMatcher error: expected value must be a non-null object\n"; - if (not) { - const fmt = comptime getSignature("toMatchObject", "expected", true) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; - } - const fmt = comptime getSignature("toMatchObject", "expected", false) ++ matcher_error; - globalObject.throwPretty(fmt, .{}); - return .zero; + inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { + const function = JSC.NewFunction(globalObject, null, 1, globalHook(name), false); + function.ensureStillAlive(); + global_hooks_object.put(globalObject, ZigString.static(name), function); } + return global_hooks_object; + } - const property_matchers = args[0]; - - var pass = received_object.jestDeepMatch(property_matchers, globalObject, true); + pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + JSC.markBinding(@src()); - if (not) pass = !pass; - if (pass) return thisValue; + const module = JSC.JSValue.createEmptyObject(globalObject, 11); - // handle failure - const diff_formatter = DiffFormatter{ - .received = received_object, - .expected = property_matchers, - .globalObject = globalObject, - .not = not, - }; + const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); + module.put( + globalObject, + ZigString.static("test"), + test_fn, + ); + test_fn.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), + ); + test_fn.put( + globalObject, + ZigString.static("skip"), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, TestScope.skip, false), + ); + test_fn.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + ); + test_fn.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), + ); + test_fn.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), + ); - if (not) { - const signature = comptime getSignature("toMatchObject", "expected", true); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } + module.put( + globalObject, + ZigString.static("it"), + test_fn, + ); + const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); + describe.put( + globalObject, + ZigString.static("only"), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), + ); + describe.put( + globalObject, + ZigString.static("skip"), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), + ); + describe.put( + globalObject, + ZigString.static("todo"), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), + ); + describe.put( + globalObject, + ZigString.static("if"), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), + ); + describe.put( + globalObject, + ZigString.static("skipIf"), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), + ); - const signature = comptime getSignature("toMatchObject", "expected", false); - const fmt = signature ++ "\n\n{any}\n"; - globalObject.throwPretty(fmt, .{diff_formatter}); - return .zero; - } + module.put( + globalObject, + ZigString.static("describe"), + describe, + ); - pub const toHaveBeenCalledWith = notImplementedJSCFn; - pub const toHaveBeenLastCalledWith = notImplementedJSCFn; - pub const toHaveBeenNthCalledWith = notImplementedJSCFn; - pub const toHaveReturnedTimes = notImplementedJSCFn; - pub const toHaveReturnedWith = notImplementedJSCFn; - pub const toHaveLastReturnedWith = notImplementedJSCFn; - pub const toHaveNthReturnedWith = notImplementedJSCFn; - pub const toContainEqual = notImplementedJSCFn; - pub const toMatchInlineSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; - pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; - - pub const getStaticNot = notImplementedStaticProp; - pub const getStaticResolves = notImplementedStaticProp; - pub const getStaticRejects = notImplementedStaticProp; - - pub fn getNot(this: *Expect, thisValue: JSValue, globalObject: *JSGlobalObject) callconv(.C) JSValue { - _ = Expect.capturedValueGetCached(thisValue) orelse { - globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); - return .zero; - }; + module.put( + globalObject, + ZigString.static("beforeAll"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeAll"), 1, DescribeScope.beforeAll, false), + ); + module.put( + globalObject, + ZigString.static("beforeEach"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeEach"), 1, DescribeScope.beforeEach, false), + ); + module.put( + globalObject, + ZigString.static("afterAll"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("afterAll"), 1, DescribeScope.afterAll, false), + ); + module.put( + globalObject, + ZigString.static("afterEach"), + JSC.NewRuntimeFunction(globalObject, ZigString.static("afterEach"), 1, DescribeScope.afterEach, false), + ); + module.put( + globalObject, + ZigString.static("expect"), + Expect.getConstructor(globalObject), + ); - this.op.toggle(.not); + const mock_fn = JSMockFunction__createObject(globalObject); + const spyOn = JSC.NewFunction(globalObject, ZigString.static("spyOn"), 2, JSMock__spyOn, false); + const restoreAllMocks = JSC.NewFunction(globalObject, ZigString.static("restoreAllMocks"), 2, jsFunctionResetSpies, false); + module.put(globalObject, ZigString.static("mock"), mock_fn); - return thisValue; - } + const jest = JSValue.createEmptyObject(globalObject, 3); + jest.put(globalObject, ZigString.static("fn"), mock_fn); + jest.put(globalObject, ZigString.static("spyOn"), spyOn); + jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks); + module.put(globalObject, ZigString.static("jest"), jest); + module.put(globalObject, ZigString.static("spyOn"), spyOn); - pub const getResolves = notImplementedJSCProp; - pub const getRejects = notImplementedJSCProp; + const vi = JSValue.createEmptyObject(globalObject, 1); + vi.put(globalObject, ZigString.static("fn"), mock_fn); + module.put(globalObject, ZigString.static("vi"), vi); - pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectAny.call(globalObject, callFrame); + return module; } - pub fn anything(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectAnything.call(globalObject, callFrame); - } + extern fn JSMockFunction__createObject(*JSC.JSGlobalObject) JSC.JSValue; - pub fn stringContaining(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectStringContaining.call(globalObject, callFrame); - } + extern fn Bun__Jest__testPreloadObject(*JSC.JSGlobalObject) JSC.JSValue; + extern fn Bun__Jest__testModuleObject(*JSC.JSGlobalObject) JSC.JSValue; + extern fn jsFunctionResetSpies(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; + extern fn JSMock__spyOn(*JSC.JSGlobalObject, *JSC.CallFrame) JSC.JSValue; - pub fn stringMatching(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - return ExpectStringMatching.call(globalObject, callFrame); - } + pub fn call( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments_: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + JSC.markBinding(@src()); + var runner_ = runner orelse { + JSError(getAllocator(ctx), "Run \"bun test\" to run a test", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + }; + const arguments = @ptrCast([]const JSC.JSValue, arguments_); - pub const extend = notImplementedStaticFn; - pub const arrayContaining = notImplementedStaticFn; - pub const assertions = notImplementedStaticFn; - pub const hasAssertions = notImplementedStaticFn; - pub const objectContaining = notImplementedStaticFn; - pub const addSnapshotSerializer = notImplementedStaticFn; + if (arguments.len < 1 or !arguments[0].isString()) { + JSError(getAllocator(ctx), "Bun.jest() expects a string filename", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + var str = arguments[0].toSlice(ctx, bun.default_allocator); + defer str.deinit(); + var slice = str.slice(); - pub fn notImplementedJSCFn(_: *Expect, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + if (str.len == 0 or slice[0] != '/') { + JSError(getAllocator(ctx), "Bun.jest() expects an absolute file path", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + var vm = ctx.bunVM(); + if (vm.is_in_preload) { + return Bun__Jest__testPreloadObject(ctx).asObjectRef(); + } - pub fn notImplementedStaticFn(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - pub fn notImplementedJSCProp(_: *Expect, _: JSC.JSValue, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; - } + var scope = runner_.getOrPutFile(filepath); + DescribeScope.active = scope; + DescribeScope.module = scope; - pub fn notImplementedStaticProp(globalObject: *JSC.JSGlobalObject, _: JSC.JSValue, _: JSC.JSValue) callconv(.C) JSC.JSValue { - globalObject.throw("Not implemented", .{}); - return .zero; + return Bun__Jest__testModuleObject(ctx).asObjectRef(); } - pub fn postMatch(_: *Expect, globalObject: *JSC.JSGlobalObject) void { - var vm = globalObject.bunVM(); - vm.autoGarbageCollect(); + comptime { + if (!JSC.is_bindgen) { + @export(Bun__Jest__createTestModuleObject, .{ .name = "Bun__Jest__createTestModuleObject" }); + @export(Bun__Jest__createTestPreloadObject, .{ .name = "Bun__Jest__createTestPreloadObject" }); + } } }; @@ -4338,7 +554,7 @@ pub const TestScope = struct { const err = arguments.ptr[0]; globalThis.bunVM().runErrorHandler(err, null); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .promise); + task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .promise); globalThis.bunVM().autoGarbageCollect(); return JSValue.jsUndefined(); } @@ -4346,7 +562,7 @@ pub const TestScope = struct { pub fn onResolve(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { const arguments = callframe.arguments(2); var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask); - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .promise); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .promise); globalThis.bunVM().autoGarbageCollect(); return JSValue.jsUndefined(); } @@ -4365,13 +581,13 @@ pub const TestScope = struct { if (args.len > 0) { const err = args.ptr[0]; if (err.isEmptyOrUndefinedOrNull()) { - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .callback); } else { globalThis.bunVM().runErrorHandlerWithDedupe(err, null); - task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .callback); } } else { - task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback); + task.handleResult(.{ .pass = expect.active_test_expectation_counter.actual }, .callback); } } @@ -4432,7 +648,7 @@ pub const TestScope = struct { return .{ .todo = {} }; } - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; } if (initial_value.asAnyPromise()) |promise| { @@ -4459,7 +675,7 @@ pub const TestScope = struct { return .{ .todo = {} }; } - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; }, .Pending => { task.promise_state = .pending; @@ -4481,15 +697,15 @@ pub const TestScope = struct { return .{ .pending = {} }; } - if (active_test_expectation_counter.expected > 0 and active_test_expectation_counter.expected < active_test_expectation_counter.actual) { + if (expect.active_test_expectation_counter.expected > 0 and expect.active_test_expectation_counter.expected < expect.active_test_expectation_counter.actual) { Output.prettyErrorln("Test fail: {d} / {d} expectations\n (make this better!)", .{ - active_test_expectation_counter.actual, - active_test_expectation_counter.expected, + expect.active_test_expectation_counter.actual, + expect.active_test_expectation_counter.expected, }); - return .{ .fail = active_test_expectation_counter.actual }; + return .{ .fail = expect.active_test_expectation_counter.actual }; } - return .{ .pass = active_test_expectation_counter.actual }; + return .{ .pass = expect.active_test_expectation_counter.actual }; } pub const name = "TestScope"; @@ -4876,8 +1092,6 @@ pub const DescribeScope = struct { }; -var active_test_expectation_counter: TestScope.Counter = undefined; - pub const TestRunnerTask = struct { test_id: TestRunner.Test.ID, describe: *DescribeScope, @@ -4920,7 +1134,7 @@ pub const TestRunnerTask = struct { if (jsc_vm.onUnhandledRejectionCtx) |ctx| { var this = bun.cast(*TestRunnerTask, ctx); jsc_vm.onUnhandledRejectionCtx = null; - this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .unhandledRejection); + this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .unhandledRejection); } } @@ -4932,7 +1146,7 @@ pub const TestRunnerTask = struct { // reset the global state for each test // prior to the run DescribeScope.active = describe; - active_test_expectation_counter = .{}; + expect.active_test_expectation_counter = .{}; jsc_vm.last_reported_error_for_dedupe = .zero; const test_id = this.test_id; @@ -4999,7 +1213,7 @@ pub const TestRunnerTask = struct { this.ref.unref(this.globalThis.bunVM()); this.globalThis.throwTerminationException(); - this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .timeout); + this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .timeout); } pub fn handleResult(this: *TestRunnerTask, result: Result, comptime from: @Type(.EnumLiteral)) void { @@ -5404,11 +1618,3 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { Output.printError("\n", .{}); Output.flush(); } - -/// JSValue.zero is used to indicate it was not a JSMockFunction -/// If there were no calls, it returns an empty JSArray* -extern fn JSMockFunction__getCalls(JSValue) JSValue; - -/// JSValue.zero is used to indicate it was not a JSMockFunction -/// If there were no calls, it returns an empty JSArray* -extern fn JSMockFunction__getReturns(JSValue) JSValue; diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 29e330a04..4a245c3bb 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -15,6 +15,7 @@ const JSPrinter = bun.js_printer; const JSPrivateDataPtr = JSC.JSPrivateDataPtr; const JS = @import("../javascript.zig"); const JSPromise = JSC.JSPromise; +const expect = @import("./expect.zig"); pub const EventType = enum(u8) { Event, @@ -1264,12 +1265,12 @@ pub const JestPrettyFormat = struct { } else if (value.as(JSC.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (value.as(JSC.Jest.ExpectAnything) != null) { + } else if (value.as(expect.ExpectAnything) != null) { this.addForNewLine("Anything".len); writer.writeAll("Anything"); return; - } else if (value.as(JSC.Jest.ExpectAny) != null) { - const constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectAny) != null) { + const constructor_value = expect.ExpectAny.constructorValueGetCached(value) orelse return; this.addForNewLine("Any<".len); writer.writeAll("Any<"); @@ -1281,16 +1282,16 @@ pub const JestPrettyFormat = struct { writer.writeAll(">"); return; - } else if (value.as(JSC.Jest.ExpectStringContaining) != null) { - const substring_value = JSC.Jest.ExpectStringContaining.stringValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectStringContaining) != null) { + const substring_value = expect.ExpectStringContaining.stringValueGetCached(value) orelse return; this.addForNewLine("StringContaining ".len); writer.writeAll("StringContaining "); this.printAs(.String, Writer, writer_, substring_value, .String, enable_ansi_colors); return; - } else if (value.as(JSC.Jest.ExpectStringMatching) != null) { - const test_value = JSC.Jest.ExpectStringMatching.testValueGetCached(value) orelse return; + } else if (value.as(expect.ExpectStringMatching) != null) { + const test_value = expect.ExpectStringMatching.testValueGetCached(value) orelse return; this.addForNewLine("StringMatching ".len); writer.writeAll("StringMatching "); diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig new file mode 100644 index 000000000..12c7b3c36 --- /dev/null +++ b/src/bun.js/test/snapshot.zig @@ -0,0 +1,284 @@ +const std = @import("std"); +const bun = @import("root").bun; +const default_allocator = bun.default_allocator; +const string = bun.string; +const MutableString = bun.MutableString; +const strings = bun.strings; +const logger = bun.logger; +const jest = @import("./jest.zig"); +const Jest = jest.Jest; +const TestRunner = jest.TestRunner; +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const VirtualMachine = JSC.VirtualMachine; +const Expect = @import("./expect.zig").Expect; + +pub const Snapshots = struct { + const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; + pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); + + allocator: std.mem.Allocator, + update_snapshots: bool, + total: usize = 0, + added: usize = 0, + passed: usize = 0, + failed: usize = 0, + + file_buf: *std.ArrayList(u8), + values: *ValuesHashMap, + counts: *bun.StringHashMap(usize), + _current_file: ?File = null, + snapshot_dir_path: ?string = null, + + const File = struct { + id: TestRunner.File.ID, + file: std.fs.File, + }; + + pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { + switch (try this.getSnapshotFile(expect.scope.file_id)) { + .result => {}, + .err => |err| { + return switch (err.syscall) { + .mkdir => error.FailedToMakeSnapshotDirectory, + .open => error.FailedToOpenSnapshotFile, + else => error.SnapshotFailed, + }; + }, + } + + const snapshot_name = try expect.getSnapshotName(this.allocator, hint); + this.total += 1; + + var count_entry = try this.counts.getOrPut(snapshot_name); + const counter = brk: { + if (count_entry.found_existing) { + this.allocator.free(snapshot_name); + count_entry.value_ptr.* += 1; + break :brk count_entry.value_ptr.*; + } + count_entry.value_ptr.* = 1; + break :brk count_entry.value_ptr.*; + }; + + const name = count_entry.key_ptr.*; + + var counter_string_buf = [_]u8{0} ** 32; + var counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); + + var name_with_counter = try this.allocator.alloc(u8, name.len + 1 + counter_string.len); + defer this.allocator.free(name_with_counter); + bun.copy(u8, name_with_counter[0..name.len], name); + name_with_counter[name.len] = ' '; + bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); + + const name_hash = bun.hash(name_with_counter); + if (this.values.get(name_hash)) |expected| { + return expected; + } + + // doesn't exist. append to file bytes and add to hashmap. + var pretty_value = try MutableString.init(this.allocator, 0); + try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); + + const serialized_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; + try this.file_buf.ensureUnusedCapacity(serialized_length); + this.file_buf.appendSliceAssumeCapacity("\nexports[`"); + this.file_buf.appendSliceAssumeCapacity(name_with_counter); + this.file_buf.appendSliceAssumeCapacity("`] = `"); + this.file_buf.appendSliceAssumeCapacity(pretty_value.list.items); + this.file_buf.appendSliceAssumeCapacity("`;\n"); + + this.added += 1; + try this.values.put(name_hash, pretty_value.toOwnedSlice()); + return null; + } + + pub fn parseFile(this: *Snapshots) !void { + if (this.file_buf.items.len == 0) return; + + const vm = VirtualMachine.get(); + var opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); + var temp_log = logger.Log.init(this.allocator); + + const test_file = Jest.runner.?.files.get(this._current_file.?.id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + const source = logger.Source.initPathString(snapshot_file_path, this.file_buf.items); + + var parser = try js_parser.Parser.init( + opts, + &temp_log, + &source, + vm.bundler.options.define, + this.allocator, + ); + + var parse_result = try parser.parse(); + var ast = if (parse_result == .ast) parse_result.ast else return error.ParseError; + defer ast.deinit(); + + if (ast.exports_ref.isNull()) return; + const exports_ref = ast.exports_ref; + + // TODO: when common js transform changes, keep this updated or add flag to support this version + + const export_default = brk: { + for (ast.parts.slice()) |part| { + for (part.stmts) |stmt| { + if (stmt.data == .s_export_default and stmt.data.s_export_default.value == .expr) { + break :brk stmt.data.s_export_default.value.expr; + } + } + } + + return; + }; + + if (export_default.data == .e_call) { + const function_call = export_default.data.e_call; + if (function_call.args.len == 2 and function_call.args.ptr[0].data == .e_function) { + const arg_function_stmts = function_call.args.ptr[0].data.e_function.func.body.stmts; + for (arg_function_stmts) |stmt| { + switch (stmt.data) { + .s_expr => |expr| { + if (expr.value.data == .e_binary and expr.value.data.e_binary.op == .bin_assign) { + const left = expr.value.data.e_binary.left; + if (left.data == .e_index and left.data.e_index.index.data == .e_string and left.data.e_index.target.data == .e_identifier) { + const target: js_ast.E.Identifier = left.data.e_index.target.data.e_identifier; + var index: *js_ast.E.String = left.data.e_index.index.data.e_string; + if (target.ref.eql(exports_ref) and expr.value.data.e_binary.right.data == .e_string) { + const key = index.slice(this.allocator); + var value_string = expr.value.data.e_binary.right.data.e_string; + const value = value_string.slice(this.allocator); + defer { + if (!index.isUTF8()) this.allocator.free(key); + if (!value_string.isUTF8()) this.allocator.free(value); + } + const value_clone = try this.allocator.alloc(u8, value.len); + bun.copy(u8, value_clone, value); + const name_hash = bun.hash(key); + try this.values.put(name_hash, value_clone); + } + } + } + }, + else => {}, + } + } + } + } + } + + pub fn writeSnapshotFile(this: *Snapshots) !void { + if (this._current_file) |_file| { + var file = _file; + file.file.writeAll(this.file_buf.items) catch { + return error.FailedToWriteSnapshotFile; + }; + file.file.close(); + this.file_buf.clearAndFree(); + + var value_itr = this.values.valueIterator(); + while (value_itr.next()) |value| { + this.allocator.free(value.*); + } + this.values.clearAndFree(); + + var count_key_itr = this.counts.keyIterator(); + while (count_key_itr.next()) |key| { + this.allocator.free(key.*); + } + this.counts.clearAndFree(); + } + } + + fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { + if (this._current_file == null or this._current_file.?.id != file_id) { + try this.writeSnapshotFile(); + + const test_file = Jest.runner.?.files.get(file_id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + + if (this.snapshot_dir_path == null or !strings.eqlLong(dir_path, this.snapshot_dir_path.?, true)) { + remain[0] = 0; + const snapshot_dir_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + switch (JSC.Node.Syscall.mkdir(snapshot_dir_path, 0o777)) { + .result => this.snapshot_dir_path = dir_path, + .err => |err| { + switch (err.getErrno()) { + std.os.E.EXIST => this.snapshot_dir_path = dir_path, + else => return JSC.Maybe(void){ + .err = err, + }, + } + }, + } + } + + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + var flags: JSC.Node.Mode = std.os.O.CREAT | std.os.O.RDWR; + if (this.update_snapshots) flags |= std.os.O.TRUNC; + const fd = switch (JSC.Node.Syscall.open(snapshot_file_path, flags, 0o644)) { + .result => |_fd| _fd, + .err => |err| return JSC.Maybe(void){ + .err = err, + }, + }; + + var file: File = .{ + .id = file_id, + .file = .{ .handle = fd }, + }; + + if (this.update_snapshots) { + try this.file_buf.appendSlice(file_header); + } else { + const length = try file.file.getEndPos(); + if (length == 0) { + try this.file_buf.appendSlice(file_header); + } else { + const buf = try this.allocator.alloc(u8, length); + _ = try file.file.preadAll(buf, 0); + try this.file_buf.appendSlice(buf); + this.allocator.free(buf); + } + } + + this._current_file = file; + try this.parseFile(); + } + + return JSC.Maybe(void).success; + } +}; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index b7712c0de..445473839 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -41,7 +41,7 @@ const HTTPThread = @import("root").bun.HTTP.HTTPThread; const JSC = @import("root").bun.JSC; const jest = JSC.Jest; const TestRunner = JSC.Jest.TestRunner; -const Snapshots = JSC.Jest.Snapshots; +const Snapshots = JSC.Snapshot.Snapshots; const Test = TestRunner.Test; const NetworkThread = @import("root").bun.HTTP.NetworkThread; const uws = @import("root").bun.uws; diff --git a/src/jsc.zig b/src/jsc.zig index 26ad7cc5f..67cf3f05c 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -26,6 +26,8 @@ pub const Cloudflare = struct { pub const AttributeIterator = @import("./bun.js/api/html_rewriter.zig").AttributeIterator; }; pub const Jest = @import("./bun.js/test/jest.zig"); +pub const Expect = @import("./bun.js/test/expect.zig"); +pub const Snapshot = @import("./bun.js/test/snapshot.zig"); pub const API = struct { pub const JSBundler = @import("./bun.js/api/JSBundler.zig").JSBundler; pub const BuildArtifact = @import("./bun.js/api/JSBundler.zig").BuildArtifact; diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 47ea17db3..534c17513 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -192,14 +192,34 @@ describe("bun test", () => { }); expect(stderr).toContain("Invalid timeout"); }); + test("timeout can be set to 0ms", () => { + const stderr = runTest({ + args: ["--timeout", "0"], + input: ` + import { test, expect } from "bun:test"; + import { sleep } from "bun"; + test("ok", async () => { + await expect(Promise.resolve()).resolves.toBeUndefined(); + await expect(Promise.reject()).rejects.toBeUndefined(); + }); + test("timeout", async () => { + await expect(sleep(1)).resolves.toBeUndefined(); + }); + `, + }); + expect(stderr).toContain("timed out after 0ms"); + }); test("timeout can be set to 1ms", () => { const stderr = runTest({ args: ["--timeout", "1"], input: ` import { test, expect } from "bun:test"; import { sleep } from "bun"; + test("ok", async () => { + await expect(sleep(1)).resolves.toBeUndefined(); + }); test("timeout", async () => { - await sleep(2); + await expect(sleep(2)).resolves.toBeUndefined(); }); `, }); -- cgit v1.2.3 From 069b42a7cc1275969859dc60e7c303528ca2dccb Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 24 Jun 2023 03:24:34 -0300 Subject: [feat] fs.watch (#3249) * initial support * add types * fix comment * fix types * bigfix up * more fixes * fix some encoding support for watch * fix rename event * fixup * fix latin1 * add fs_events, still failing some tests * fixuup * remove unecesary check * readd tests ops * this is necessary? just testing CI/CD weird errors * just use dupe here * cleanup and fix deinit * fix zig upgrade --- packages/bun-types/fs.d.ts | 97 +++ packages/bun-types/fs/promises.d.ts | 58 ++ src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- src/bun.js/bindings/JSSinkLookupTable.h | 2 +- .../ZigGeneratedClasses+DOMClientIsoSubspaces.h | 1 + .../bindings/ZigGeneratedClasses+DOMIsoSubspaces.h | 1 + .../ZigGeneratedClasses+lazyStructureHeader.h | 6 + .../ZigGeneratedClasses+lazyStructureImpl.h | 7 + src/bun.js/bindings/ZigGeneratedClasses.cpp | 322 ++++++++ src/bun.js/bindings/ZigGeneratedClasses.h | 56 ++ src/bun.js/bindings/generated_classes.zig | 94 +++ src/bun.js/bindings/generated_classes_list.zig | 1 + src/bun.js/event_loop.zig | 7 + src/bun.js/javascript.zig | 7 + src/bun.js/node/fs_events.zig | 609 ++++++++++++++ src/bun.js/node/node.classes.ts | 30 +- src/bun.js/node/node_fs.zig | 13 +- src/bun.js/node/node_fs_binding.zig | 2 + src/bun.js/node/node_fs_watcher.zig | 913 +++++++++++++++++++++ src/bun.js/node/types.zig | 4 + src/bun.js/webcore/encoding.zig | 15 +- src/fs.zig | 54 ++ src/http.zig | 7 +- src/js/node/fs.js | 65 +- src/js/node/fs.promises.ts | 51 ++ src/js/out/modules/node/fs.js | 50 +- src/js/out/modules/node/fs.promises.js | 2 +- src/js/private.d.ts | 86 +- src/jsc.zig | 1 + src/watcher.zig | 68 +- test/js/node/watch/fixtures/close.js | 7 + test/js/node/watch/fixtures/persistent.js | 5 + test/js/node/watch/fixtures/relative.js | 23 + test/js/node/watch/fixtures/unref.js | 7 + test/js/node/watch/fs.watch.test.js | 424 ++++++++++ 36 files changed, 3072 insertions(+), 27 deletions(-) create mode 100644 src/bun.js/node/fs_events.zig create mode 100644 src/bun.js/node/node_fs_watcher.zig create mode 100644 test/js/node/watch/fixtures/close.js create mode 100644 test/js/node/watch/fixtures/persistent.js create mode 100644 test/js/node/watch/fixtures/relative.js create mode 100644 test/js/node/watch/fixtures/unref.js create mode 100644 test/js/node/watch/fs.watch.test.js (limited to 'src/bun.js/javascript.zig') diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts index 14c5c1d1d..5dfb2c7f2 100644 --- a/packages/bun-types/fs.d.ts +++ b/packages/bun-types/fs.d.ts @@ -19,6 +19,7 @@ */ declare module "fs" { import * as stream from "stream"; + import type EventEmitter from "events"; import type { SystemError, ArrayBufferView } from "bun"; interface ObjectEncodingOptions { encoding?: BufferEncoding | null | undefined; @@ -3929,6 +3930,102 @@ declare module "fs" { */ recursive?: boolean; } + + export interface FSWatcher extends EventEmitter { + /** + * Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the `fs.FSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; + + /** + * events.EventEmitter + * 1. change + * 2. error + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + addListener(event: 'error', listener: (error: Error) => void): this; + addListener(event: 'close', listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + on(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'close', listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: 'error', listener: (error: Error) => void): this; + once(event: 'close', listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependListener(event: 'error', listener: (error: Error) => void): this; + prependListener(event: 'close', listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependOnceListener(event: 'error', listener: (error: Error) => void): this; + prependOnceListener(event: 'close', listener: () => void): this; + } + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'` or `'change'`, and `filename` is the name of the file + * which triggered the event. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * The listener callback is attached to the `'change'` event fired by `fs.FSWatcher`, but it is not the same thing as the `'change'` value of`eventType`. + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `fs.FSWatcher`. + * @since v0.6.8 + * @param listener + */ + export function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer', + listener?: WatchListener + ): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options?: WatchOptions | BufferEncoding | null, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options: WatchOptions | string, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + export function watch(filename: PathLike, listener?: WatchListener): FSWatcher; } declare module "node:fs" { diff --git a/packages/bun-types/fs/promises.d.ts b/packages/bun-types/fs/promises.d.ts index 0d71464b9..2b908fceb 100644 --- a/packages/bun-types/fs/promises.d.ts +++ b/packages/bun-types/fs/promises.d.ts @@ -26,6 +26,7 @@ declare module "fs/promises" { Abortable, RmOptions, RmDirOptions, + WatchOptions, } from "node:fs"; const constants: typeof import("node:fs")["constants"]; @@ -709,6 +710,63 @@ declare module "fs/promises" { * To remove a directory recursively, use `fs.promises.rm()` instead, with the `recursive` option set to `true`. */ function rmdir(path: PathLike, options?: RmDirOptions): Promise; + + /** + * Returns an async iterator that watches for changes on `filename`, where `filename`is either a file or a directory. + * + * ```js + * const { watch } = require('node:fs/promises'); + * + * const ac = new AbortController(); + * const { signal } = ac; + * setTimeout(() => ac.abort(), 10000); + * + * (async () => { + * try { + * const watcher = watch(__filename, { signal }); + * for await (const event of watcher) + * console.log(event); + * } catch (err) { + * if (err.name === 'AbortError') + * return; + * throw err; + * } + * })(); + * ``` + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * All the `caveats` for `fs.watch()` also apply to `fsPromises.watch()`. + * @since v0.6.8 + * @return of objects with the properties: + */ + function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer' + ): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options?: WatchOptions | BufferEncoding): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options: WatchOptions | string): AsyncIterable> | AsyncIterable>; } declare module "node:fs/promises" { diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index 36be334dd..4acf01ff7 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.447Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 5bbfab777..37c458e9b 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.446Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // #pragma once diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h index a4ace6dc3..e4ed81629 100644 --- a/src/bun.js/bindings/JSSinkLookupTable.h +++ b/src/bun.js/bindings/JSSinkLookupTable.h @@ -1,4 +1,4 @@ -// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/jarred/Code/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! +// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h index b16febcdb..f0d491c0b 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_clientSubspaceForExpectConstructor;std: std::unique_ptr m_clientSubspaceForExpectAnything; std::unique_ptr m_clientSubspaceForExpectStringContaining; std::unique_ptr m_clientSubspaceForExpectStringMatching; +std::unique_ptr m_clientSubspaceForFSWatcher; std::unique_ptr m_clientSubspaceForFileSystemRouter; std::unique_ptr m_clientSubspaceForFileSystemRouterConstructor;std::unique_ptr m_clientSubspaceForListener; std::unique_ptr m_clientSubspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h index 59263e62c..02a9adbca 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_subspaceForExpectConstructor;std::unique_ptr m_subspaceForExpectAnything; std::unique_ptr m_subspaceForExpectStringContaining; std::unique_ptr m_subspaceForExpectStringMatching; +std::unique_ptr m_subspaceForFSWatcher; std::unique_ptr m_subspaceForFileSystemRouter; std::unique_ptr m_subspaceForFileSystemRouterConstructor;std::unique_ptr m_subspaceForListener; std::unique_ptr m_subspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h index 4471fbab3..ac03032e6 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h @@ -58,6 +58,12 @@ JSC::Structure* JSExpectStringMatchingStructure() { return m_JSExpectStringMatch JSC::LazyClassStructure m_JSExpectStringMatching; bool hasJSExpectStringMatchingSetterValue { false }; mutable JSC::WriteBarrier m_JSExpectStringMatchingSetterValue; +JSC::Structure* JSFSWatcherStructure() { return m_JSFSWatcher.getInitializedOnMainThread(this); } + JSC::JSObject* JSFSWatcherConstructor() { return m_JSFSWatcher.constructorInitializedOnMainThread(this); } + JSC::JSValue JSFSWatcherPrototype() { return m_JSFSWatcher.prototypeInitializedOnMainThread(this); } + JSC::LazyClassStructure m_JSFSWatcher; + bool hasJSFSWatcherSetterValue { false }; + mutable JSC::WriteBarrier m_JSFSWatcherSetterValue; JSC::Structure* JSFileSystemRouterStructure() { return m_JSFileSystemRouter.getInitializedOnMainThread(this); } JSC::JSObject* JSFileSystemRouterConstructor() { return m_JSFileSystemRouter.constructorInitializedOnMainThread(this); } JSC::JSValue JSFileSystemRouterPrototype() { return m_JSFileSystemRouter.prototypeInitializedOnMainThread(this); } diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h index 4e5a2c1fa..b3b5327a4 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h @@ -58,6 +58,12 @@ void GlobalObject::initGeneratedLazyClasses() { init.setPrototype(WebCore::JSExpectStringMatching::createPrototype(init.vm, reinterpret_cast(init.global))); init.setStructure(WebCore::JSExpectStringMatching::createStructure(init.vm, init.global, init.prototype)); + }); + m_JSFSWatcher.initLater( + [](LazyClassStructure::Initializer& init) { + init.setPrototype(WebCore::JSFSWatcher::createPrototype(init.vm, reinterpret_cast(init.global))); + init.setStructure(WebCore::JSFSWatcher::createStructure(init.vm, init.global, init.prototype)); + }); m_JSFileSystemRouter.initLater( [](LazyClassStructure::Initializer& init) { @@ -211,6 +217,7 @@ void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& thisObject->m_JSExpectAnything.visit(visitor); visitor.append(thisObject->m_JSExpectAnythingSetterValue); thisObject->m_JSExpectStringContaining.visit(visitor); visitor.append(thisObject->m_JSExpectStringContainingSetterValue); thisObject->m_JSExpectStringMatching.visit(visitor); visitor.append(thisObject->m_JSExpectStringMatchingSetterValue); + thisObject->m_JSFSWatcher.visit(visitor); visitor.append(thisObject->m_JSFSWatcherSetterValue); thisObject->m_JSFileSystemRouter.visit(visitor); visitor.append(thisObject->m_JSFileSystemRouterSetterValue); thisObject->m_JSListener.visit(visitor); visitor.append(thisObject->m_JSListenerSetterValue); thisObject->m_JSMD4.visit(visitor); visitor.append(thisObject->m_JSMD4SetterValue); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index d51a1959a..e0a3f33d6 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -5381,6 +5381,297 @@ void JSExpectStringMatching::visitOutputConstraintsImpl(JSCell* cell, Visitor& v } DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpectStringMatching); +class JSFSWatcherPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSFSWatcherPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSFSWatcherPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcherPrototype(vm, globalObject, structure); + ptr->finishCreation(vm, globalObject); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSFSWatcherPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); +}; + +extern "C" void FSWatcherClass__finalize(void*); + +extern "C" EncodedJSValue FSWatcherPrototype__doClose(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__closeCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__hasRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__refCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback); + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSFSWatcherPrototype, JSFSWatcherPrototype::Base); + +static const HashTableValue JSFSWatcherPrototypeTableValues[] = { + { "close"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__closeCallback, 0 } }, + { "hasRef"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__hasRefCallback, 0 } }, + { "ref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__refCallback, 0 } }, + { "unref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__unrefCallback, 0 } } +}; + +const ClassInfo JSFSWatcherPrototype::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcherPrototype) }; + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__closeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doClose(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__hasRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__refCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +extern "C" void FSWatcherPrototype__listenerSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSValue::decode(thisValue)); + thisObject->m_listener.set(vm, thisObject, JSValue::decode(value)); +} + +extern "C" EncodedJSValue FSWatcherPrototype__listenerGetCachedValue(JSC::EncodedJSValue thisValue) +{ + auto* thisObject = jsCast(JSValue::decode(thisValue)); + return JSValue::encode(thisObject->m_listener.get()); +} + +void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSFSWatcher::info(), JSFSWatcherPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSFSWatcher::~JSFSWatcher() +{ + if (m_ctx) { + FSWatcherClass__finalize(m_ctx); + } +} +void JSFSWatcher::destroy(JSCell* cell) +{ + static_cast(cell)->JSFSWatcher::~JSFSWatcher(); +} + +const ClassInfo JSFSWatcher::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcher) }; + +void JSFSWatcher::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSFSWatcher* JSFSWatcher::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) +{ + JSFSWatcher* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcher(vm, structure, ctx); + ptr->finishCreation(vm); + return ptr; +} + +extern "C" void* FSWatcher__fromJS(JSC::EncodedJSValue value) +{ + JSC::JSValue decodedValue = JSC::JSValue::decode(value); + if (decodedValue.isEmpty() || !decodedValue.isCell()) + return nullptr; + + JSC::JSCell* cell = decodedValue.asCell(); + JSFSWatcher* object = JSC::jsDynamicCast(cell); + + if (!object) + return nullptr; + + return object->wrapped(); +} + +extern "C" bool FSWatcher__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr) +{ + JSFSWatcher* object = JSC::jsDynamicCast(JSValue::decode(value)); + if (!object) + return false; + + object->m_ctx = ptr; + return true; +} + +extern "C" const size_t FSWatcher__ptrOffset = JSFSWatcher::offsetOfWrapped(); + +void JSFSWatcher::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + if (void* wrapped = thisObject->wrapped()) { + // if (thisObject->scriptExecutionContext()) + // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string()); + } + Base::analyzeHeap(cell, analyzer); +} + +JSObject* JSFSWatcher::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) +{ + return JSFSWatcherPrototype::create(vm, globalObject, JSFSWatcherPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +extern "C" EncodedJSValue FSWatcher__create(Zig::GlobalObject* globalObject, void* ptr) +{ + auto& vm = globalObject->vm(); + JSC::Structure* structure = globalObject->JSFSWatcherStructure(); + JSFSWatcher* instance = JSFSWatcher::create(vm, globalObject, structure, ptr); + + return JSValue::encode(instance); +} + +template +void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitAdditionalChildren(Visitor& visitor) +{ + JSFSWatcher* thisObject = this; + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + thisObject->visitAdditionalChildren(visitor); +} + +DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSFSWatcher); class JSFileSystemRouterPrototype final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; @@ -7654,6 +7945,9 @@ JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesCallback); extern "C" EncodedJSValue NodeJSFSPrototype__utimesSync(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback); +extern "C" EncodedJSValue NodeJSFSPrototype__watch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback); + extern "C" EncodedJSValue NodeJSFSPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback); @@ -7751,6 +8045,7 @@ static const HashTableValue JSNodeJSFSPrototypeTableValues[] = { { "unlinkSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__unlinkSyncCallback, 1 } }, { "utimes"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesCallback, 4 } }, { "utimesSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesSyncCallback, 3 } }, + { "watch"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__watchCallback, 3 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeCallback, 6 } }, { "writeFile"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileCallback, 4 } }, { "writeFileSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileSyncCallback, 3 } }, @@ -9795,6 +10090,33 @@ JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback, (JSGlobalObject return NodeJSFSPrototype__utimesSync(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSNodeJSFS* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return NodeJSFSPrototype__watch(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index 668cd3f6b..3fa0e26d2 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -578,6 +578,62 @@ public: mutable JSC::WriteBarrier m_testValue; }; +class JSFSWatcher final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static JSFSWatcher* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); + + DECLARE_EXPORT_INFO; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForFSWatcher = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForFSWatcher = std::forward(space); }); + } + + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(0b11101110), StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); + ; + + ~JSFSWatcher(); + + void* wrapped() const { return m_ctx; } + + void detach() + { + m_ctx = nullptr; + } + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSFSWatcher, m_ctx); } + + void* m_ctx { nullptr }; + + JSFSWatcher(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + : Base(vm, structure) + { + m_ctx = sinkPtr; + } + + void finishCreation(JSC::VM&); + + DECLARE_VISIT_CHILDREN; + template void visitAdditionalChildren(Visitor&); + DECLARE_VISIT_OUTPUT_CONSTRAINTS; + + mutable JSC::WriteBarrier m_listener; +}; + class JSFileSystemRouter final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 0ec65a469..74e30cd83 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -1406,6 +1406,96 @@ pub const JSExpectStringMatching = struct { } } }; +pub const JSFSWatcher = struct { + const FSWatcher = Classes.FSWatcher; + const GetterType = fn (*FSWatcher, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const GetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const SetterType = fn (*FSWatcher, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const SetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const CallbackType = fn (*FSWatcher, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue; + + /// Return the pointer to the wrapped object. + /// If the object does not match the type, return null. + pub fn fromJS(value: JSC.JSValue) ?*FSWatcher { + JSC.markBinding(@src()); + return FSWatcher__fromJS(value); + } + + extern fn FSWatcherPrototype__listenerSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; + + extern fn FSWatcherPrototype__listenerGetCachedValue(JSC.JSValue) JSC.JSValue; + + /// `FSWatcher.listener` setter + /// This value will be visited by the garbage collector. + pub fn listenerSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + JSC.markBinding(@src()); + FSWatcherPrototype__listenerSetCachedValue(thisValue, globalObject, value); + } + + /// `FSWatcher.listener` getter + /// This value will be visited by the garbage collector. + pub fn listenerGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { + JSC.markBinding(@src()); + const result = FSWatcherPrototype__listenerGetCachedValue(thisValue); + if (result == .zero) + return null; + + return result; + } + + /// Create a new instance of FSWatcher + pub fn toJS(this: *FSWatcher, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(@src()); + if (comptime Environment.allow_assert) { + const value__ = FSWatcher__create(globalObject, this); + std.debug.assert(value__.as(FSWatcher).? == this); // If this fails, likely a C ABI issue. + return value__; + } else { + return FSWatcher__create(globalObject, this); + } + } + + /// Modify the internal ptr to point to a new instance of FSWatcher. + pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*FSWatcher) bool { + JSC.markBinding(@src()); + return FSWatcher__dangerouslySetPtr(value, ptr); + } + + /// Detach the ptr from the thisValue + pub fn detachPtr(_: *FSWatcher, value: JSC.JSValue) void { + JSC.markBinding(@src()); + std.debug.assert(FSWatcher__dangerouslySetPtr(value, null)); + } + + extern fn FSWatcher__fromJS(JSC.JSValue) ?*FSWatcher; + extern fn FSWatcher__getConstructor(*JSC.JSGlobalObject) JSC.JSValue; + + extern fn FSWatcher__create(globalObject: *JSC.JSGlobalObject, ptr: ?*FSWatcher) JSC.JSValue; + + extern fn FSWatcher__dangerouslySetPtr(JSC.JSValue, ?*FSWatcher) bool; + + comptime { + if (@TypeOf(FSWatcher.finalize) != (fn (*FSWatcher) callconv(.C) void)) { + @compileLog("FSWatcher.finalize is not a finalizer"); + } + + if (@TypeOf(FSWatcher.doClose) != CallbackType) + @compileLog("Expected FSWatcher.doClose to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doClose))); + if (@TypeOf(FSWatcher.hasRef) != CallbackType) + @compileLog("Expected FSWatcher.hasRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.hasRef))); + if (@TypeOf(FSWatcher.doRef) != CallbackType) + @compileLog("Expected FSWatcher.doRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doRef))); + if (@TypeOf(FSWatcher.doUnref) != CallbackType) + @compileLog("Expected FSWatcher.doUnref to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doUnref))); + if (!JSC.is_bindgen) { + @export(FSWatcher.doClose, .{ .name = "FSWatcherPrototype__doClose" }); + @export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" }); + @export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" }); + @export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" }); + @export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" }); + } + } +}; pub const JSFileSystemRouter = struct { const FileSystemRouter = Classes.FileSystemRouter; const GetterType = fn (*FileSystemRouter, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; @@ -2312,6 +2402,8 @@ pub const JSNodeJSFS = struct { @compileLog("Expected NodeJSFS.utimes to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimes))); if (@TypeOf(NodeJSFS.utimesSync) != CallbackType) @compileLog("Expected NodeJSFS.utimesSync to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimesSync))); + if (@TypeOf(NodeJSFS.watch) != CallbackType) + @compileLog("Expected NodeJSFS.watch to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.watch))); if (@TypeOf(NodeJSFS.write) != CallbackType) @compileLog("Expected NodeJSFS.write to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.write))); if (@TypeOf(NodeJSFS.writeFile) != CallbackType) @@ -2402,6 +2494,7 @@ pub const JSNodeJSFS = struct { @export(NodeJSFS.unlinkSync, .{ .name = "NodeJSFSPrototype__unlinkSync" }); @export(NodeJSFS.utimes, .{ .name = "NodeJSFSPrototype__utimes" }); @export(NodeJSFS.utimesSync, .{ .name = "NodeJSFSPrototype__utimesSync" }); + @export(NodeJSFS.watch, .{ .name = "NodeJSFSPrototype__watch" }); @export(NodeJSFS.write, .{ .name = "NodeJSFSPrototype__write" }); @export(NodeJSFS.writeFile, .{ .name = "NodeJSFSPrototype__writeFile" }); @export(NodeJSFS.writeFileSync, .{ .name = "NodeJSFSPrototype__writeFileSync" }); @@ -4855,6 +4948,7 @@ comptime { _ = JSExpectAnything; _ = JSExpectStringContaining; _ = JSExpectStringMatching; + _ = JSFSWatcher; _ = JSFileSystemRouter; _ = JSListener; _ = JSMD4; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index c54965093..d90267337 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -37,4 +37,5 @@ pub const Classes = struct { pub const BuildArtifact = JSC.API.BuildArtifact; pub const BuildMessage = JSC.BuildMessage; pub const ResolveMessage = JSC.ResolveMessage; + pub const FSWatcher = JSC.Node.FSWatcher.JSObject; }; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 0a3459d64..a3ccd16ad 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -224,6 +224,7 @@ pub const CppTask = opaque { const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const MicrotaskForDefaultGlobalObject = JSC.MicrotaskForDefaultGlobalObject; const HotReloadTask = JSC.HotReloader.HotReloadTask; +const FSWatchTask = JSC.Node.FSWatcher.FSWatchTask; const PollPendingModulesTask = JSC.ModuleLoader.AsyncModule.Queue; // const PromiseTask = JSInternalPromise.Completion.PromiseTask; const GetAddrInfoRequestTask = JSC.DNS.GetAddrInfoRequest.Task; @@ -242,6 +243,7 @@ pub const Task = TaggedPointerUnion(.{ HotReloadTask, PollPendingModulesTask, GetAddrInfoRequestTask, + FSWatchTask, // PromiseTask, // TimeoutTasklet, }); @@ -467,6 +469,11 @@ pub const EventLoop = struct { // special case: we return return 0; }, + .FSWatchTask => { + var transform_task: *FSWatchTask = task.get(FSWatchTask).?; + transform_task.*.run(); + transform_task.deinit(); + }, @field(Task.Tag, typeBaseName(@typeName(AnyTask))) => { var any: *AnyTask = task.get(AnyTask).?; any.run(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index bebfbeb18..3baa25e22 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2609,6 +2609,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime return this.tombstones.get(key); } + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } + pub fn onFileUpdate( this: *@This(), events: []watcher.WatchEvent, diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig new file mode 100644 index 000000000..a3fba5441 --- /dev/null +++ b/src/bun.js/node/fs_events.zig @@ -0,0 +1,609 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Environment = bun.Environment; +const Mutex = @import("../../lock.zig").Lock; +const sync = @import("../../sync.zig"); +const Semaphore = sync.Semaphore; +const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; +const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion; +const string = bun.string; + +pub const CFAbsoluteTime = f64; +pub const CFTimeInterval = f64; +pub const CFArrayCallBacks = anyopaque; + +pub const FSEventStreamEventFlags = c_int; +pub const OSStatus = c_int; +pub const CFIndex = c_long; + +pub const FSEventStreamCreateFlags = u32; +pub const FSEventStreamEventId = u64; + +pub const CFStringEncoding = c_uint; + +pub const CFArrayRef = ?*anyopaque; +pub const CFAllocatorRef = ?*anyopaque; +pub const CFBundleRef = ?*anyopaque; +pub const CFDictionaryRef = ?*anyopaque; +pub const CFRunLoopRef = ?*anyopaque; +pub const CFRunLoopSourceRef = ?*anyopaque; +pub const CFStringRef = ?*anyopaque; +pub const CFTypeRef = ?*anyopaque; +pub const FSEventStreamRef = ?*anyopaque; +pub const FSEventStreamCallback = *const fn (FSEventStreamRef, ?*anyopaque, usize, ?*anyopaque, *FSEventStreamEventFlags, *FSEventStreamEventId) callconv(.C) void; + +// we only care about info and perform +pub const CFRunLoopSourceContext = extern struct { + version: CFIndex = 0, + info: *anyopaque, + retain: ?*anyopaque = null, + release: ?*anyopaque = null, + copyDescription: ?*anyopaque = null, + equal: ?*anyopaque = null, + hash: ?*anyopaque = null, + schedule: ?*anyopaque = null, + cancel: ?*anyopaque = null, + perform: *const fn (?*anyopaque) callconv(.C) void, +}; + +pub const FSEventStreamContext = extern struct { + version: CFIndex = 0, + info: ?*anyopaque = null, + pad: [3]?*anyopaque = .{ null, null, null }, +}; + +pub const kCFStringEncodingUTF8: CFStringEncoding = 0x8000100; +pub const noErr: OSStatus = 0; + +pub const kFSEventStreamCreateFlagNoDefer: c_int = 2; +pub const kFSEventStreamCreateFlagFileEvents: c_int = 16; + +pub const kFSEventStreamEventFlagEventIdsWrapped: c_int = 8; +pub const kFSEventStreamEventFlagHistoryDone: c_int = 16; +pub const kFSEventStreamEventFlagItemChangeOwner: c_int = 0x4000; +pub const kFSEventStreamEventFlagItemCreated: c_int = 0x100; +pub const kFSEventStreamEventFlagItemFinderInfoMod: c_int = 0x2000; +pub const kFSEventStreamEventFlagItemInodeMetaMod: c_int = 0x400; +pub const kFSEventStreamEventFlagItemIsDir: c_int = 0x20000; +pub const kFSEventStreamEventFlagItemModified: c_int = 0x1000; +pub const kFSEventStreamEventFlagItemRemoved: c_int = 0x200; +pub const kFSEventStreamEventFlagItemRenamed: c_int = 0x800; +pub const kFSEventStreamEventFlagItemXattrMod: c_int = 0x8000; +pub const kFSEventStreamEventFlagKernelDropped: c_int = 4; +pub const kFSEventStreamEventFlagMount: c_int = 64; +pub const kFSEventStreamEventFlagRootChanged: c_int = 32; +pub const kFSEventStreamEventFlagUnmount: c_int = 128; +pub const kFSEventStreamEventFlagUserDropped: c_int = 2; + +// Lazy function call binding. +const RTLD_LAZY = 0x1; +// Symbols exported from this image (dynamic library or bundle) +// are generally hidden and only availble to dlsym() when +// directly using the handle returned by this call to dlopen(). +const RTLD_LOCAL = 0x4; + +pub const kFSEventsModified: c_int = + kFSEventStreamEventFlagItemChangeOwner | + kFSEventStreamEventFlagItemFinderInfoMod | + kFSEventStreamEventFlagItemInodeMetaMod | + kFSEventStreamEventFlagItemModified | + kFSEventStreamEventFlagItemXattrMod; + +pub const kFSEventsRenamed: c_int = + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed; + +pub const kFSEventsSystem: c_int = + kFSEventStreamEventFlagUserDropped | + kFSEventStreamEventFlagKernelDropped | + kFSEventStreamEventFlagEventIdsWrapped | + kFSEventStreamEventFlagHistoryDone | + kFSEventStreamEventFlagMount | + kFSEventStreamEventFlagUnmount | + kFSEventStreamEventFlagRootChanged; + +var fsevents_mutex: Mutex = Mutex.init(); +var fsevents_default_loop_mutex: Mutex = Mutex.init(); +var fsevents_default_loop: ?*FSEventsLoop = null; + +fn dlsym(handle: ?*anyopaque, comptime Type: type, comptime symbol: [:0]const u8) ?Type { + if (std.c.dlsym(handle, symbol)) |ptr| { + return bun.cast(Type, ptr); + } + return null; +} + +pub const CoreFoundation = struct { + handle: ?*anyopaque, + ArrayCreate: *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, + Release: *fn (CFTypeRef) callconv(.C) void, + + RunLoopAddSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopGetCurrent: *fn () callconv(.C) CFRunLoopRef, + RunLoopRemoveSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopRun: *fn () callconv(.C) void, + RunLoopSourceCreate: *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, + RunLoopSourceSignal: *fn (CFRunLoopSourceRef) callconv(.C) void, + RunLoopStop: *fn (CFRunLoopRef) callconv(.C) void, + RunLoopWakeUp: *fn (CFRunLoopRef) callconv(.C) void, + StringCreateWithFileSystemRepresentation: *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, + RunLoopDefaultMode: *CFStringRef, + + pub fn get() CoreFoundation { + if (fsevents_cf) |cf| return cf; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cf) |cf| return cf; + + InitLibrary(); + + return fsevents_cf.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreFoundation) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +pub const CoreServices = struct { + handle: ?*anyopaque, + FSEventStreamCreate: *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, + FSEventStreamInvalidate: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamRelease: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamScheduleWithRunLoop: *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, + FSEventStreamStart: *fn (FSEventStreamRef) callconv(.C) c_int, + FSEventStreamStop: *fn (FSEventStreamRef) callconv(.C) void, + // libuv set it to -1 so the actual value is this + kFSEventStreamEventIdSinceNow: FSEventStreamEventId = 18446744073709551615, + + pub fn get() CoreServices { + if (fsevents_cs) |cs| return cs; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cs) |cs| return cs; + + InitLibrary(); + + return fsevents_cs.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreServices) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +var fsevents_cf: ?CoreFoundation = null; +var fsevents_cs: ?CoreServices = null; + +fn InitLibrary() void { + const fsevents_cf_handle = std.c.dlopen("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cf_handle == null) @panic("Cannot Load CoreFoundation"); + + fsevents_cf = CoreFoundation{ + .handle = fsevents_cf_handle, + .ArrayCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, "CFArrayCreate") orelse @panic("Cannot Load CoreFoundation"), + .Release = dlsym(fsevents_cf_handle, *fn (CFTypeRef) callconv(.C) void, "CFRelease") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopAddSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopAddSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopGetCurrent = dlsym(fsevents_cf_handle, *fn () callconv(.C) CFRunLoopRef, "CFRunLoopGetCurrent") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRemoveSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopRemoveSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRun = dlsym(fsevents_cf_handle, *fn () callconv(.C) void, "CFRunLoopRun") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, "CFRunLoopSourceCreate") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceSignal = dlsym(fsevents_cf_handle, *fn (CFRunLoopSourceRef) callconv(.C) void, "CFRunLoopSourceSignal") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopStop = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopStop") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopWakeUp = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopWakeUp") orelse @panic("Cannot Load CoreFoundation"), + .StringCreateWithFileSystemRepresentation = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, "CFStringCreateWithFileSystemRepresentation") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopDefaultMode = dlsym(fsevents_cf_handle, *CFStringRef, "kCFRunLoopDefaultMode") orelse @panic("Cannot Load CoreFoundation"), + }; + + const fsevents_cs_handle = std.c.dlopen("/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cs_handle == null) @panic("Cannot Load CoreServices"); + + fsevents_cs = CoreServices{ + .handle = fsevents_cs_handle, + .FSEventStreamCreate = dlsym(fsevents_cs_handle, *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, "FSEventStreamCreate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamInvalidate = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamInvalidate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamRelease = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamRelease") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamScheduleWithRunLoop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, "FSEventStreamScheduleWithRunLoop") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStart = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) c_int, "FSEventStreamStart") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamStop") orelse @panic("Cannot Load CoreServices"), + }; +} + +pub const FSEventsLoop = struct { + signal_source: CFRunLoopSourceRef, + mutex: Mutex, + loop: CFRunLoopRef = null, + sem: Semaphore, + thread: std.Thread = undefined, + tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{}, + watchers: bun.BabyList(?*FSEventsWatcher) = .{}, + watcher_count: u32 = 0, + fsevent_stream: FSEventStreamRef = null, + paths: ?[]?*anyopaque = null, + cf_paths: CFArrayRef = null, + has_scheduled_watchers: bool = false, + + pub const Task = struct { + ctx: ?*anyopaque, + callback: *const (fn (*anyopaque) void), + + pub fn run(this: *Task) void { + var callback = this.callback; + var ctx = this.ctx; + callback(ctx.?); + } + + pub fn New(comptime Type: type, comptime Callback: anytype) type { + return struct { + pub fn init(ctx: *Type) Task { + return Task{ + .callback = wrap, + .ctx = ctx, + }; + } + + pub fn wrap(this: ?*anyopaque) void { + @call(.always_inline, Callback, .{@ptrCast(*Type, @alignCast(@alignOf(Type), this.?))}); + } + }; + } + }; + + pub const ConcurrentTask = struct { + task: Task = undefined, + next: ?*ConcurrentTask = null, + auto_delete: bool = false, + + pub const Queue = UnboundedQueue(ConcurrentTask, .next); + + pub fn from(this: *ConcurrentTask, task: Task) *ConcurrentTask { + this.* = .{ + .task = task, + .next = null, + }; + return this; + } + }; + + pub fn CFThreadLoop(this: *FSEventsLoop) void { + bun.Output.Source.configureNamedThread("CFThreadLoop"); + + const CF = CoreFoundation.get(); + + this.loop = CF.RunLoopGetCurrent(); + + CF.RunLoopAddSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.sem.post(); + + CF.RunLoopRun(); + CF.RunLoopRemoveSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.loop = null; + } + + // Runs in CF thread, executed after `enqueueTaskConcurrent()` + fn CFLoopCallback(arg: ?*anyopaque) callconv(.C) void { + if (arg) |self| { + const this = bun.cast(*FSEventsLoop, self); + + var concurrent = this.tasks.popBatch(); + const count = concurrent.count; + if (count == 0) + return; + + var iter = concurrent.iterator(); + while (iter.next()) |task| { + task.task.run(); + if (task.auto_delete) bun.default_allocator.destroy(task); + } + } + } + + pub fn init() !*FSEventsLoop { + const this = bun.default_allocator.create(FSEventsLoop) catch unreachable; + + const CF = CoreFoundation.get(); + + var ctx = CFRunLoopSourceContext{ + .info = this, + .perform = CFLoopCallback, + }; + + const signal_source = CF.RunLoopSourceCreate(null, 0, &ctx); + if (signal_source == null) { + return error.FailedToCreateCoreFoudationSourceLoop; + } + + var fs_loop = FSEventsLoop{ .sem = Semaphore.init(0), .mutex = Mutex.init(), .signal_source = signal_source }; + + this.* = fs_loop; + this.thread = try std.Thread.spawn(.{}, FSEventsLoop.CFThreadLoop, .{this}); + + // sync threads + this.sem.wait(); + return this; + } + + fn enqueueTaskConcurrent(this: *FSEventsLoop, task: Task) void { + const CF = CoreFoundation.get(); + var concurrent = bun.default_allocator.create(ConcurrentTask) catch unreachable; + concurrent.auto_delete = true; + this.tasks.push(concurrent.from(task)); + CF.RunLoopSourceSignal(this.signal_source); + CF.RunLoopWakeUp(this.loop); + } + + // Runs in CF thread, when there're events in FSEventStream + fn _events_cb(_: FSEventStreamRef, info: ?*anyopaque, numEvents: usize, eventPaths: ?*anyopaque, eventFlags: *FSEventStreamEventFlags, _: *FSEventStreamEventId) callconv(.C) void { + const paths_ptr = bun.cast([*][*:0]const u8, eventPaths); + const paths = paths_ptr[0..numEvents]; + var loop = bun.cast(*FSEventsLoop, info); + const event_flags = bun.cast([*]FSEventStreamEventFlags, eventFlags); + + for (loop.watchers.slice()) |watcher| { + if (watcher) |handle| { + for (paths, 0..) |path_ptr, i| { + var flags = event_flags[i]; + var path = path_ptr[0..bun.len(path_ptr)]; + // Filter out paths that are outside handle's request + if (path.len < handle.path.len or !bun.strings.startsWith(path, handle.path)) { + continue; + } + const is_file = (flags & kFSEventStreamEventFlagItemIsDir) == 0; + + // Remove common prefix, unless the watched folder is "/" + if (!(handle.path.len == 1 and handle.path[0] == '/')) { + path = path[handle.path.len..]; + + // Ignore events with path equal to directory itself + if (path.len <= 1 and is_file) { + continue; + } + if (path.len == 0) { + // Since we're using fsevents to watch the file itself, path == handle.path, and we now need to get the basename of the file back + while (path.len > 0) { + if (bun.strings.startsWithChar(path, '/')) { + path = path[1..]; + break; + } else { + path = path[1..]; + } + } + + // Created and Removed seem to be always set, but don't make sense + flags &= ~kFSEventsRenamed; + } else { + // Skip forward slash + path = path[1..]; + } + } + + // Do not emit events from subdirectories (without option set) + if (path.len == 0 or (bun.strings.containsChar(path, '/') and !handle.recursive)) { + continue; + } + + var is_rename = true; + + if ((flags & kFSEventsRenamed) == 0) { + if ((flags & kFSEventsModified) != 0 or is_file) { + is_rename = false; + } + } + + handle.callback(handle.ctx, path, is_file, is_rename); + } + } + } + } + + // Runs on CF Thread + pub fn _schedule(this: *FSEventsLoop) void { + this.mutex.lock(); + defer this.mutex.unlock(); + this.has_scheduled_watchers = false; + + var watchers = this.watchers.slice(); + + const CF = CoreFoundation.get(); + const CS = CoreServices.get(); + + if (this.fsevent_stream) |stream| { + // Stop emitting events + CS.FSEventStreamStop(stream); + + // Release stream + CS.FSEventStreamInvalidate(stream); + CS.FSEventStreamRelease(stream); + this.fsevent_stream = null; + } + // clean old paths + if (this.paths) |p| { + this.paths = null; + bun.default_allocator.destroy(p); + } + if (this.cf_paths) |cf| { + this.cf_paths = null; + CF.Release(cf); + } + + const paths = bun.default_allocator.alloc(?*anyopaque, this.watcher_count) catch unreachable; + var count: u32 = 0; + for (watchers) |w| { + if (w) |watcher| { + const path = CF.StringCreateWithFileSystemRepresentation(null, watcher.path.ptr); + paths[count] = path; + count += 1; + } + } + + const cf_paths = CF.ArrayCreate(null, paths.ptr, count, null); + var ctx: FSEventStreamContext = .{ + .info = this, + }; + + const latency: CFAbsoluteTime = 0.05; + // Explanation of selected flags: + // 1. NoDefer - without this flag, events that are happening continuously + // (i.e. each event is happening after time interval less than `latency`, + // counted from previous event), will be deferred and passed to callback + // once they'll either fill whole OS buffer, or when this continuous stream + // will stop (i.e. there'll be delay between events, bigger than + // `latency`). + // Specifying this flag will invoke callback after `latency` time passed + // since event. + // 2. FileEvents - fire callback for file changes too (by default it is firing + // it only for directory changes). + // + const flags: FSEventStreamCreateFlags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; + + // + // NOTE: It might sound like a good idea to remember last seen StreamEventId, + // but in reality one dir might have last StreamEventId less than, the other, + // that is being watched now. Which will cause FSEventStream API to report + // changes to files from the past. + // + const ref = CS.FSEventStreamCreate(null, _events_cb, &ctx, cf_paths, CS.kFSEventStreamEventIdSinceNow, latency, flags); + + CS.FSEventStreamScheduleWithRunLoop(ref, this.loop, CF.RunLoopDefaultMode.*); + if (CS.FSEventStreamStart(ref) == 0) { + //clean in case of failure + bun.default_allocator.destroy(paths); + CF.Release(cf_paths); + CS.FSEventStreamInvalidate(ref); + CS.FSEventStreamRelease(ref); + return; + } + this.fsevent_stream = ref; + this.paths = paths; + this.cf_paths = cf_paths; + } + + fn registerWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + if (this.watcher_count == this.watchers.len) { + this.watcher_count += 1; + this.watchers.push(bun.default_allocator, watcher) catch unreachable; + } else { + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w == null) { + watchers[i] = watcher; + this.watcher_count += 1; + break; + } + } + } + + if (this.has_scheduled_watchers == false) { + this.has_scheduled_watchers = true; + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, _schedule).init(this)); + } + } + + fn unregisterWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w) |item| { + if (item == watcher) { + watchers[i] = null; + // if is the last one just pop + if (i == watchers.len - 1) { + this.watchers.len -= 1; + } + this.watcher_count -= 1; + break; + } + } + } + } + + // Runs on CF loop to close the loop + fn _stop(this: *FSEventsLoop) void { + const CF = CoreFoundation.get(); + CF.RunLoopStop(this.loop); + } + fn deinit(this: *FSEventsLoop) void { + // signal close and wait + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, FSEventsLoop._stop).init(this)); + this.thread.join(); + const CF = CoreFoundation.get(); + + CF.Release(this.signal_source); + this.signal_source = null; + + this.sem.deinit(); + this.mutex.deinit(); + if (this.watcher_count > 0) { + while (this.watchers.popOrNull()) |watcher| { + if (watcher) |w| { + // unlink watcher + w.loop = null; + } + } + } + + this.watchers.deinitWithAllocator(bun.default_allocator); + + bun.default_allocator.destroy(this); + } +}; + +pub const FSEventsWatcher = struct { + path: string, + callback: Callback, + loop: ?*FSEventsLoop, + recursive: bool, + ctx: ?*anyopaque, + + const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void; + + pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, ctx: ?*anyopaque) *FSEventsWatcher { + var this = bun.default_allocator.create(FSEventsWatcher) catch unreachable; + this.* = FSEventsWatcher{ + .path = path, + .callback = callback, + .loop = loop, + .recursive = recursive, + .ctx = ctx, + }; + + loop.registerWatcher(this); + return this; + } + + pub fn deinit(this: *FSEventsWatcher) void { + if (this.loop) |loop| { + loop.unregisterWatcher(this); + } + bun.default_allocator.destroy(this); + } +}; + +pub fn watch(path: string, recursive: bool, callback: FSEventsWatcher.Callback, ctx: ?*anyopaque) !*FSEventsWatcher { + if (fsevents_default_loop) |loop| { + return FSEventsWatcher.init(loop, path, recursive, callback, ctx); + } else { + fsevents_default_loop_mutex.lock(); + defer fsevents_default_loop_mutex.unlock(); + if (fsevents_default_loop == null) { + fsevents_default_loop = try FSEventsLoop.init(); + } + return FSEventsWatcher.init(fsevents_default_loop.?, path, recursive, callback, ctx); + } +} diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index f984077e4..ce35c940a 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -1,6 +1,34 @@ import { define } from "../scripts/class-definitions"; export default [ + define({ + name: "FSWatcher", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + klass: {}, + JSType: "0b11101110", + proto: { + ref: { + fn: "doRef", + length: 0, + }, + unref: { + fn: "doUnref", + length: 0, + }, + hasRef: { + fn: "hasRef", + length: 0, + }, + close: { + fn: "doClose", + length: 0, + }, + }, + values: ["listener"], + }), define({ name: "Timeout", construct: false, @@ -300,7 +328,7 @@ export default [ utimes: { fn: "utimes", length: 4 }, utimesSync: { fn: "utimesSync", length: 3 }, // TODO: - // watch: { fn: "watch", length: 3 }, + watch: { fn: "watch", length: 3 }, // watchFile: { fn: "watchFile", length: 3 }, writeFile: { fn: "writeFile", length: 4 }, writeFileSync: { fn: "writeFileSync", length: 3 }, diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 3ea0822e6..21a65251a 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -34,7 +34,6 @@ const Mode = JSC.Node.Mode; const uid_t = std.os.uid_t; const gid_t = std.os.gid_t; - /// u63 to allow one null bit const ReadPosition = u63; @@ -2313,7 +2312,7 @@ pub const Arguments = struct { }; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.Node.FSWatcher.Arguments; pub const WatchFile = void; pub const Fsync = struct { fd: FileDescriptor, @@ -2475,7 +2474,7 @@ const Return = struct { pub const Truncate = void; pub const Unlink = void; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.JSValue; pub const WatchFile = void; pub const Utimes = void; @@ -4181,8 +4180,12 @@ pub const NodeFS = struct { return Maybe(Return.Lutimes).todo; } - pub fn watch(_: *NodeFS, _: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - return Maybe(Return.Watch).todo; + pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { + const watcher = args.createFSWatcher() catch |err| { + args.global_this.throwError(err, "Failed to watch filename"); + return Maybe(Return.Watch){ .result = JSC.JSValue.jsUndefined() }; + }; + return Maybe(Return.Watch){ .result = watcher }; } pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) { return Maybe(Return.CreateReadStream).todo; diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 74b769bf6..f178f0355 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -241,6 +241,8 @@ pub const NodeJSFS = struct { return JSC.Node.Stats.getConstructor(globalThis); } + pub const watch = callSync(.watch); + // Not implemented yet: const notimpl = fdatasync; pub const opendir = notimpl; diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig new file mode 100644 index 000000000..397d51916 --- /dev/null +++ b/src/bun.js/node/node_fs_watcher.zig @@ -0,0 +1,913 @@ +const std = @import("std"); +const JSC = @import("root").bun.JSC; +const bun = @import("root").bun; +const Fs = @import("../../fs.zig"); +const Path = @import("../../resolver/resolve_path.zig"); +const Encoder = JSC.WebCore.Encoder; + +const FSEvents = @import("./fs_events.zig"); + +const VirtualMachine = JSC.VirtualMachine; +const EventLoop = JSC.EventLoop; +const PathLike = JSC.Node.PathLike; +const ArgumentsSlice = JSC.Node.ArgumentsSlice; +const Output = bun.Output; +const string = bun.string; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const Environment = bun.Environment; + +pub const FSWatcher = struct { + const watcher = @import("../../watcher.zig"); + const options = @import("../../options.zig"); + pub const Watcher = watcher.NewWatcher(*FSWatcher); + const log = Output.scoped(.FSWatcher, false); + + pub const ChangeEvent = struct { + hash: Watcher.HashType = 0, + event_type: FSWatchTask.EventType = .change, + time_stamp: i64 = 0, + }; + + onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, + ctx: *VirtualMachine, + js_watcher: ?*JSObject = null, + watcher_instance: ?*FSWatcher.Watcher = null, + verbose: bool = false, + file_paths: bun.BabyList(string) = .{}, + entry_path: ?string = null, + entry_dir: string = "", + last_change_event: ChangeEvent = .{}, + + pub fn toJS(this: *FSWatcher) JSC.JSValue { + return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined(); + } + + pub fn eventLoop(this: FSWatcher) *EventLoop { + return this.ctx.eventLoop(); + } + + pub fn enqueueTaskConcurrent(this: FSWatcher, task: *JSC.ConcurrentTask) void { + this.eventLoop().enqueueTaskConcurrent(task); + } + + pub fn deinit(this: *FSWatcher) void { + while (this.file_paths.popOrNull()) |file_path| { + bun.default_allocator.destroy(file_path); + } + this.file_paths.deinitWithAllocator(bun.default_allocator); + if (this.entry_path) |path| { + this.entry_path = null; + bun.default_allocator.destroy(path); + } + bun.default_allocator.destroy(this); + } + + pub const FSWatchTask = struct { + ctx: *FSWatcher, + count: u8 = 0, + + entries: [8]Entry = undefined, + concurrent_task: JSC.ConcurrentTask = undefined, + + pub const EventType = enum { + rename, + change, + @"error", + abort, + }; + + pub const EventFreeType = enum { + destroy, + free, + none, + }; + + pub const Entry = struct { + file_path: string, + event_type: EventType, + free_type: EventFreeType, + }; + + pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, free_type: EventFreeType) void { + if (this.count == 8) { + this.enqueue(); + var ctx = this.ctx; + this.* = .{ + .ctx = ctx, + .count = 0, + }; + } + + this.entries[this.count] = .{ + .file_path = file_path, + .event_type = event_type, + .free_type = free_type, + }; + this.count += 1; + } + + pub fn run(this: *FSWatchTask) void { + // this runs on JS Context + if (this.ctx.js_watcher) |js_watcher| { + for (this.entries[0..this.count]) |entry| { + switch (entry.event_type) { + .rename => { + js_watcher.emit(entry.file_path, "rename"); + }, + .change => { + js_watcher.emit(entry.file_path, "change"); + }, + .@"error" => { + // file_path is the error message in this case + js_watcher.emitError(entry.file_path); + }, + .abort => { + js_watcher.emitIfAborted(); + }, + } + } + } + } + + pub fn enqueue(this: *FSWatchTask) void { + if (this.count == 0) + return; + + var that = bun.default_allocator.create(FSWatchTask) catch unreachable; + + that.* = this.*; + this.count = 0; + that.concurrent_task.task = JSC.Task.init(that); + this.ctx.enqueueTaskConcurrent(&that.concurrent_task); + } + + pub fn deinit(this: *FSWatchTask) void { + while (this.count > 0) { + this.count -= 1; + switch (this.entries[this.count].free_type) { + .destroy => bun.default_allocator.destroy(this.entries[this.count].file_path), + .free => bun.default_allocator.free(this.entries[this.count].file_path), + else => {}, + } + } + bun.default_allocator.destroy(this); + } + }; + + fn NewCallback(comptime FunctionSignature: type) type { + return union(enum) { + javascript_callback: JSC.Strong, + zig_callback: struct { + ptr: *anyopaque, + function: *const FunctionSignature, + }, + }; + } + + pub const OnAcceptCallback = NewCallback(fn ( + vm: *JSC.VirtualMachine, + specifier: []const u8, + ) void); + + fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES + 1]u8, is_entry_path: bool) !void { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + + if (is_entry_path) { + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; + } else { + ctx.file_paths.push(bun.default_allocator, dir_path_clone) catch unreachable; + } + fs_watcher.addDirectory(fd, dir_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + var iter = (std.fs.IterableDir{ .dir = std.fs.Dir{ + .fd = fd, + } }).iterate(); + + while (iter.next() catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }) |entry| { + var parts = [2]string{ dir_path_clone, entry.name }; + var entry_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), + buf, + &parts, + .auto, + ); + + buf[entry_path.len] = 0; + var entry_path_z = buf[0..entry_path.len :0]; + + var fs_info = fdFromAbsolutePathZ(entry_path_z) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + if (fs_info.is_file) { + const file_path_clone = bun.default_allocator.dupeZ(u8, entry_path) catch unreachable; + + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + + fs_watcher.addFile(fs_info.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } else { + if (recursive) { + addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } + } + } + } + + pub fn onError( + this: *FSWatcher, + err: anyerror, + ) void { + var current_task: FSWatchTask = .{ + .ctx = this, + }; + current_task.append(@errorName(err), .@"error", .none); + current_task.enqueue(); + } + + pub fn onFSEventUpdate( + ctx: ?*anyopaque, + path: string, + _: bool, + is_rename: bool, + ) void { + const this = bun.cast(*FSWatcher, ctx.?); + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + const relative_path = bun.default_allocator.dupe(u8, path) catch unreachable; + const event_type: FSWatchTask.EventType = if (is_rename) .rename else .change; + + current_task.append(relative_path, event_type, .destroy); + } + + pub fn onFileUpdate( + this: *FSWatcher, + events: []watcher.WatchEvent, + changed_files: []?[:0]u8, + watchlist: watcher.Watchlist, + ) void { + var slice = watchlist.slice(); + const file_paths = slice.items(.file_path); + + var counts = slice.items(.count); + const kinds = slice.items(.kind); + var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + var ctx = this.watcher_instance.?; + defer ctx.flushEvictions(); + defer Output.flush(); + + var bundler = if (@TypeOf(this.ctx.bundler) == *bun.Bundler) + this.ctx.bundler + else + &this.ctx.bundler; + + var fs: *Fs.FileSystem = bundler.fs; + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + const time_stamp = std.time.milliTimestamp(); + const time_diff = time_stamp - this.last_change_event.time_stamp; + + for (events) |event| { + const file_path = file_paths[event.index]; + const update_count = counts[event.index] + 1; + counts[event.index] = update_count; + const kind = kinds[event.index]; + + if (comptime Environment.isDebug) { + if (this.verbose) { + Output.prettyErrorln("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op }); + } + } + + switch (kind) { + .file => { + if (event.op.delete) { + ctx.removeAtIndex( + event.index, + 0, + &.{}, + .file, + ); + } + + var file_hash: FSWatcher.Watcher.HashType = FSWatcher.Watcher.getHash(file_path); + + if (event.op.write or event.op.delete or event.op.rename) { + const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; + // skip consecutive duplicates + if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) { + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + const relative_slice = fs.relative(this.entry_dir, file_path); + + if (this.verbose) + Output.prettyErrorln("File changed: {s}", .{relative_slice}); + + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; + + current_task.append(relative_path, event_type, .destroy); + } + } + }, + .directory => { + // macOS should use FSEvents for directories + if (comptime Environment.isMac) { + @panic("Unexpected directory watch"); + } + + const affected = event.names(changed_files); + + for (affected) |changed_name_| { + const changed_name: []const u8 = bun.asByteSlice(changed_name_.?); + if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; + + var file_hash: FSWatcher.Watcher.HashType = 0; + const relative_slice: string = brk: { + var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); + + @memcpy(_on_file_update_path_buf[0..file_path_without_trailing_slash.len], file_path_without_trailing_slash); + + _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..][0..changed_name.len], changed_name); + const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; + file_hash = FSWatcher.Watcher.getHash(path_slice); + + const relative = fs.relative(this.entry_dir, path_slice); + + break :brk relative; + }; + + // skip consecutive duplicates + const event_type: FSWatchTask.EventType = .rename; // renaming folders, creating folder or files will be always be rename + if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) { + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; + + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + current_task.append(relative_path, event_type, .destroy); + + if (this.verbose) + Output.prettyErrorln(" Dir change: {s}", .{relative_path}); + } + } + + if (this.verbose and affected.len == 0) { + Output.prettyErrorln(" Dir change: {s}", .{fs.relative(this.entry_dir, file_path)}); + } + }, + } + } + } + + pub const Arguments = struct { + path: PathLike, + listener: JSC.JSValue, + global_this: JSC.C.JSContextRef, + signal: ?*JSC.AbortSignal, + persistent: bool, + recursive: bool, + encoding: JSC.Node.Encoding, + verbose: bool, + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Arguments { + const vm = ctx.vm(); + const path = PathLike.fromJS(ctx, arguments, exception) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "filename must be a string or TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + + if (exception.* != null) return null; + var listener: JSC.JSValue = .zero; + var signal: ?*JSC.AbortSignal = null; + var persistent: bool = true; + var recursive: bool = false; + var encoding: JSC.Node.Encoding = .utf8; + var verbose = false; + if (arguments.nextEat()) |options_or_callable| { + + // options + if (options_or_callable.isObject()) { + if (options_or_callable.get(ctx, "persistent")) |persistent_| { + if (!persistent_.isBoolean()) { + JSC.throwInvalidArguments( + "persistent must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + persistent = persistent_.toBoolean(); + } + + if (options_or_callable.get(ctx, "verbose")) |verbose_| { + if (!verbose_.isBoolean()) { + JSC.throwInvalidArguments( + "verbose must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + verbose = verbose_.toBoolean(); + } + + if (options_or_callable.get(ctx, "encoding")) |encoding_| { + if (!encoding_.isString()) { + JSC.throwInvalidArguments( + "encoding must be a string.", + .{}, + ctx, + exception, + ); + return null; + } + if (JSC.Node.Encoding.fromJS(encoding_, ctx.ptr())) |node_encoding| { + encoding = node_encoding; + } else { + JSC.throwInvalidArguments( + "invalid encoding.", + .{}, + ctx, + exception, + ); + return null; + } + } + + if (options_or_callable.get(ctx, "recursive")) |recursive_| { + if (!recursive_.isBoolean()) { + JSC.throwInvalidArguments( + "recursive must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + recursive = recursive_.toBoolean(); + } + + // abort signal + if (options_or_callable.get(ctx, "signal")) |signal_| { + if (JSC.AbortSignal.fromJS(signal_)) |signal_obj| { + //Keep it alive + signal_.ensureStillAlive(); + signal = signal_obj; + } else { + JSC.throwInvalidArguments( + "signal is not of type AbortSignal.", + .{}, + ctx, + exception, + ); + + return null; + } + } + + // listener + if (arguments.nextEat()) |callable| { + if (!callable.isCell() or !callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = callable; + } + } else { + if (!options_or_callable.isCell() or !options_or_callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = options_or_callable; + } + } + if (listener == .zero) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback", .{}, ctx).asObjectRef(); + return null; + } + + return Arguments{ + .path = path, + .listener = listener, + .global_this = ctx, + .signal = signal, + .persistent = persistent, + .recursive = recursive, + .encoding = encoding, + .verbose = verbose, + }; + } + + pub fn createFSWatcher(this: Arguments) !JSC.JSValue { + const obj = try FSWatcher.init(this); + return obj.toJS(); + } + }; + + pub const JSObject = struct { + signal: ?*JSC.AbortSignal, + persistent: bool, + manager: ?*FSWatcher.Watcher, + fsevents_watcher: ?*FSEvents.FSEventsWatcher, + poll_ref: JSC.PollRef = .{}, + globalThis: ?*JSC.JSGlobalObject, + js_this: JSC.JSValue, + encoding: JSC.Node.Encoding, + closed: bool, + + pub usingnamespace JSC.Codegen.JSFSWatcher; + + pub fn getFSWatcher(this: *JSObject) *FSWatcher { + if (this.manager) |manager| return manager.ctx; + if (this.fsevents_watcher) |manager| return bun.cast(*FSWatcher, manager.ctx.?); + + @panic("No context attached to JSFSWatcher"); + } + + pub fn init(globalThis: *JSC.JSGlobalObject, manager: ?*FSWatcher.Watcher, fsevents_watcher: ?*FSEvents.FSEventsWatcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { + var obj = try globalThis.allocator().create(JSObject); + obj.* = .{ + .signal = null, + .persistent = persistent, + .manager = manager, + .fsevents_watcher = fsevents_watcher, + .globalThis = globalThis, + .js_this = .zero, + .encoding = encoding, + .closed = false, + }; + const instance = obj.getFSWatcher(); + + if (persistent) { + obj.poll_ref.ref(instance.ctx); + } + + var js_this = JSObject.toJS(obj, globalThis); + JSObject.listenerSetCached(js_this, globalThis, listener); + obj.js_this = js_this; + obj.js_this.protect(); + + if (signal) |s| { + + // already aborted? + if (s.aborted()) { + obj.signal = s.ref(); + // abort next tick + var current_task: FSWatchTask = .{ + .ctx = instance, + }; + current_task.append("", .abort, .none); + current_task.enqueue(); + } else { + // watch for abortion + obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); + } + } + return obj; + } + + pub fn emitIfAborted(this: *JSObject) void { + if (this.signal) |s| { + if (s.aborted()) { + const err = s.abortReason(); + this.emitAbort(err); + } + } + } + + pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void { + if (this.closed) return; + defer this.close(true); + + err.ensureStillAlive(); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, globalThis) else err, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + pub fn emitError(this: *JSObject, err: string) void { + if (this.closed) return; + defer this.close(true); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + JSC.ZigString.fromUTF8(err).toErrorInstance(globalThis), + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn emit(this: *JSObject, file_name: string, comptime eventType: string) void { + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var filename: JSC.JSValue = JSC.JSValue.jsUndefined(); + if (file_name.len > 0) { + if (this.encoding == .buffer) + filename = JSC.ArrayBuffer.createBuffer(globalThis, file_name) + else if (this.encoding == .utf8) { + filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis); + } else { + // convert to desired encoding + filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding); + } + } + var args = [_]JSC.JSValue{ + JSC.ZigString.static(eventType).toValue(globalThis), + filename, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn ref(this: *JSObject) void { + if (this.closed) return; + + if (!this.persistent) { + this.persistent = true; + this.poll_ref.ref(this.getFSWatcher().ctx); + } + } + + pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.ref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn unref(this: *JSObject) void { + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.getFSWatcher().ctx); + } + } + + pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.unref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.persistent); + } + + pub fn close( + this: *JSObject, + emitEvent: bool, + ) void { + if (!this.closed) { + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + this.closed = true; + if (emitEvent) { + this.emit("", "close"); + } + + this.detach(); + } + } + + pub fn detach(this: *JSObject) void { + this.unref(); + + if (this.js_this != .zero) { + this.js_this.unprotect(); + this.js_this = .zero; + } + + this.globalThis = null; + + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + if (this.manager) |manager| { + var ctx = manager.ctx; + this.manager = null; + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(true); + } + + if (this.fsevents_watcher) |manager| { + var ctx = bun.cast(*FSWatcher, manager.ctx.?); + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(); + } + } + + pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.close(true); + return JSC.JSValue.jsUndefined(); + } + + pub fn finalize(this: *JSObject) callconv(.C) void { + if (!this.closed) { + this.detach(); + } + + bun.default_allocator.destroy(this); + } + }; + + const PathResult = struct { + fd: StoredFileDescriptorType = 0, + is_file: bool = true, + }; + + fn fdFromAbsolutePathZ( + absolute_path_z: [:0]const u8, + ) !PathResult { + var stat = try bun.C.lstat_absolute(absolute_path_z); + var result = PathResult{}; + + switch (stat.kind) { + .sym_link => { + var file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); + result.fd = file.handle; + const _stat = try file.stat(); + + result.is_file = _stat.kind == .directory; + }, + .directory => { + const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{ + .access_sub_paths = true, + })).dir; + result.fd = dir.fd; + result.is_file = false; + }, + else => { + const file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); + result.fd = file.handle; + result.is_file = true; + }, + } + return result; + } + + pub fn init(args: Arguments) !*FSWatcher { + var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + var slice = args.path.slice(); + if (bun.strings.startsWith(slice, "file://")) { + slice = slice[6..]; + } + var parts = [_]string{ + slice, + }; + + var file_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.top_level_dir, + &buf, + &parts, + .auto, + ); + + buf[file_path.len] = 0; + var file_path_z = buf[0..file_path.len :0]; + + var fs_type = try fdFromAbsolutePathZ(file_path_z); + + var ctx = try bun.default_allocator.create(FSWatcher); + const vm = args.global_this.bunVM(); + ctx.* = .{ + .ctx = vm, + .verbose = args.verbose, + .file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| { + ctx.deinit(); + return err; + }, + }; + + if (comptime Environment.isMac) { + if (!fs_type.is_file) { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; + + var fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, null, fsevents_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + fsevents_watcher.deinit(); + return err; + }; + + return ctx; + } + } + + var fs_watcher = FSWatcher.Watcher.init( + ctx, + vm.bundler.fs, + bun.default_allocator, + ) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.watcher_instance = fs_watcher; + + if (fs_type.is_file) { + var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + + ctx.entry_path = file_path_clone; + ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone; + + fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } else { + addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } + + fs_watcher.start() catch |err| { + ctx.deinit(); + + fs_watcher.deinit(true); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, null, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + return ctx; + } +}; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index e2de35706..659ac31bb 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -93,6 +93,10 @@ pub fn Maybe(comptime ResultType: type) type { return JSC.JSValue.jsUndefined(); } + if (comptime ReturnType == JSC.JSValue) { + return r; + } + if (comptime ReturnType == JSC.ArrayBuffer) { return r.toJS(globalThis, null); } diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 5c8221128..061a25eed 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -802,7 +802,20 @@ pub const Encoder = struct { // pub fn writeUTF16AsUTF8(utf16: [*]const u16, len: usize, to: [*]u8, to_len: usize) callconv(.C) i32 { // return @intCast(i32, strings.copyUTF16IntoUTF8(to[0..to_len], []const u16, utf16[0..len], true).written); // } - + pub fn toStringAtRuntime(input: [*]const u8, len: usize, globalObject: *JSGlobalObject, encoding: JSC.Node.Encoding) JSValue { + return switch (encoding) { + .ucs2 => toString(input, len, globalObject, .utf16le), + .utf16le => toString(input, len, globalObject, .utf16le), + .utf8 => toString(input, len, globalObject, .utf8), + .ascii => toString(input, len, globalObject, .ascii), + .hex => toString(input, len, globalObject, .hex), + .base64 => toString(input, len, globalObject, .base64), + .base64url => toString(input, len, globalObject, .base64url), + .latin1 => toString(input, len, globalObject, .latin1), + // treat everything else as utf8 + else => toString(input, len, globalObject, .utf8), + }; + } pub fn toString(input_ptr: [*]const u8, len: usize, global: *JSGlobalObject, comptime encoding: JSC.Node.Encoding) JSValue { if (len == 0) return ZigString.Empty.toValue(global); diff --git a/src/fs.zig b/src/fs.zig index e87d931df..98174fac3 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1109,6 +1109,60 @@ pub const FileSystem = struct { return File{ .path = Path.init(path), .contents = file_contents }; } + pub fn kindFromAbsolute( + fs: *RealFS, + absolute_path: [:0]const u8, + existing_fd: StoredFileDescriptorType, + store_fd: bool, + ) !Entry.Cache { + var outpath: [bun.MAX_PATH_BYTES]u8 = undefined; + + var stat = try C.lstat_absolute(absolute_path); + const is_symlink = stat.kind == std.fs.File.Kind.SymLink; + var _kind = stat.kind; + var cache = Entry.Cache{ + .kind = Entry.Kind.file, + .symlink = PathString.empty, + }; + var symlink: []const u8 = ""; + + if (is_symlink) { + var file = try if (existing_fd != 0) + std.fs.File{ .handle = existing_fd } + else if (store_fd) + std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only }) + else + bun.openFileForPath(absolute_path); + setMaxFd(file.handle); + + defer { + if ((!store_fd or fs.needToCloseFiles()) and existing_fd == 0) { + file.close(); + } else if (comptime FeatureFlags.store_file_descriptors) { + cache.fd = file.handle; + } + } + const _stat = try file.stat(); + + symlink = try bun.getFdPath(file.handle, &outpath); + + _kind = _stat.kind; + } + + std.debug.assert(_kind != .SymLink); + + if (_kind == .Directory) { + cache.kind = .dir; + } else { + cache.kind = .file; + } + if (symlink.len > 0) { + cache.symlink = PathString.init(try FilenameStore.instance.append([]const u8, symlink)); + } + + return cache; + } + pub fn kind( fs: *RealFS, _dir: string, diff --git a/src/http.zig b/src/http.zig index 827bfa6de..80718db2f 100644 --- a/src/http.zig +++ b/src/http.zig @@ -3238,7 +3238,12 @@ pub const Server = struct { threadlocal var filechange_buf: [32]u8 = undefined; threadlocal var filechange_buf_hinted: [32]u8 = undefined; - + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } pub fn onFileUpdate( ctx: *Server, events: []watcher.WatchEvent, diff --git a/src/js/node/fs.js b/src/js/node/fs.js index f117020dd..6b0e3954e 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -1,3 +1,5 @@ +import { EventEmitter } from "stream"; + // Hardcoded module "node:fs" var { direct, isPromise, isCallable } = import.meta.primordials; var promises = import.meta.require("node:fs/promises"); @@ -7,6 +9,63 @@ var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means var fs = Bun.fs(); var debug = process.env.DEBUG ? console.log : () => {}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + + if (typeof options === "function") { + listener = options; + options = {}; + } else if (typeof options === "string") { + options = { encoding: options }; + } + + if (typeof listener !== "function") { + listener = () => {}; + } + + this.#listener = listener; + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) { + throw e; + } + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + notFound.code = "ENOENT"; + notFound.errno = -2; + notFound.path = path; + notFound.syscall = "watch"; + notFound.filename = path; + throw notFound; + } + } + + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") { + this.emit(eventType, filenameOrError); + } else { + this.emit("change", eventType, filenameOrError); + this.#listener(eventType, filenameOrError); + } + } + + close() { + this.#watcher?.close(); + this.#watcher = null; + } + + ref() { + this.#watcher?.ref(); + } + + unref() { + this.#watcher?.unref(); + } +} export var access = function access(...args) { callbackify(fs.accessSync, args); }, @@ -153,6 +212,9 @@ export var access = function access(...args) { rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, + watch = function watch(path, options, listener) { + return new FSWatcher(path, options, listener); + }, promises = import.meta.require("node:fs/promises"); function callbackify(fsFunction, args) { @@ -1002,7 +1064,8 @@ export default { writeSync, WriteStream, ReadStream, - + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass, diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index de802928b..7df446ccb 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:fs/promises" + // Note: `constants` is injected into the top of this file declare var constants: typeof import("node:fs/promises").constants; @@ -38,6 +39,55 @@ var promisify = { }, }[notrace]; +export function watch( + filename: string | Buffer | URL, + options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {}, +) { + type Event = { + eventType: string; + filename: string | Buffer | undefined; + }; + const events: Array = []; + if (filename instanceof URL) { + throw new TypeError("Watch URLs are not supported yet"); + } else if (Buffer.isBuffer(filename)) { + filename = filename.toString(); + } else if (typeof filename !== "string") { + throw new TypeError("Expected path to be a string or Buffer"); + } + let nextEventResolve: Function | null = null; + if (typeof options === "string") { + options = { encoding: options }; + } + fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => { + events.push({ eventType, filename }); + if (nextEventResolve) { + const resolve = nextEventResolve; + nextEventResolve = null; + resolve(); + } + }); + return { + async *[Symbol.asyncIterator]() { + let closed = false; + while (!closed) { + while (events.length) { + let event = events.shift() as Event; + if (event.eventType === "close") { + closed = true; + break; + } + if (event.eventType === "error") { + closed = true; + throw event.filename; + } + yield event; + } + await new Promise((resolve: Function) => (nextEventResolve = resolve)); + } + }, + }; +} export var access = promisify(fs.accessSync), appendFile = promisify(fs.appendFileSync), close = promisify(fs.closeSync), @@ -112,6 +162,7 @@ export default { lutimes, rm, rmdir, + watch, constants, [Symbol.for("CommonJS")]: 0, }; diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js index cc1e14d2b..cc3763cfc 100644 --- a/src/js/out/modules/node/fs.js +++ b/src/js/out/modules/node/fs.js @@ -1,3 +1,4 @@ +var {EventEmitter } = import.meta.require("node:stream"); var callbackify = function(fsFunction, args) { try { const result = fsFunction.apply(fs, args.slice(0, args.length - 1)), callback = args[args.length - 1]; @@ -16,7 +17,47 @@ function createWriteStream(path, options) { return new WriteStream(path, options); } var { direct, isPromise, isCallable } = import.meta.primordials, promises = import.meta.require("node:fs/promises"), { Readable, NativeWritable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream"), NativeReadable = _getNativeReadableStreamPrototype(2, Readable), fs = Bun.fs(), debug = process.env.DEBUG ? console.log : () => { -}, access = function access2(...args) { +}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + if (typeof options === "function") + listener = options, options = {}; + else if (typeof options === "string") + options = { encoding: options }; + if (typeof listener !== "function") + listener = () => { + }; + this.#listener = listener; + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) + throw e; + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + throw notFound.code = "ENOENT", notFound.errno = -2, notFound.path = path, notFound.syscall = "watch", notFound.filename = path, notFound; + } + } + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") + this.emit(eventType, filenameOrError); + else + this.emit("change", eventType, filenameOrError), this.#listener(eventType, filenameOrError); + } + close() { + this.#watcher?.close(), this.#watcher = null; + } + ref() { + this.#watcher?.ref(); + } + unref() { + this.#watcher?.unref(); + } +} +var access = function access2(...args) { callbackify(fs.accessSync, args); }, appendFile = function appendFile2(...args) { callbackify(fs.appendFileSync, args); @@ -88,7 +129,9 @@ var { direct, isPromise, isCallable } = import.meta.primordials, promises = impo callbackify(fs.utimesSync, args); }, lutimes = function lutimes2(...args) { callbackify(fs.lutimesSync, args); -}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { +}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) { + return new FSWatcher(path, options, listener); +}, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { file: void 0, fd: void 0, flags: "r", @@ -590,6 +633,8 @@ var fs_default = { writeSync, WriteStream, ReadStream, + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass @@ -600,6 +645,7 @@ export { writeFileSync, writeFile, write, + watch, utimesSync, utimes, unlinkSync, diff --git a/src/js/out/modules/node/fs.promises.js b/src/js/out/modules/node/fs.promises.js index 2780ff166..ef3330771 100644 --- a/src/js/out/modules/node/fs.promises.js +++ b/src/js/out/modules/node/fs.promises.js @@ -1 +1 @@ -var D=Bun.fs(),B="::bunternal::",E={[B]:(S)=>{var b={[B]:function(C,J,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,J(A);return}C(z)}}[B];return async function(...C){return await new Promise((J,q)=>{process.nextTick(b,J,q,C)})}}}[B],G=E(D.accessSync),H=E(D.appendFileSync),I=E(D.closeSync),K=E(D.copyFileSync),L=E(D.existsSync),M=E(D.chownSync),N=E(D.chmodSync),O=E(D.fchmodSync),P=E(D.fchownSync),Q=E(D.fstatSync),R=E(D.fsyncSync),T=E(D.ftruncateSync),U=E(D.futimesSync),V=E(D.lchmodSync),W=E(D.lchownSync),X=E(D.linkSync),Y=E(D.lstatSync),Z=E(D.mkdirSync),_=E(D.mkdtempSync),$=E(D.openSync),x=E(D.readSync),j=E(D.writeSync),v=E(D.readdirSync),w=E(D.readFileSync),k=E(D.writeFileSync),F=E(D.readlinkSync),h=E(D.realpathSync),g=E(D.renameSync),u=E(D.statSync),d=E(D.symlinkSync),n=E(D.truncateSync),l=E(D.unlinkSync),a=E(D.utimesSync),c=E(D.lutimesSync),t=E(D.rmSync),y=E(D.rmdirSync),p={access:G,appendFile:H,close:I,copyFile:K,exists:L,chown:M,chmod:N,fchmod:O,fchown:P,fstat:Q,fsync:R,ftruncate:T,futimes:U,lchmod:V,lchown:W,link:X,lstat:Y,mkdir:Z,mkdtemp:_,open:$,read:x,write:j,readdir:v,readFile:w,writeFile:k,readlink:F,realpath:h,rename:g,stat:u,symlink:d,truncate:n,unlink:l,utimes:a,lutimes:c,rm:t,rmdir:y,constants,[Symbol.for("CommonJS")]:0};export{k as writeFile,j as write,a as utimes,l as unlink,n as truncate,d as symlink,u as stat,y as rmdir,t as rm,g as rename,h as realpath,F as readlink,v as readdir,w as readFile,x as read,$ as open,_ as mkdtemp,Z as mkdir,c as lutimes,Y as lstat,X as link,W as lchown,V as lchmod,U as futimes,T as ftruncate,R as fsync,Q as fstat,P as fchown,O as fchmod,L as exists,p as default,K as copyFile,I as close,M as chown,N as chmod,H as appendFile,G as access}; +function H(S,C={}){const J=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let b=null;if(typeof C==="string")C={encoding:C};return D.watch(S,C||{},(q,z)=>{if(J.push({eventType:q,filename:z}),b){const A=b;b=null,A()}}),{async*[Symbol.asyncIterator](){let q=!1;while(!q){while(J.length){let z=J.shift();if(z.eventType==="close"){q=!0;break}if(z.eventType==="error")throw q=!0,z.filename;yield z}await new Promise((z)=>b=z)}}}}var D=Bun.fs(),B="::bunternal::",G={[B]:(S)=>{var C={[B]:function(J,b,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,b(A);return}J(z)}}[B];return async function(...J){return await new Promise((b,q)=>{process.nextTick(C,b,q,J)})}}}[B],I=G(D.accessSync),K=G(D.appendFileSync),L=G(D.closeSync),M=G(D.copyFileSync),N=G(D.existsSync),O=G(D.chownSync),P=G(D.chmodSync),Q=G(D.fchmodSync),U=G(D.fchownSync),V=G(D.fstatSync),W=G(D.fsyncSync),X=G(D.ftruncateSync),Y=G(D.futimesSync),Z=G(D.lchmodSync),_=G(D.lchownSync),$=G(D.linkSync),T=G(D.lstatSync),E=G(D.mkdirSync),j=G(D.mkdtempSync),R=G(D.openSync),k=G(D.readSync),x=G(D.writeSync),F=G(D.readdirSync),u=G(D.readFileSync),w=G(D.writeFileSync),g=G(D.readlinkSync),h=G(D.realpathSync),d=G(D.renameSync),c=G(D.statSync),v=G(D.symlinkSync),a=G(D.truncateSync),y=G(D.unlinkSync),l=G(D.utimesSync),t=G(D.lutimesSync),p=G(D.rmSync),n=G(D.rmdirSync),m={access:I,appendFile:K,close:L,copyFile:M,exists:N,chown:O,chmod:P,fchmod:Q,fchown:U,fstat:V,fsync:W,ftruncate:X,futimes:Y,lchmod:Z,lchown:_,link:$,lstat:T,mkdir:E,mkdtemp:j,open:R,read:k,write:x,readdir:F,readFile:u,writeFile:w,readlink:g,realpath:h,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:t,rm:p,rmdir:n,watch:H,constants,[Symbol.for("CommonJS")]:0};export{w as writeFile,x as write,H as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,p as rm,d as rename,h as realpath,g as readlink,F as readdir,u as readFile,k as read,R as open,j as mkdtemp,E as mkdir,t as lutimes,T as lstat,$ as link,_ as lchown,Z as lchmod,Y as futimes,X as ftruncate,W as fsync,V as fstat,U as fchown,Q as fchmod,N as exists,m as default,M as copyFile,L as close,O as chown,P as chmod,K as appendFile,I as access}; diff --git a/src/js/private.d.ts b/src/js/private.d.ts index b6ed64801..b689c208e 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -6,11 +6,95 @@ */ declare function $bundleError(error: string); +type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal }; + +type BunWatchEventType = "rename" | "change" | "error" | "close"; +type BunWatchListener = (event: WatchEventType, filename: T | Error | undefined) => void; + +interface BunFSWatcher { + /** + * Stop watching for changes on the given `BunFSWatcher`. Once stopped, the `BunFSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; +} +type BunFS = Omit & { + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'`, `'change', 'error' or 'close'`, and `filename` is the name of the file + * which triggered the event, the error when `eventType` is 'error' or undefined when eventType is 'close'. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `BunFSWatcher`. + * @since v0.6.8 + * @param listener + */ + watch( + filename: string, + options: + | (WatchOptions & { + encoding: "buffer"; + }) + | "buffer", + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options?: WatchOptions | BufferEncoding | null, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options: BunWatchListener | string, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + watch(filename: string, listener?: BunWatchListener): BunFSWatcher; +}; + declare module "bun" { var TOML: { parse(contents: string): any; }; - function fs(): typeof import("node:fs"); + function fs(): BunFS; function _Os(): typeof import("node:os"); function jest(): typeof import("bun:test"); var main: string; diff --git a/src/jsc.zig b/src/jsc.zig index 67cf3f05c..ca31d5f1a 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -50,6 +50,7 @@ pub const FFI = @import("./bun.js/api/ffi.zig").FFI; pub const Node = struct { pub usingnamespace @import("./bun.js/node/types.zig"); pub usingnamespace @import("./bun.js/node/node_fs.zig"); + pub usingnamespace @import("./bun.js/node/node_fs_watcher.zig"); pub usingnamespace @import("./bun.js/node/node_fs_binding.zig"); pub usingnamespace @import("./bun.js/node/node_os.zig"); pub const Syscall = @import("./bun.js/node/syscall.zig"); diff --git a/src/watcher.zig b/src/watcher.zig index 155c0b473..044770dc4 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -108,6 +108,10 @@ pub const INotify = struct { std.os.inotify_rm_watch(inotify_fd, wd); } + pub fn isRunning() bool { + return loaded_inotify; + } + var coalesce_interval: isize = 100_000; pub fn init() !void { std.debug.assert(!loaded_inotify); @@ -229,6 +233,10 @@ const DarwinWatcher = struct { if (fd == 0) return error.KQueueError; } + pub fn isRunning() bool { + return fd != 0; + } + pub fn stop() void { if (fd != 0) { std.os.close(fd); @@ -361,6 +369,8 @@ pub fn NewWatcher(comptime ContextType: type) type { watchloop_handle: ?std.Thread.Id = null, cwd: string, thread: std.Thread = undefined, + running: bool = true, + close_descriptors: bool = false, pub const HashType = u32; @@ -372,7 +382,9 @@ pub fn NewWatcher(comptime ContextType: type) type { pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { var watcher = try allocator.create(Watcher); - try PlatformWatcher.init(); + if (!PlatformWatcher.isRunning()) { + try PlatformWatcher.init(); + } watcher.* = Watcher{ .fs = fs, @@ -393,6 +405,26 @@ pub fn NewWatcher(comptime ContextType: type) type { this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } + pub fn deinit(this: *Watcher, close_descriptors: bool) void { + this.mutex.lock(); + defer this.mutex.unlock(); + + this.close_descriptors = close_descriptors; + if (this.watchloop_handle != null) { + this.running = false; + } else { + if (this.close_descriptors and this.running) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); + const allocator = this.allocator; + allocator.destroy(this); + } + } + // This must only be called from the watcher thread pub fn watchLoop(this: *Watcher) !void { this.watchloop_handle = std.Thread.getCurrentId(); @@ -402,12 +434,24 @@ pub fn NewWatcher(comptime ContextType: type) type { if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); this._watchLoop() catch |err| { - Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); - this.watchloop_handle = null; PlatformWatcher.stop(); - return; + if (this.running) { + this.ctx.onError(err); + } }; + + // deinit and close descriptors if needed + if (this.close_descriptors) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); + + const allocator = this.allocator; + allocator.destroy(this); } var evict_list_i: WatchItemIndex = 0; @@ -475,7 +519,7 @@ pub fn NewWatcher(comptime ContextType: type) type { var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); var changelist = &changelist_array; - while (true) { + while (this.running) { defer Output.flush(); var count_ = std.os.system.kevent( @@ -530,11 +574,12 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + if (this.running) { + this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + } } } else if (Environment.isLinux) { - restart: while (true) { + restart: while (this.running) { defer Output.flush(); var events = try INotify.read(); @@ -600,9 +645,10 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); - remaining_events -= slice.len; + if (this.running) { + this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); + remaining_events -= slice.len; + } } } } diff --git a/test/js/node/watch/fixtures/close.js b/test/js/node/watch/fixtures/close.js new file mode 100644 index 000000000..8eeeb79a3 --- /dev/null +++ b/test/js/node/watch/fixtures/close.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .close(); diff --git a/test/js/node/watch/fixtures/persistent.js b/test/js/node/watch/fixtures/persistent.js new file mode 100644 index 000000000..72a2b6564 --- /dev/null +++ b/test/js/node/watch/fixtures/persistent.js @@ -0,0 +1,5 @@ +import fs from "fs"; +fs.watch(import.meta.path, { persistent: false, signal: AbortSignal.timeout(4000) }).on("error", err => { + console.error(err.message); + process.exit(1); +}); diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js new file mode 100644 index 000000000..26e09da1a --- /dev/null +++ b/test/js/node/watch/fixtures/relative.js @@ -0,0 +1,23 @@ +import fs from "fs"; +const watcher = fs.watch("relative.txt", { signal: AbortSignal.timeout(2000) }); + +watcher.on("change", function (event, filename) { + if (filename !== "relative.txt" && event !== "change") { + console.error("fail"); + clearInterval(interval); + watcher.close(); + process.exit(1); + } else { + clearInterval(interval); + watcher.close(); + } +}); +watcher.on("error", err => { + clearInterval(interval); + console.error(err.message); + process.exit(1); +}); + +const interval = setInterval(() => { + fs.writeFileSync("relative.txt", "world"); +}, 10); diff --git a/test/js/node/watch/fixtures/unref.js b/test/js/node/watch/fixtures/unref.js new file mode 100644 index 000000000..a0c506a04 --- /dev/null +++ b/test/js/node/watch/fixtures/unref.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .unref(); diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js new file mode 100644 index 000000000..56e1798f1 --- /dev/null +++ b/test/js/node/watch/fs.watch.test.js @@ -0,0 +1,424 @@ +import fs from "fs"; +import path from "path"; +import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; +import { pathToFileURL } from "bun"; + +import { describe, expect, test } from "bun:test"; +// Because macOS (and possibly other operating systems) can return a watcher +// before it is actually watching, we need to repeat the operation to avoid +// a race condition. +function repeat(fn) { + const interval = setInterval(fn, 20); + return interval; +} +const encodingFileName = `新建文夹件.txt`; +const testDir = tempDirWithFiles("watch", { + "watch.txt": "hello", + "relative.txt": "hello", + "abort.txt": "hello", + "url.txt": "hello", + [encodingFileName]: "hello", +}); + +describe("fs.watch", () => { + test("non-persistent watcher should not block the event loop", done => { + try { + // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("watcher should close and not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("unref watcher should not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("should work with relative files", done => { + try { + bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("add file/folder to folder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + let err = undefined; + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + if (count >= 2) { + watcher.close(); + } + } catch (e) { + err = e; + watcher.close(); + } + }); + + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + }); + + test("add file/folder to subfolder", done => { + let count = 0; + const root = path.join(testDir, "add-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + let err = undefined; + watcher.on("change", (event, filename) => { + const basename = path.basename(filename); + if (basename === "subfolder") return; + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + if (count >= 2) { + watcher.close(); + } + } catch (e) { + err = e; + watcher.close(); + } + }); + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + }); + + test("should emit event when file is deleted", done => { + const testsubdir = tempDirWithFiles("subdir", { + "deleted.txt": "hello", + }); + const filepath = path.join(testsubdir, "deleted.txt"); + let err = undefined; + const watcher = fs.watch(testsubdir, function (event, filename) { + try { + expect(event).toBe("rename"); + expect(filename).toBe("deleted.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.rmSync(filepath, { force: true }); + const fd = fs.openSync(filepath, "w"); + fs.closeSync(fd); + }); + }); + + test("should emit 'change' event when file is modified", done => { + const filepath = path.join(testDir, "watch.txt"); + + const watcher = fs.watch(filepath); + let err = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + }); + + test("should error on invalid path", done => { + try { + fs.watch(path.join(testDir, "404.txt")); + done(new Error("should not reach here")); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ENOENT"); + expect(err.syscall).toBe("watch"); + done(); + } + }); + + const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2", "latin1", "binary"]; + + test(`should work with encodings ${encodings.join(", ")}`, async () => { + const watchers = []; + const filepath = path.join(testDir, encodingFileName); + + const promises = []; + encodings.forEach(name => { + const encoded_filename = + name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); + + promises.push( + new Promise((resolve, reject) => { + watchers.push( + fs.watch(filepath, { encoding: name }, (event, filename) => { + try { + expect(event).toBe("change"); + + if (name !== "buffer") { + expect(filename).toBe(encoded_filename); + } else { + expect(filename).toBeInstanceOf(Buffer); + expect(filename.toString("utf8")).toBe(encodingFileName); + } + + resolve(); + } catch (e) { + reject(e); + } + }), + ); + }), + ); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + + try { + await Promise.all(promises); + } finally { + clearInterval(interval); + watchers.forEach(watcher => watcher.close()); + } + }); + + test("should work with url", done => { + const filepath = path.join(testDir, "url.txt"); + try { + const watcher = fs.watch(pathToFileURL(filepath)); + let err = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("url.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + } catch (e) { + done(e); + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const promise = new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal: ac.signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + await new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + }); +}); + +describe("fs.promises.watchFile", () => { + test("add file/folder to folder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-directory"); + try { + fs.mkdirSync(root); + } catch {} + let success = false; + let err = undefined; + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + + for await (const event of watcher) { + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(event.filename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); + + test("add file/folder to subfolder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + let success = false; + let err = undefined; + + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + for await (const event of watcher) { + const basename = path.basename(event.filename); + if (basename === "subfolder") continue; + + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const watcher = fs.promises.watch(filepath, { signal: ac.signal }); + + const promise = (async () => { + try { + for await (const _ of watcher); + } catch (e) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + const watcher = fs.promises.watch(filepath, { signal }); + await (async () => { + try { + for await (const _ of watcher); + } catch (e) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + }); +}); -- cgit v1.2.3 From fdfbb18531828fc5dec329d5d9e5c828a3c83921 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jun 2023 16:32:27 -0700 Subject: Support reading embedded files in compiled executables (#3405) * Support reading embedded files in compiled executables * :nail_care: --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/bun.zig | 3 +++ src/bun.js/api/server.zig | 22 +++++++++------------- src/bun.js/javascript.zig | 5 ++++- src/bun.js/node/node_fs.zig | 30 ++++++++++++++++++++++++++++++ src/bun.js/webcore/blob.zig | 36 ++++++++++++++++++++++++++++++++++-- src/cli/build_command.zig | 1 + src/standalone_bun.zig | 44 +++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 124 insertions(+), 17 deletions(-) (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 034aaa81f..2e6381c74 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -896,6 +896,9 @@ pub fn createNodeFS( ) js.JSValueRef { var module = ctx.allocator().create(JSC.Node.NodeJSFS) catch unreachable; module.* = .{}; + var vm = ctx.bunVM(); + if (vm.standalone_module_graph != null) + module.node_fs.vm = vm; return module.toJS(ctx).asObjectRef(); } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index a56ff971f..ebfacdcc9 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2744,19 +2744,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // 1. Bun.file("foo") // 2. The content-disposition header is not present if (!has_content_disposition and content_type.category.autosetFilename()) { - if (this.blob.store()) |store| { - if (store.data == .file) { - if (store.data.file.pathlike == .path) { - const basename = std.fs.path.basename(store.data.file.pathlike.path.slice()); - if (basename.len > 0) { - var filename_buf: [1024]u8 = undefined; - - resp.writeHeader( - "content-disposition", - std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", - ); - } - } + if (this.blob.getFileName()) |filename| { + const basename = std.fs.path.basename(filename); + if (basename.len > 0) { + var filename_buf: [1024]u8 = undefined; + + resp.writeHeader( + "content-disposition", + std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", + ); } } } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 3baa25e22..cb1a50f1d 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -593,7 +593,10 @@ pub const VirtualMachine = struct { pub inline fn nodeFS(this: *VirtualMachine) *Node.NodeFS { return this.node_fs orelse brk: { this.node_fs = bun.default_allocator.create(Node.NodeFS) catch unreachable; - this.node_fs.?.* = Node.NodeFS{}; + this.node_fs.?.* = Node.NodeFS{ + // only used when standalone module graph is enabled + .vm = if (this.standalone_module_graph != null) this else null, + }; break :brk this.node_fs.?; }; } diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 21a65251a..35c616a89 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -2492,6 +2492,7 @@ pub const NodeFS = struct { /// That means a stack-allocated buffer won't suffice. Instead, we re-use /// the heap allocated buffer on the NodefS struct sync_error_buf: [bun.MAX_PATH_BYTES]u8 = undefined, + vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; @@ -3442,6 +3443,35 @@ pub const NodeFS = struct { const fd = switch (args.path) { .path => brk: { path = args.path.path.sliceZ(&this.sync_error_buf); + if (this.vm) |vm| { + if (vm.standalone_module_graph) |graph| { + if (graph.find(path)) |file| { + if (args.encoding == .buffer) { + return .{ + .result = .{ + .buffer = Buffer.fromBytes( + bun.default_allocator.dupe(u8, file.contents) catch @panic("out of memory"), + bun.default_allocator, + .Uint8Array, + ), + }, + }; + } else if (comptime string_type == .default) + .{ + .result = .{ + .string = bun.default_allocator.dupe(u8, file.contents) catch @panic("out of memory"), + }, + } + else + .{ + .result = .{ + .null_terminated = bun.default_allocator.dupeZ(u8, file.contents) catch @panic("out of memory"), + }, + }; + } + } + } + break :brk switch (Syscall.open( path, os.O.RDONLY | os.O.NOCTTY, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 1e63ea3a2..868acbb80 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -952,6 +952,13 @@ pub const Blob = struct { switch (path_) { .path => { const slice = path_.path.slice(); + + if (vm.standalone_module_graph) |graph| { + if (graph.find(slice)) |file| { + return file.blob(globalThis).dupe(); + } + } + var cloned = (allocator.dupeZ(u8, slice) catch unreachable)[0..slice.len]; break :brk .{ @@ -2195,6 +2202,9 @@ pub const Blob = struct { cap: SizeType = 0, allocator: std.mem.Allocator, + /// Used by standalone module graph + stored_name: bun.PathString = bun.PathString.empty, + pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { return .{ .ptr = bytes.ptr, @@ -2528,17 +2538,31 @@ pub const Blob = struct { this: *Blob, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSValue { + if (this.getFileName()) |path| { + var str = bun.String.create(path); + return str.toJS(globalThis); + } + + return JSValue.undefined; + } + + pub fn getFileName( + this: *const Blob, + ) ?[]const u8 { if (this.store) |store| { if (store.data == .file) { if (store.data.file.pathlike == .path) { - return ZigString.fromUTF8(store.data.file.pathlike.path.slice()).toValueGC(globalThis); + return store.data.file.pathlike.path.slice(); } // we shouldn't return Number here. + } else if (store.data == .bytes) { + if (store.data.bytes.stored_name.slice().len > 0) + return store.data.bytes.stored_name.slice(); } } - return JSC.JSValue.jsUndefined(); + return null; } // TODO: Move this to a separate `File` object or BunFile @@ -3469,6 +3493,14 @@ pub const AnyBlob = union(enum) { InternalBlob: InternalBlob, WTFStringImpl: bun.WTF.StringImpl, + pub fn getFileName(this: *const AnyBlob) ?[]const u8 { + return switch (this.*) { + .Blob => this.Blob.getFileName(), + .WTFStringImpl => null, + .InternalBlob => null, + }; + } + pub inline fn fastSize(this: *const AnyBlob) Blob.SizeType { return switch (this.*) { .Blob => this.Blob.size, diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 44e512996..ef99f7765 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -107,6 +107,7 @@ pub const BuildCommand = struct { // We never want to hit the filesystem for these files // This "compiled" protocol is specially handled by the module resolver. this_bundler.options.public_path = "compiled://root/"; + this_bundler.resolver.opts.public_path = "compiled://root/"; if (outfile.len == 0) { outfile = std.fs.path.basename(this_bundler.options.entry_points[0]); diff --git a/src/standalone_bun.zig b/src/standalone_bun.zig index e7363fb58..b18fe384e 100644 --- a/src/standalone_bun.zig +++ b/src/standalone_bun.zig @@ -18,6 +18,14 @@ pub const StandaloneModuleGraph = struct { return &this.files.values()[this.entry_point_id]; } + pub fn find(this: *const StandaloneModuleGraph, name: []const u8) ?*File { + if (!bun.strings.hasPrefixComptime(name, "compiled://root/")) { + return null; + } + + return this.files.getPtr(name); + } + pub const CompiledModuleGraphFile = struct { name: Schema.StringPointer = .{}, loader: bun.options.Loader = .file, @@ -30,6 +38,32 @@ pub const StandaloneModuleGraph = struct { loader: bun.options.Loader, contents: []const u8 = "", sourcemap: LazySourceMap, + blob_: ?*bun.JSC.WebCore.Blob = null, + + pub fn blob(this: *File, globalObject: *bun.JSC.JSGlobalObject) *bun.JSC.WebCore.Blob { + if (this.blob_ == null) { + var store = bun.JSC.WebCore.Blob.Store.init(@constCast(this.contents), bun.default_allocator) catch @panic("out of memory"); + // make it never free + store.ref(); + + var blob_ = bun.default_allocator.create(bun.JSC.WebCore.Blob) catch @panic("out of memory"); + blob_.* = bun.JSC.WebCore.Blob.initWithStore(store, globalObject); + blob_.allocator = bun.default_allocator; + + if (bun.HTTP.MimeType.byExtensionNoDefault(bun.strings.trimLeadingChar(std.fs.path.extension(this.name), '.'))) |mime| { + store.mime_type = mime; + blob_.content_type = mime.value; + blob_.content_type_was_set = true; + blob_.content_type_allocated = false; + } + + store.data.bytes.stored_name = bun.PathString.init(this.name); + + this.blob_ = blob_; + } + + return this.blob_.?; + } }; pub const LazySourceMap = union(enum) { @@ -152,8 +186,16 @@ pub const StandaloneModuleGraph = struct { continue; } + var dest_path = output_file.dest_path; + if (bun.strings.hasPrefixComptime(dest_path, "./")) { + dest_path = dest_path[2..]; + } + var module = CompiledModuleGraphFile{ - .name = string_builder.fmtAppendCount("{s}{s}", .{ prefix, output_file.dest_path }), + .name = string_builder.fmtAppendCount("{s}{s}", .{ + prefix, + dest_path, + }), .loader = output_file.loader, .contents = string_builder.appendCount(output_file.value.buffer.bytes), }; -- cgit v1.2.3 From 3d5573921e732c4a63794e5d25f5953d0a40ff0e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jun 2023 13:53:09 -0700 Subject: Error.prototype.stack gets sourcemapped stacktraces and introduce Error.appendStackTrace (#3441) * Fix potential crash when reading sourcemapped stack traces * Format & sourcemap Error.prototype.stack * prevent double sourcemapping * Introduce Error.appendStackTrace * Fix source url * hide private stack traces in non-debug builds * fixes #3443 * Bump WebKit * Fix test failure in vm.test * Support new() & add test --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- .github/workflows/bun-linux-aarch64.yml | 2 +- .github/workflows/bun-linux-build.yml | 4 +- .github/workflows/bun-mac-aarch64.yml | 16 +-- .github/workflows/bun-mac-x64-baseline.yml | 16 +-- .github/workflows/bun-mac-x64.yml | 16 +-- Dockerfile | 2 +- bun.lockb | Bin 72925 -> 72925 bytes package.json | 2 +- src/bun.js/bindings/ZigGlobalObject.cpp | 188 +++++++++++++++++++++++++--- src/bun.js/bindings/bindings.cpp | 28 +++-- src/bun.js/javascript.zig | 22 ++-- test/js/node/v8/capture-stack-trace.test.js | 12 ++ 12 files changed, 241 insertions(+), 67 deletions(-) (limited to 'src/bun.js/javascript.zig') diff --git a/.github/workflows/bun-linux-aarch64.yml b/.github/workflows/bun-linux-aarch64.yml index 9bee835b7..01714460b 100644 --- a/.github/workflows/bun-linux-aarch64.yml +++ b/.github/workflows/bun-linux-aarch64.yml @@ -36,7 +36,7 @@ jobs: arch: aarch64 build_arch: arm64 runner: linux-arm64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-linux-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-arm64-lto.tar.gz" webkit_basename: "bun-webkit-linux-arm64-lto" build_machine_arch: aarch64 diff --git a/.github/workflows/bun-linux-build.yml b/.github/workflows/bun-linux-build.yml index 8689d488e..798ffaf33 100644 --- a/.github/workflows/bun-linux-build.yml +++ b/.github/workflows/bun-linux-build.yml @@ -46,7 +46,7 @@ jobs: arch: x86_64 build_arch: amd64 runner: big-ubuntu - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-linux-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-amd64-lto.tar.gz" webkit_basename: "bun-webkit-linux-amd64-lto" build_machine_arch: x86_64 - cpu: nehalem @@ -54,7 +54,7 @@ jobs: arch: x86_64 build_arch: amd64 runner: big-ubuntu - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-linux-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-linux-amd64-lto.tar.gz" webkit_basename: "bun-webkit-linux-amd64-lto" build_machine_arch: x86_64 diff --git a/.github/workflows/bun-mac-aarch64.yml b/.github/workflows/bun-mac-aarch64.yml index 3040eee43..f281783bd 100644 --- a/.github/workflows/bun-mac-aarch64.yml +++ b/.github/workflows/bun-mac-aarch64.yml @@ -117,7 +117,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false # - cpu: haswell @@ -126,7 +126,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false # - cpu: nehalem @@ -135,7 +135,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true # - cpu: haswell @@ -144,7 +144,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true - cpu: native @@ -152,7 +152,7 @@ jobs: tag: bun-darwin-aarch64 obj: bun-obj-darwin-aarch64 artifact: bun-obj-darwin-aarch64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" runner: macos-arm64 dependencies: true compile_obj: true @@ -257,7 +257,7 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: haswell # arch: x86_64 # tag: bun-darwin-x64 @@ -265,14 +265,14 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" - cpu: native arch: aarch64 tag: bun-darwin-aarch64 obj: bun-obj-darwin-aarch64 package: bun-darwin-aarch64 artifact: bun-obj-darwin-aarch64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-arm64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/bun-mac-x64-baseline.yml b/.github/workflows/bun-mac-x64-baseline.yml index dea617b21..b5f79a757 100644 --- a/.github/workflows/bun-mac-x64-baseline.yml +++ b/.github/workflows/bun-mac-x64-baseline.yml @@ -117,7 +117,7 @@ jobs: obj: bun-obj-darwin-x64-baseline runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" dependencies: true compile_obj: false # - cpu: haswell @@ -126,7 +126,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false - cpu: nehalem @@ -135,7 +135,7 @@ jobs: obj: bun-obj-darwin-x64-baseline runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" dependencies: false compile_obj: true # - cpu: haswell @@ -144,7 +144,7 @@ jobs: # obj: bun-obj-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true # - cpu: native @@ -152,7 +152,7 @@ jobs: # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # runner: macos-arm64 # dependencies: true # compile_obj: true @@ -258,7 +258,7 @@ jobs: package: bun-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64-baseline - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: haswell # arch: x86_64 # tag: bun-darwin-x64 @@ -266,14 +266,14 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: native # arch: aarch64 # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # package: bun-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/bun-mac-x64.yml b/.github/workflows/bun-mac-x64.yml index 2f2143ab1..8b30321f0 100644 --- a/.github/workflows/bun-mac-x64.yml +++ b/.github/workflows/bun-mac-x64.yml @@ -117,7 +117,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: true # compile_obj: false - cpu: haswell @@ -126,7 +126,7 @@ jobs: obj: bun-obj-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" dependencies: true compile_obj: false # - cpu: nehalem @@ -135,7 +135,7 @@ jobs: # obj: bun-obj-darwin-x64-baseline # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # dependencies: false # compile_obj: true - cpu: haswell @@ -144,7 +144,7 @@ jobs: obj: bun-obj-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" dependencies: false compile_obj: true # - cpu: native @@ -152,7 +152,7 @@ jobs: # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-arm64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" # runner: macos-arm64 # dependencies: true # compile_obj: true @@ -260,7 +260,7 @@ jobs: # package: bun-darwin-x64 # runner: macos-11 # artifact: bun-obj-darwin-x64-baseline - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" - cpu: haswell arch: x86_64 tag: bun-darwin-x64 @@ -268,14 +268,14 @@ jobs: package: bun-darwin-x64 runner: macos-11 artifact: bun-obj-darwin-x64 - webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-amd64-lto.tar.gz" + webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-amd64-lto.tar.gz" # - cpu: native # arch: aarch64 # tag: bun-darwin-aarch64 # obj: bun-obj-darwin-aarch64 # package: bun-darwin-aarch64 # artifact: bun-obj-darwin-aarch64 - # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-1/bun-webkit-macos-arm64-lto.tar.gz" + # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/may20-2/bun-webkit-macos-arm64-lto.tar.gz" # runner: macos-arm64 steps: - uses: actions/checkout@v3 diff --git a/Dockerfile b/Dockerfile index b4a76208f..f50ada2d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG ARCH=x86_64 ARG BUILD_MACHINE_ARCH=x86_64 ARG TRIPLET=${ARCH}-linux-gnu ARG BUILDARCH=amd64 -ARG WEBKIT_TAG=may20-1 +ARG WEBKIT_TAG=may20-2 ARG ZIG_TAG=jul1 ARG ZIG_VERSION="0.11.0-dev.3737+9eb008717" ARG WEBKIT_BASENAME="bun-webkit-linux-$BUILDARCH" diff --git a/bun.lockb b/bun.lockb index 2c2b03711..e53db751b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 94f9968ea..4fe213747 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@types/react": "^18.0.25", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", - "bun-webkit": "0.0.1-4b3750ddfe644a5bb131d652407653fac528ad51" + "bun-webkit": "0.0.1-8a03cf746abef8a48c932ab25f8821390632f2e2" }, "version": "0.0.0", "prettier": "./.prettierrc.cjs" diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index b27e0aafc..4b4edf097 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -219,7 +219,9 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c JSC::Options::useJITCage() = false; JSC::Options::useShadowRealm() = true; JSC::Options::useResizableArrayBuffer() = true; +#ifdef BUN_DEBUG JSC::Options::showPrivateScriptsInStackTraces() = true; +#endif JSC::Options::useSetMethods() = true; /* @@ -312,6 +314,123 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c } extern "C" void* Bun__getVM(); +extern "C" JSGlobalObject* Bun__getDefaultGlobal(); + +static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector& stackTrace, unsigned& line, unsigned& column, String& sourceURL, JSObject* errorInstance) +{ + if (!errorInstance) { + return String(); + } + + Zig::GlobalObject* globalObject = jsDynamicCast(errorInstance->globalObject()); + if (!globalObject) { + // Happens in node:vm + globalObject = jsDynamicCast(Bun__getDefaultGlobal()); + } + + WTF::String name = "Error"_s; + WTF::String message; + + if (errorInstance) { + // Note that we are not allowed to allocate memory in here. It's called inside a finalizer. + if (auto* instance = jsDynamicCast(errorInstance)) { + name = instance->sanitizedNameString(globalObject); + message = instance->sanitizedMessageString(globalObject); + } + } + + WTF::StringBuilder sb; + + if (!name.isEmpty()) { + sb.append(name); + sb.append(": "_s); + } + + if (!message.isEmpty()) { + sb.append(message); + } + + if (stackTrace.isEmpty()) { + return sb.toString(); + } + + if ((!message.isEmpty() || !name.isEmpty())) { + sb.append("\n"_s); + } + + size_t framesCount = stackTrace.size(); + ZigStackFrame remappedFrames[framesCount]; + bool hasSet = false; + for (size_t i = 0; i < framesCount; i++) { + StackFrame& frame = stackTrace.at(i); + + sb.append(" at "_s); + + WTF::String functionName = frame.functionName(vm); + + if (auto codeblock = frame.codeBlock()) { + if (codeblock->isConstructor()) { + sb.append("new "_s); + } + + // TODO: async + } + + if (functionName.isEmpty()) { + sb.append(""_s); + } else { + sb.append(functionName); + } + + sb.append(" ("_s); + + if (frame.hasLineAndColumnInfo()) { + unsigned int thisLine = 0; + unsigned int thisColumn = 0; + frame.computeLineAndColumn(thisLine, thisColumn); + remappedFrames[i].position.line = thisLine; + remappedFrames[i].position.column_start = thisColumn; + remappedFrames[i].source_url = Zig::toZigString(frame.sourceURL(vm)); + + // This ensures the lifetime of the sourceURL is accounted for correctly + Bun__remapStackFramePositions(globalObject, remappedFrames + i, 1); + + if (!hasSet) { + hasSet = true; + line = thisLine; + column = thisColumn; + sourceURL = frame.sourceURL(vm); + + if (errorInstance) { + if (remappedFrames[i].remapped) { + errorInstance->putDirect(vm, Identifier::fromString(vm, "originalLine"_s), jsNumber(thisLine), 0); + errorInstance->putDirect(vm, Identifier::fromString(vm, "originalColumn"_s), jsNumber(thisColumn), 0); + } + } + } + + sb.append(frame.sourceURL(vm)); + sb.append(":"_s); + sb.append(remappedFrames[i].position.line); + sb.append(":"_s); + sb.append(remappedFrames[i].position.column_start); + } else { + sb.append("native"_s); + } + sb.append(")"_s); + + if (i != framesCount - 1) { + sb.append("\n"_s); + } + } + + return sb.toString(); +} + +static String computeErrorInfo(JSC::VM& vm, Vector& stackTrace, unsigned& line, unsigned& column, String& sourceURL, JSObject* errorInstance) +{ + return computeErrorInfoWithoutPrepareStackTrace(vm, stackTrace, line, column, sourceURL, errorInstance); +} extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObjectClass, int count, void* console_client) @@ -329,6 +448,9 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObje Zig::GlobalObject* globalObject = Zig::GlobalObject::create(vm, Zig::GlobalObject::createStructure(vm, JSC::JSGlobalObject::create(vm, JSC::JSGlobalObject::createStructure(vm, JSC::jsNull())), JSC::jsNull())); globalObject->setConsole(globalObject); globalObject->isThreadLocalDefaultGlobalObject = true; + globalObject->setStackTraceLimit(DEFAULT_ERROR_STACK_TRACE_LIMIT); // Node.js defaults to 10 + vm.setOnComputeErrorInfo(computeErrorInfo); + if (count > 0) { globalObject->installAPIGlobals(globalObjectClass, count, vm); } @@ -2585,7 +2707,32 @@ JSC::JSValue GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* le extern "C" EncodedJSValue JSPasswordObject__create(JSC::JSGlobalObject*, bool); -JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace); +JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::ErrorInstance* source = jsDynamicCast(callFrame->argument(0)); + JSC::ErrorInstance* destination = jsDynamicCast(callFrame->argument(1)); + + if (!source || !destination) { + throwTypeError(lexicalGlobalObject, scope, "First & second argument must be an Error object"_s); + return JSC::JSValue::encode(jsUndefined()); + } + + if (!destination->stackTrace()) { + destination->captureStackTrace(vm, globalObject, 1); + } + + if (source->stackTrace()) { + destination->stackTrace()->appendVector(*source->stackTrace()); + source->stackTrace()->clear(); + } + + return JSC::JSValue::encode(jsUndefined()); +} + JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); @@ -2600,18 +2747,15 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb JSC::JSObject* errorObject = objectArg.asCell()->getObject(); JSC::JSValue caller = callFrame->argument(1); + // We cannot use our ErrorInstance::captureStackTrace() fast path here unfortunately. + // We need to return these CallSite array objects which means we need to create them JSValue errorValue = lexicalGlobalObject->get(lexicalGlobalObject, vm.propertyNames->Error); auto* errorConstructor = jsDynamicCast(errorValue); - - size_t stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT; - if (JSC::JSValue stackTraceLimitProp = errorConstructor->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->stackTraceLimit)) { - if (stackTraceLimitProp.isNumber()) { - stackTraceLimit = std::min(std::max(static_cast(stackTraceLimitProp.toIntegerOrInfinity(lexicalGlobalObject)), 0ul), 2048ul); - if (stackTraceLimit == 0) { - stackTraceLimit = 2048; - } - } + size_t stackTraceLimit = globalObject->stackTraceLimit().value(); + if (stackTraceLimit == 0) { + stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT; } + JSCStackTrace stackTrace = JSCStackTrace::captureCurrentJSStackTrace(globalObject, callFrame, stackTraceLimit, caller); // Create an (uninitialized) array for our "call sites" @@ -2672,9 +2816,20 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb errorObject->deleteProperty(lexicalGlobalObject, vm.propertyNames->stack); } if (formattedStackTrace.isUndefinedOrNull()) { - errorObject->putDirect(vm, vm.propertyNames->stack, jsUndefined(), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - } else { - errorObject->putDirect(vm, vm.propertyNames->stack, formattedStackTrace, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + formattedStackTrace = JSC::jsUndefined(); + } + + errorObject->putDirect(vm, vm.propertyNames->stack, formattedStackTrace, 0); + + if (!(caller && caller.isObject())) { + if (auto* instance = jsDynamicCast(errorObject)) { + // we make a separate copy of the StackTrace unfortunately so that we + // can later console.log it without losing the info + // + // This is not good. We should remove this in the future as it strictly makes this function + // already slower than necessary. + instance->captureStackTrace(vm, globalObject, 1, false); + } } RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSValue {})); @@ -3118,11 +3273,8 @@ void GlobalObject::finishCreation(VM& vm) RELEASE_ASSERT(classInfo()); JSC::JSObject* errorConstructor = this->errorConstructor(); - errorConstructor->putDirectNativeFunctionWithoutTransition(vm, this, JSC::Identifier::fromString(vm, "captureStackTrace"_s), 2, errorConstructorFuncCaptureStackTrace, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); - - // JSC default is 100 - errorConstructor->putDirect(vm, vm.propertyNames->stackTraceLimit, jsNumber(DEFAULT_ERROR_STACK_TRACE_LIMIT), JSC::PropertyAttribute::DontEnum | 0); - + errorConstructor->putDirectNativeFunction(vm, this, JSC::Identifier::fromString(vm, "captureStackTrace"_s), 2, errorConstructorFuncCaptureStackTrace, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); + errorConstructor->putDirectNativeFunction(vm, this, JSC::Identifier::fromString(vm, "appendStackTrace"_s), 2, errorConstructorFuncAppendStackTrace, ImplementationVisibility::Private, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); JSC::JSValue console = this->get(this, JSC::Identifier::fromString(vm, "console"_s)); JSC::JSObject* consoleObject = console.getObject(); consoleObject->putDirectBuiltinFunction(vm, this, vm.propertyNames->asyncIteratorSymbol, consoleObjectAsyncIteratorCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 3f3d82dc4..68aef29dd 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3528,12 +3528,13 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, JSC::JSValue val) { JSC::JSObject* obj = JSC::jsDynamicCast(val); + JSC::VM& vm = global->vm(); bool getFromSourceURL = false; if (stackTrace != nullptr && stackTrace->size() > 0) { - populateStackTrace(global->vm(), *stackTrace, &except->stack); + populateStackTrace(vm, *stackTrace, &except->stack); } else if (err->stackTrace() != nullptr && err->stackTrace()->size() > 0) { - populateStackTrace(global->vm(), *err->stackTrace(), &except->stack); + populateStackTrace(vm, *err->stackTrace(), &except->stack); } else { getFromSourceURL = true; } @@ -3546,7 +3547,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } if (except->code == SYNTAX_ERROR_CODE) { except->message = Zig::toZigString(err->sanitizedMessageString(global)); - } else if (JSC::JSValue message = obj->getIfPropertyExists(global, global->vm().propertyNames->message)) { + } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { except->message = Zig::toZigString(message, global); @@ -3556,7 +3557,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->name = Zig::toZigString(err->sanitizedNameString(global)); except->runtime_type = err->runtimeTypeForCause(); - auto clientData = WebCore::clientData(global->vm()); + auto clientData = WebCore::clientData(vm); if (except->code != SYNTAX_ERROR_CODE) { if (JSC::JSValue syscall = obj->getIfPropertyExists(global, clientData->builtinNames().syscallPublicName())) { @@ -3571,7 +3572,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->path = Zig::toZigString(path, global); } - if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(global->vm(), "fd"_s))) { + if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(vm, "fd"_s))) { if (fd.isAnyInt()) { except->fd = fd.toInt32(global); } @@ -3583,17 +3584,17 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } if (getFromSourceURL) { - if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, global->vm().propertyNames->sourceURL)) { + if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, vm.propertyNames->sourceURL)) { except->stack.frames_ptr[0].source_url = Zig::toZigString(sourceURL, global); - if (JSC::JSValue column = obj->getIfPropertyExists(global, global->vm().propertyNames->column)) { + if (JSC::JSValue column = obj->getIfPropertyExists(global, vm.propertyNames->column)) { except->stack.frames_ptr[0].position.column_start = column.toInt32(global); } - if (JSC::JSValue line = obj->getIfPropertyExists(global, global->vm().propertyNames->line)) { + if (JSC::JSValue line = obj->getIfPropertyExists(global, vm.propertyNames->line)) { except->stack.frames_ptr[0].position.line = line.toInt32(global); - if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(global->vm(), "lineText"_s))) { + if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "lineText"_s))) { if (JSC::JSString* jsStr = lineText.toStringOrNull(global)) { auto str = jsStr->value(global); except->stack.source_lines_ptr[0] = Zig::toZigString(str); @@ -3603,7 +3604,9 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, } } } + except->stack.frames_len = 1; + except->stack.frames_ptr[0].remapped = obj->hasProperty(global, JSC::Identifier::fromString(vm, "originalLine"_s)); } } @@ -3654,7 +3657,12 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSValue line = obj->getIfPropertyExists(global, global->vm().propertyNames->line)) { if (line) { - except->stack.frames_ptr[0].position.line = line.toInt32(global); + // TODO: don't sourcemap it twice + if (auto originalLine = obj->getIfPropertyExists(global, JSC::Identifier::fromString(global->vm(), "originalLine"_s))) { + except->stack.frames_ptr[0].position.line = originalLine.toInt32(global); + } else { + except->stack.frames_ptr[0].position.line = line.toInt32(global); + } except->stack.frames_len = 1; } } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index cb1a50f1d..5c58eff60 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1981,19 +1981,21 @@ pub const VirtualMachine = struct { } pub fn remapStackFramePositions(this: *VirtualMachine, frames: [*]JSC.ZigStackFrame, frames_count: usize) void { - var i: usize = 0; - while (i < frames_count) : (i += 1) { - if (frames[i].position.isInvalid()) continue; + for (frames[0..frames_count]) |*frame| { + if (frame.position.isInvalid() or frame.remapped) continue; + var sourceURL = frame.source_url.toSlice(bun.default_allocator); + defer sourceURL.deinit(); + if (this.source_mappings.resolveMapping( - frames[i].source_url.slice(), - @max(frames[i].position.line, 0), - @max(frames[i].position.column_start, 0), + sourceURL.slice(), + @max(frame.position.line, 0), + @max(frame.position.column_start, 0), )) |mapping| { - frames[i].position.line = mapping.original.lines; - frames[i].position.column_start = mapping.original.columns; - frames[i].remapped = true; + frame.position.line = mapping.original.lines; + frame.position.column_start = mapping.original.columns; + frame.remapped = true; } else { - frames[i].remapped = true; + frame.remapped = true; } } } diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index d96f91483..cb2624681 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -5,6 +5,18 @@ afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; }); +test("Regular .stack", () => { + var err; + class Foo { + constructor() { + err = new Error("wat"); + } + } + + new Foo(); + expect(err.stack).toMatch(/at new Foo/); +}); + test("capture stack trace", () => { function f1() { f2(); -- cgit v1.2.3 From 3258bed1c03a7808b9f2e4970012668e984ed390 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 28 Jun 2023 19:28:53 -0700 Subject: use main field over module for runtime (#3448) * use main field over module for runtime * move flag to `Resolver` * set `prefer_module_field` in `initWithModuleGraph` --- src/bun.js/javascript.zig | 2 + src/resolver/resolver.zig | 9 +- test/bundler/esbuild/default.test.ts | 200 +++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 5c58eff60..cf6a65841 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -801,6 +801,7 @@ pub const VirtualMachine = struct { vm.bundler.macro_context = null; vm.bundler.resolver.store_fd = false; + vm.bundler.resolver.prefer_module_field = false; vm.bundler.resolver.onWakePackageManager = .{ .context = &vm.modules, @@ -898,6 +899,7 @@ pub const VirtualMachine = struct { vm.bundler.macro_context = null; vm.bundler.resolver.store_fd = store_fd; + vm.bundler.resolver.prefer_module_field = false; vm.bundler.resolver.onWakePackageManager = .{ .context = &vm.modules, diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index d9f4dc887..e1e83ba4f 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -533,6 +533,10 @@ pub const Resolver = struct { // all parent directories dir_cache: *DirInfo.HashMap, + /// This is set to false for the runtime. The runtime should choose "main" + /// over "module" in package.json + prefer_module_field: bool = true, + pub fn getPackageManager(this: *Resolver) *PackageManager { return this.package_manager orelse brk: { bun.HTTPThead.init() catch unreachable; @@ -3408,7 +3412,10 @@ pub const Resolver = struct { // with this same path. The goal of this code is to avoid having // both the "module" file and the "main" file in the bundle at the // same time. - if (kind != ast.ImportKind.require) { + // + // Additionally, if this is for the runtime, use the "main" field. + // If it doesn't exist, the "module" field will be used. + if (r.prefer_module_field and kind != ast.ImportKind.require) { if (r.debug_logs) |*debug| { debug.addNoteFmt("Resolved to \"{s}\" using the \"module\" field in \"{s}\"", .{ auto_main_result.path_pair.primary.text, pkg_json.source.key_path.text }); diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts index 4c4cf87be..20856ecdf 100644 --- a/test/bundler/esbuild/default.test.ts +++ b/test/bundler/esbuild/default.test.ts @@ -6565,4 +6565,204 @@ describe("bundler", () => { api.expectFile("/out.js").not.toContain("data = 123"); }, }); + itBundled("default/BundlerUsesModuleFieldForEsm", { + files: { + "/entry.js": ` + import { foo } from 'foo'; + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js", + "main": "index.cjs.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + run: { + stdout: "hello index.esm.js", + }, + }); + itBundled("default/BundlerUsesMainFieldForCjs", { + files: { + "/entry.js": ` + const { foo } = require('foo'); + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js", + "main": "index.cjs.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + run: { + stdout: "hello index.cjs.js", + }, + }); + itBundled("default/RuntimeUsesMainFieldForCjs", { + files: { + "/entry.js": ` + const { foo } = require('foo'); + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js", + "main": "index.cjs.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + bundling: false, + run: { + stdout: "hello index.cjs.js", + }, + }); + itBundled("default/RuntimeUsesMainFieldForEsm", { + files: { + "/entry.js": ` + import { foo } from 'foo'; + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js", + "main": "index.cjs.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + bundling: false, + run: { + stdout: "hello index.cjs.js", + }, + }); + itBundled("default/BundlerUsesModuleFieldIfMainDoesNotExistCjs", { + files: { + "/entry.js": ` + const { foo } = require('foo'); + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + run: { + stdout: "hello index.esm.js", + }, + }); + itBundled("default/BundlerUsesModuleFieldIfMainDoesNotExistEsm", { + files: { + "/entry.js": ` + import { foo } from 'foo'; + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + run: { + stdout: "hello index.esm.js", + }, + }); + itBundled("default/RuntimeUsesModuleFieldIfMainDoesNotExistCjs", { + files: { + "/entry.js": ` + const { foo } = require('foo'); + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + bundling: false, + run: { + stdout: "hello index.esm.js", + }, + }); + itBundled("default/RuntimeUsesModuleFieldIfMainDoesNotExistEsm", { + files: { + "/entry.js": ` + import { foo } from 'foo'; + console.log(foo); + `, + "/node_modules/foo/package.json": ` + { + "name": "foo", + "version": "2.0.0", + "module": "index.esm.js" + } + `, + "/node_modules/foo/index.cjs.js": ` + module.exports.foo = "hello index.cjs.js"; + `, + "/node_modules/foo/index.esm.js": ` + export const foo = "hello index.esm.js"; + `, + }, + bundling: false, + run: { + stdout: "hello index.esm.js", + }, + }); }); -- cgit v1.2.3 From 68e6fe00a4be7857f474cb3aa80e0d876499e3f8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jun 2023 21:11:06 -0700 Subject: Use `bun.String` for `ZigException` (#3451) * Use `bun.String` for `ZigException` * woopsie --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/ZigGlobalObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 69 +++++----------- src/bun.js/bindings/exports.zig | 130 ++++++++++++++++++++---------- src/bun.js/bindings/headers-handwritten.h | 16 ++-- src/bun.js/javascript.zig | 72 ++++++++++------- src/bun.js/test/jest.zig | 18 ++--- src/js/out/modules/node/path.js | 2 +- src/js/out/modules/node/stream.web.js | 2 +- src/logger.zig | 4 +- src/string.zig | 24 ++++++ 10 files changed, 198 insertions(+), 143 deletions(-) (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4b4edf097..9ff3ebf93 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -390,7 +390,7 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vectorline.zeroBasedInt(); remappedFrames[i].position.column_start = sourcePositions->startColumn.zeroBasedInt() + 1; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 68aef29dd..141215ebe 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3303,7 +3303,7 @@ bool JSC__JSValue__stringIncludes(JSC__JSValue value, JSC__JSGlobalObject* globa static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stackFrame, ZigStackFrame* frame) { - frame->source_url = Zig::toZigString(stackFrame->sourceURL(vm)); + frame->source_url = Bun::toString(stackFrame->sourceURL(vm)); if (stackFrame->isWasmFrame()) { frame->code_type = ZigStackFrameCodeWasm; @@ -3340,37 +3340,11 @@ static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stack JSC::JSObject* callee = JSC::jsCast(calleeCell); - // Does the code block have a user-defined name property? - JSC::JSValue name = callee->getDirect(vm, vm.propertyNames->name); - if (name && name.isString()) { - auto str = name.toWTFString(m_codeBlock->globalObject()); - frame->function_name = Zig::toZigString(str); - return; - } - - /* For functions (either JSFunction or InternalFunction), fallback to their "native" name - * property. Based on JSC::getCalculatedDisplayName, "inlining" the - * JSFunction::calculatedDisplayName\InternalFunction::calculatedDisplayName calls */ - if (JSC::JSFunction* function = JSC::jsDynamicCast(callee)) { - - WTF::String actualName = function->name(vm); - if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) { - frame->function_name = Zig::toZigString(actualName); - return; - } - - auto inferred_name = function->jsExecutable()->name(); - frame->function_name = Zig::toZigString(inferred_name.string()); - } - - if (JSC::InternalFunction* function = JSC::jsDynamicCast(callee)) { - // Based on JSC::InternalFunction::calculatedDisplayName, skipping the "displayName" property - frame->function_name = Zig::toZigString(function->name()); - } + frame->function_name = Bun::toString(JSC::getCalculatedDisplayName(vm, callee)); } // Based on // https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/src/shim/JSCStackTrace.cpp#L298 -static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigString* source_lines, +static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunString* source_lines, int32_t* source_line_numbers, uint8_t source_lines_count, ZigStackFramePosition* position) { @@ -3440,7 +3414,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigStr // Most of the time, when you look at a stack trace, you want a couple lines above - source_lines[0] = { &chars[lineStart], lineStop - lineStart }; + source_lines[0] = Bun::toString(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); source_line_numbers[0] = line; if (lineStart > 0) { @@ -3457,8 +3431,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, ZigStr } // We are at the beginning of the line - source_lines[source_line_i] = { &chars[byte_offset_in_source_string], - end_of_line_offset - byte_offset_in_source_string + 1 }; + source_lines[source_line_i] = Bun::toString(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); source_line_numbers[source_line_i] = line - source_line_i; source_line_i++; @@ -3546,30 +3519,30 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->code = 8; } if (except->code == SYNTAX_ERROR_CODE) { - except->message = Zig::toZigString(err->sanitizedMessageString(global)); + except->message = Bun::toString(err->sanitizedMessageString(global)); } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { - except->message = Zig::toZigString(message, global); + except->message = Bun::toString(global, message); } else { - except->message = Zig::toZigString(err->sanitizedMessageString(global)); + except->message = Bun::toString(err->sanitizedMessageString(global)); } - except->name = Zig::toZigString(err->sanitizedNameString(global)); + except->name = Bun::toString(err->sanitizedNameString(global)); except->runtime_type = err->runtimeTypeForCause(); auto clientData = WebCore::clientData(vm); if (except->code != SYNTAX_ERROR_CODE) { if (JSC::JSValue syscall = obj->getIfPropertyExists(global, clientData->builtinNames().syscallPublicName())) { - except->syscall = Zig::toZigString(syscall, global); + except->syscall = Bun::toString(global, syscall); } if (JSC::JSValue code = obj->getIfPropertyExists(global, clientData->builtinNames().codePublicName())) { - except->code_ = Zig::toZigString(code, global); + except->code_ = Bun::toString(global, code); } if (JSC::JSValue path = obj->getIfPropertyExists(global, clientData->builtinNames().pathPublicName())) { - except->path = Zig::toZigString(path, global); + except->path = Bun::toString(global, path); } if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(vm, "fd"_s))) { @@ -3585,7 +3558,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (getFromSourceURL) { if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, vm.propertyNames->sourceURL)) { - except->stack.frames_ptr[0].source_url = Zig::toZigString(sourceURL, global); + except->stack.frames_ptr[0].source_url = Bun::toString(global, sourceURL); if (JSC::JSValue column = obj->getIfPropertyExists(global, vm.propertyNames->column)) { except->stack.frames_ptr[0].position.column_start = column.toInt32(global); @@ -3597,7 +3570,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "lineText"_s))) { if (JSC::JSString* jsStr = lineText.toStringOrNull(global)) { auto str = jsStr->value(global); - except->stack.source_lines_ptr[0] = Zig::toZigString(str); + except->stack.source_lines_ptr[0] = Bun::toString(str); except->stack.source_lines_numbers[0] = except->stack.frames_ptr[0].position.line; except->stack.source_lines_len = 1; except->remapped = true; @@ -3620,7 +3593,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSObject* obj = JSC::jsDynamicCast(value)) { if (obj->hasProperty(global, global->vm().propertyNames->name)) { auto name_str = obj->getIfPropertyExists(global, global->vm().propertyNames->name).toWTFString(global); - except->name = Zig::toZigString(name_str); + except->name = Bun::toString(name_str); if (name_str == "Error"_s) { except->code = JSErrorCodeError; } else if (name_str == "EvalError"_s) { @@ -3642,14 +3615,14 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSValue message = obj->getIfPropertyExists(global, global->vm().propertyNames->message)) { if (message) { - except->message = Zig::toZigString( + except->message = Bun::toString( message.toWTFString(global)); } } if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, global->vm().propertyNames->sourceURL)) { if (sourceURL) { - except->stack.frames_ptr[0].source_url = Zig::toZigString( + except->stack.frames_ptr[0].source_url = Bun::toString( sourceURL.toWTFString(global)); except->stack.frames_len = 1; } @@ -3678,9 +3651,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal } scope.release(); - auto ref = OpaqueJSString::tryCreate(str); - except->message = ZigString { ref->characters8(), ref->length() }; - ref->ref(); + except->message = Bun::toString(str); } void JSC__VM__releaseWeakRefs(JSC__VM* arg0) @@ -3790,8 +3761,8 @@ void JSC__JSValue__toZigException(JSC__JSValue JSValue0, JSC__JSGlobalObject* ar JSC::JSValue value = JSC::JSValue::decode(JSValue0); if (value == JSC::JSValue {}) { exception->code = JSErrorCodeError; - exception->name = Zig::toZigString("Error"_s); - exception->message = Zig::toZigString("Unknown error"_s); + exception->name = Bun::toString("Error"_s); + exception->message = Bun::toString("Unknown error"_s); return; } diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 213291e7b..73a26e4be 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -29,6 +29,7 @@ const Backtrace = @import("../../crash_reporter.zig"); const JSPrinter = bun.js_printer; const JSLexer = bun.js_lexer; const typeBaseName = @import("../../meta.zig").typeBaseName; +const String = bun.String; pub const ZigGlobalObject = extern struct { pub const shim = Shimmer("Zig", "GlobalObject", @This()); @@ -438,7 +439,7 @@ pub const Process = extern struct { }; pub const ZigStackTrace = extern struct { - source_lines_ptr: [*c]ZigString, + source_lines_ptr: [*c]bun.String, source_lines_numbers: [*c]i32, source_lines_len: u8, source_lines_to_collect: u8, @@ -456,23 +457,24 @@ pub const ZigStackTrace = extern struct { { var source_lines_iter = this.sourceLineIterator(); - var source_line_len: usize = 0; - var count: usize = 0; - while (source_lines_iter.next()) |source| { - count += 1; - source_line_len += source.text.len; - } + var source_line_len = source_lines_iter.getLength(); - if (count > 0 and source_line_len > 0) { - var source_lines = try allocator.alloc(Api.SourceLine, count); + if (source_line_len > 0) { + var source_lines = try allocator.alloc(Api.SourceLine, @intCast(usize, @max(source_lines_iter.i, 0))); var source_line_buf = try allocator.alloc(u8, source_line_len); source_lines_iter = this.sourceLineIterator(); var remain_buf = source_line_buf[0..]; var i: usize = 0; while (source_lines_iter.next()) |source| { - bun.copy(u8, remain_buf, source.text); - const copied_line = remain_buf[0..source.text.len]; - remain_buf = remain_buf[source.text.len..]; + const text = source.text.slice(); + defer source.text.deinit(); + defer bun.copy( + u8, + remain_buf, + text, + ); + const copied_line = remain_buf[0..text.len]; + remain_buf = remain_buf[text.len..]; source_lines[i] = .{ .text = copied_line, .line = source.line }; i += 1; } @@ -508,9 +510,18 @@ pub const ZigStackTrace = extern struct { pub const SourceLine = struct { line: i32, - text: string, + text: ZigString.Slice, }; + pub fn getLength(this: *SourceLineIterator) usize { + var count: usize = 0; + for (this.trace.source_lines_ptr[0..@intCast(usize, this.i)]) |*line| { + count += line.length(); + } + + return count; + } + pub fn untilLast(this: *SourceLineIterator) ?SourceLine { if (this.i < 1) return null; return this.next(); @@ -522,7 +533,7 @@ pub const ZigStackTrace = extern struct { const source_line = this.trace.source_lines_ptr[@intCast(usize, this.i)]; const result = SourceLine{ .line = this.trace.source_lines_numbers[@intCast(usize, this.i)], - .text = source_line.slice(), + .text = source_line.toUTF8(bun.default_allocator), }; this.i -= 1; return result; @@ -541,21 +552,28 @@ pub const ZigStackTrace = extern struct { }; pub const ZigStackFrame = extern struct { - function_name: ZigString, - source_url: ZigString, + function_name: String, + source_url: String, position: ZigStackFramePosition, code_type: ZigStackFrameCode, /// This informs formatters whether to display as a blob URL or not remapped: bool = false, + pub fn deinit(this: *ZigStackFrame) void { + this.function_name.deref(); + this.source_url.deref(); + } + pub fn toAPI(this: *const ZigStackFrame, root_path: string, origin: ?*const ZigURL, allocator: std.mem.Allocator) !Api.StackFrame { var frame: Api.StackFrame = comptime std.mem.zeroes(Api.StackFrame); - if (this.function_name.len > 0) { - frame.function_name = try allocator.dupe(u8, this.function_name.slice()); + if (!this.function_name.isEmpty()) { + var slicer = this.function_name.toUTF8(allocator); + defer slicer.deinit(); + frame.function_name = (try slicer.clone(allocator)).slice(); } - if (this.source_url.len > 0) { + if (!this.source_url.isEmpty()) { frame.file = try std.fmt.allocPrint(allocator, "{any}", .{this.sourceURLFormatter(root_path, origin, true, false)}); } @@ -576,7 +594,7 @@ pub const ZigStackFrame = extern struct { } pub const SourceURLFormatter = struct { - source_url: ZigString, + source_url: bun.String, position: ZigStackFramePosition, enable_color: bool, origin: ?*const ZigURL, @@ -588,7 +606,9 @@ pub const ZigStackFrame = extern struct { try writer.writeAll(Output.prettyFmt("", true)); } - var source_slice = this.source_url.slice(); + var source_slice_ = this.source_url.toUTF8(bun.default_allocator); + var source_slice = source_slice_.slice(); + defer source_slice_.deinit(); if (!this.remapped) { if (this.origin) |origin| { @@ -647,12 +667,12 @@ pub const ZigStackFrame = extern struct { }; pub const NameFormatter = struct { - function_name: ZigString, + function_name: String, code_type: ZigStackFrameCode, enable_color: bool, pub fn format(this: NameFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - const name = this.function_name.slice(); + const name = this.function_name; switch (this.code_type) { .Eval => { @@ -662,26 +682,26 @@ pub const ZigStackFrame = extern struct { // try writer.writeAll("(esm)"); }, .Function => { - if (name.len > 0) { + if (!name.isEmpty()) { if (this.enable_color) { - try std.fmt.format(writer, comptime Output.prettyFmt("{s}", true), .{name}); + try std.fmt.format(writer, comptime Output.prettyFmt("{}", true), .{name}); } else { - try std.fmt.format(writer, "{s}", .{name}); + try std.fmt.format(writer, "{}", .{name}); } } }, .Global => { - if (name.len > 0) { - try std.fmt.format(writer, "globalThis {s}", .{name}); + if (!name.isEmpty()) { + try std.fmt.format(writer, "globalThis {}", .{name}); } else { try writer.writeAll("globalThis"); } }, .Wasm => { - try std.fmt.format(writer, "WASM {s}", .{name}); + try std.fmt.format(writer, "WASM {}", .{name}); }, .Constructor => { - try std.fmt.format(writer, "new {s}", .{name}); + try std.fmt.format(writer, "new {}", .{name}); }, else => {}, } @@ -689,9 +709,9 @@ pub const ZigStackFrame = extern struct { }; pub const Zero: ZigStackFrame = ZigStackFrame{ - .function_name = ZigString{ ._unsafe_ptr_do_not_use = "", .len = 0 }, + .function_name = String.empty, .code_type = ZigStackFrameCode.None, - .source_url = ZigString{ ._unsafe_ptr_do_not_use = "", .len = 0 }, + .source_url = String.empty, .position = ZigStackFramePosition.Invalid, }; @@ -744,14 +764,14 @@ pub const ZigException = extern struct { /// SystemError only errno: c_int = 0, /// SystemError only - syscall: ZigString = ZigString.Empty, + syscall: String = String.empty, /// SystemError only - system_code: ZigString = ZigString.Empty, + system_code: String = String.empty, /// SystemError only - path: ZigString = ZigString.Empty, + path: String = String.empty, - name: ZigString, - message: ZigString, + name: String, + message: String, stack: ZigStackTrace, exception: ?*anyopaque, @@ -760,6 +780,19 @@ pub const ZigException = extern struct { fd: i32 = -1, + pub fn deinit(this: *ZigException) void { + this.syscall.deref(); + this.system_code.deref(); + this.path.deref(); + + this.name.deref(); + this.message.deref(); + + for (this.stack.frames_ptr[0..this.stack.frames_len]) |*frame| { + frame.deinit(); + } + } + pub const shim = Shimmer("Zig", "Exception", @This()); pub const name = "ZigException"; pub const namespace = shim.namespace; @@ -768,7 +801,7 @@ pub const ZigException = extern struct { const frame_count = 32; pub const source_lines_count = 6; source_line_numbers: [source_lines_count]i32, - source_lines: [source_lines_count]ZigString, + source_lines: [source_lines_count]String, frames: [frame_count]ZigStackFrame, loaded: bool, zig_exception: ZigException, @@ -786,8 +819,8 @@ pub const ZigException = extern struct { }, .source_lines = brk: { - var lines: [source_lines_count]ZigString = undefined; - @memset(&lines, ZigString.Empty); + var lines: [source_lines_count]String = undefined; + @memset(&lines, String.empty); break :brk lines; }, .zig_exception = undefined, @@ -798,13 +831,17 @@ pub const ZigException = extern struct { return Holder.Zero; } + pub fn deinit(this: *Holder) void { + this.zigException().deinit(); + } + pub fn zigException(this: *Holder) *ZigException { if (!this.loaded) { this.zig_exception = ZigException{ .code = @enumFromInt(JSErrorCode, 255), .runtime_type = JSRuntimeType.Nothing, - .name = ZigString.Empty, - .message = ZigString.Empty, + .name = String.empty, + .message = String.empty, .exception = null, .stack = ZigStackTrace{ .source_lines_ptr = &this.source_lines, @@ -832,8 +869,13 @@ pub const ZigException = extern struct { root_path: string, origin: ?*const ZigURL, ) !void { - const _name: string = @field(this, "name").slice(); - const message: string = @field(this, "message").slice(); + const name_slice = @field(this, "name").toUTF8(bun.default_allocator); + const message_slice = @field(this, "message").toUTF8(bun.default_allocator); + + const _name = name_slice.slice(); + defer name_slice.deinit(); + const message = message_slice.slice(); + defer message_slice.deinit(); var is_empty = true; var api_exception = Api.JsException{ diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index db1e38d3e..c7429b633 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -120,15 +120,15 @@ typedef struct ZigStackFramePosition { } ZigStackFramePosition; typedef struct ZigStackFrame { - ZigString function_name; - ZigString source_url; + BunString function_name; + BunString source_url; ZigStackFramePosition position; ZigStackFrameCode code_type; bool remapped; } ZigStackFrame; typedef struct ZigStackTrace { - ZigString* source_lines_ptr; + BunString* source_lines_ptr; int32_t* source_lines_numbers; uint8_t source_lines_len; uint8_t source_lines_to_collect; @@ -140,11 +140,11 @@ typedef struct ZigException { unsigned char code; uint16_t runtime_type; int errno_; - ZigString syscall; - ZigString code_; - ZigString path; - ZigString name; - ZigString message; + BunString syscall; + BunString code_; + BunString path; + BunString name; + BunString message; ZigStackTrace stack; void* exception; bool remapped; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index cf6a65841..605cc0c25 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1805,6 +1805,7 @@ pub const VirtualMachine = struct { if (exception) |exception_| { var holder = ZigException.Holder.init(); var zig_exception: *ZigException = holder.zigException(); + defer zig_exception.deinit(); exception_.getStackTrace(&zig_exception.stack); if (zig_exception.stack.frames_len > 0) { if (allow_ansi_color) { @@ -1932,8 +1933,14 @@ pub const VirtualMachine = struct { while (i < stack.len) : (i += 1) { const frame = stack[@intCast(usize, i)]; - const file = frame.source_url.slice(); - const func = frame.function_name.slice(); + const file_slice = frame.source_url.toSlice(bun.default_allocator); + defer file_slice.deinit(); + const func_slice = frame.function_name.toSlice(bun.default_allocator); + defer func_slice.deinit(); + + const file = file_slice.slice(); + const func = func_slice.slice(); + if (file.len == 0 and func.len == 0) continue; const has_name = std.fmt.count("{any}", .{frame.nameFormatter( @@ -1985,7 +1992,7 @@ pub const VirtualMachine = struct { pub fn remapStackFramePositions(this: *VirtualMachine, frames: [*]JSC.ZigStackFrame, frames_count: usize) void { for (frames[0..frames_count]) |*frame| { if (frame.position.isInvalid() or frame.remapped) continue; - var sourceURL = frame.source_url.toSlice(bun.default_allocator); + var sourceURL = frame.source_url.toUTF8(bun.default_allocator); defer sourceURL.deinit(); if (this.source_mappings.resolveMapping( @@ -2049,8 +2056,10 @@ pub const VirtualMachine = struct { if (frames.len == 0) return; var top = &frames[0]; + var top_source_url = top.source_url.toUTF8(bun.default_allocator); + defer top_source_url.deinit(); if (this.source_mappings.resolveMapping( - top.source_url.slice(), + top_source_url.slice(), @max(top.position.line, 0), @max(top.position.column_start, 0), )) |mapping| { @@ -2078,18 +2087,18 @@ pub const VirtualMachine = struct { )) |lines| { var source_lines = exception.stack.source_lines_ptr[0..JSC.ZigException.Holder.source_lines_count]; var source_line_numbers = exception.stack.source_lines_numbers[0..JSC.ZigException.Holder.source_lines_count]; - @memset(source_lines, ZigString.Empty); + @memset(source_lines, String.empty); @memset(source_line_numbers, 0); var lines_ = lines[0..@min(lines.len, source_lines.len)]; for (lines_, 0..) |line, j| { - source_lines[(lines_.len - 1) - j] = ZigString.init(line); + source_lines[(lines_.len - 1) - j] = String.init(line); source_line_numbers[j] = top.position.line - @intCast(i32, j) + 1; } exception.stack.source_lines_len = @intCast(u8, lines_.len); - top.position.column_stop = @intCast(i32, source_lines[lines_.len - 1].len); + top.position.column_stop = @intCast(i32, source_lines[lines_.len - 1].length()); top.position.line_stop = top.position.column_stop; // This expression range is no longer accurate @@ -2101,8 +2110,10 @@ pub const VirtualMachine = struct { if (frames.len > 1) { for (frames[1..]) |*frame| { if (frame.position.isInvalid()) continue; + const source_url = frame.source_url.toUTF8(bun.default_allocator); + defer source_url.deinit(); if (this.source_mappings.resolveMapping( - frame.source_url.slice(), + source_url.slice(), @max(frame.position.line, 0), @max(frame.position.column_start, 0), )) |mapping| { @@ -2117,6 +2128,7 @@ pub const VirtualMachine = struct { pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) !void { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); + defer exception_holder.deinit(); this.remapZigException(exception, error_instance, exception_list); this.had_errors = true; @@ -2134,15 +2146,18 @@ pub const VirtualMachine = struct { var source_lines = exception.stack.sourceLineIterator(); var last_pad: u64 = 0; while (source_lines.untilLast()) |source| { + defer source.text.deinit(); + const int_size = std.fmt.count("{d}", .{source.line}); const pad = max_line_number_pad - int_size; last_pad = pad; try writer.writeByteNTimes(' ', pad); + try writer.print( comptime Output.prettyFmt("{d} | {s}\n", allow_ansi_color), .{ source.line, - std.mem.trim(u8, source.text, "\n"), + std.mem.trim(u8, source.text.slice(), "\n"), }, ); } @@ -2158,7 +2173,8 @@ pub const VirtualMachine = struct { const top_frame = if (exception.stack.frames_len > 0) exception.stack.frames()[0] else null; if (top_frame == null or top_frame.?.position.isInvalid()) { defer did_print_name = true; - var text = std.mem.trim(u8, source.text, "\n"); + defer source.text.deinit(); + var text = std.mem.trim(u8, source.text.slice(), "\n"); try writer.print( comptime Output.prettyFmt( @@ -2176,7 +2192,9 @@ pub const VirtualMachine = struct { const int_size = std.fmt.count("{d}", .{source.line}); const pad = max_line_number_pad - int_size; try writer.writeByteNTimes(' ', pad); - var remainder = std.mem.trim(u8, source.text, "\n"); + defer source.text.deinit(); + const text = source.text.slice(); + var remainder = std.mem.trim(u8, text, "\n"); try writer.print( comptime Output.prettyFmt( @@ -2188,7 +2206,7 @@ pub const VirtualMachine = struct { if (!top.position.isInvalid()) { var first_non_whitespace = @intCast(u32, top.position.column_start); - while (first_non_whitespace < source.text.len and source.text[first_non_whitespace] == ' ') { + while (first_non_whitespace < text.len and text[first_non_whitespace] == ' ') { first_non_whitespace += 1; } const indent = @intCast(usize, pad) + " | ".len + first_non_whitespace; @@ -2219,10 +2237,10 @@ pub const VirtualMachine = struct { }; var show = Show{ - .system_code = exception.system_code.len > 0 and !strings.eql(exception.system_code.slice(), name.slice()), - .syscall = exception.syscall.len > 0, + .system_code = !exception.system_code.eql(name) and !exception.system_code.isEmpty(), + .syscall = !exception.syscall.isEmpty(), .errno = exception.errno < 0, - .path = exception.path.len > 0, + .path = !exception.path.isEmpty(), .fd = exception.fd != -1, }; @@ -2262,7 +2280,7 @@ pub const VirtualMachine = struct { } else if (show.errno) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt(" path: \"{s}\"\n", allow_ansi_color), .{exception.path}); + try writer.print(comptime Output.prettyFmt(" path: \"{}\"\n", allow_ansi_color), .{exception.path}); } if (show.fd) { @@ -2281,12 +2299,12 @@ pub const VirtualMachine = struct { } else if (show.errno) { try writer.writeAll(" "); } - try writer.print(comptime Output.prettyFmt(" code: \"{s}\"\n", allow_ansi_color), .{exception.system_code}); + try writer.print(comptime Output.prettyFmt(" code: \"{}\"\n", allow_ansi_color), .{exception.system_code}); add_extra_line = true; } if (show.syscall) { - try writer.print(comptime Output.prettyFmt(" syscall: \"{s}\"\n", allow_ansi_color), .{exception.syscall}); + try writer.print(comptime Output.prettyFmt(" syscall: \"{}\"\n", allow_ansi_color), .{exception.syscall}); add_extra_line = true; } @@ -2303,22 +2321,22 @@ pub const VirtualMachine = struct { try printStackTrace(@TypeOf(writer), writer, exception.stack, allow_ansi_color); } - fn printErrorNameAndMessage(_: *VirtualMachine, name: ZigString, message: ZigString, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { - if (name.len > 0 and message.len > 0) { - const display_name: ZigString = if (!name.is16Bit() and strings.eqlComptime(name.slice(), "Error")) ZigString.init("error") else name; + fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { + if (!name.isEmpty() and !message.isEmpty()) { + const display_name: String = if (name.eqlComptime("Error")) String.init("error") else name; try writer.print(comptime Output.prettyFmt("{any}: {s}\n", allow_ansi_color), .{ display_name, message, }); - } else if (name.len > 0) { - if (name.is16Bit() or !strings.hasPrefixComptime(name.slice(), "error")) { - try writer.print(comptime Output.prettyFmt("error: {s}\n", allow_ansi_color), .{name}); + } else if (!name.isEmpty()) { + if (!name.hasPrefixComptime("error")) { + try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{name}); } else { - try writer.print(comptime Output.prettyFmt("{s}\n", allow_ansi_color), .{name}); + try writer.print(comptime Output.prettyFmt("{}\n", allow_ansi_color), .{name}); } - } else if (message.len > 0) { - try writer.print(comptime Output.prettyFmt("error: {s}\n", allow_ansi_color), .{message}); + } else if (!message.isEmpty()) { + try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{message}); } else { try writer.print(comptime Output.prettyFmt("error\n", allow_ansi_color), .{}); } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d3bb90747..5ae2337f9 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1532,8 +1532,8 @@ inline fn createIfScope( // In Github Actions, emit an annotation that renders the error and location. // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message pub fn printGithubAnnotation(exception: *JSC.ZigException) void { - const name = exception.name; - const message = exception.message; + const name = @field(exception, "name"); + const message = @field(exception, "message"); const frames = exception.stack.frames(); const top_frame = if (frames.len > 0) frames[0] else null; const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; @@ -1543,7 +1543,7 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { if (top_frame) |frame| { if (!frame.position.isInvalid()) { - const source_url = frame.source_url.toSlice(allocator); + const source_url = frame.source_url.toUTF8(allocator); defer source_url.deinit(); const file = bun.path.relative(dir, source_url.slice()); Output.printError("\n::error file={s},line={d},col={d},title=", .{ @@ -1559,14 +1559,14 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { Output.printError("\n::error title=", .{}); } - if (name.len == 0 or name.eqlComptime("Error")) { + if (name.isEmpty() or name.eqlComptime("Error")) { Output.printError("error", .{}); } else { Output.printError("{s}", .{name.githubAction()}); } - if (message.len > 0) { - const message_slice = message.toSlice(allocator); + if (!message.isEmpty()) { + const message_slice = message.toUTF8(allocator); defer message_slice.deinit(); const msg = message_slice.slice(); @@ -1574,7 +1574,7 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { while (strings.indexOfNewlineOrNonASCIIOrANSI(msg, cursor)) |i| { cursor = i + 1; if (msg[i] == '\n') { - const first_line = ZigString.init(msg[0..i]); + const first_line = bun.String.fromUTF8(msg[0..i]); Output.printError(": {s}::", .{first_line.githubAction()}); break; } @@ -1605,10 +1605,10 @@ pub fn printGithubAnnotation(exception: *JSC.ZigException) void { var i: i16 = 0; while (i < frames.len) : (i += 1) { const frame = frames[@intCast(usize, i)]; - const source_url = frame.source_url.toSlice(allocator); + const source_url = frame.source_url.toUTF8(allocator); defer source_url.deinit(); const file = bun.path.relative(dir, source_url.slice()); - const func = frame.function_name.toSlice(allocator); + const func = frame.function_name.toUTF8(allocator); if (file.len == 0 and func.len == 0) continue; diff --git a/src/js/out/modules/node/path.js b/src/js/out/modules/node/path.js index f8cc1ec08..3a3a75207 100644 --- a/src/js/out/modules/node/path.js +++ b/src/js/out/modules/node/path.js @@ -1 +1 @@ -var g=function(i){var f=m({basename:i.basename.bind(i),dirname:i.dirname.bind(i),extname:i.extname.bind(i),format:i.format.bind(i),isAbsolute:i.isAbsolute.bind(i),join:i.join.bind(i),normalize:i.normalize.bind(i),parse:i.parse.bind(i),relative:i.relative.bind(i),resolve:i.resolve.bind(i),toNamespacedPath:i.toNamespacedPath.bind(i),sep:i.sep,delimiter:i.delimiter});return f.default=f,f},m=(i)=>Object.assign(Object.create(null),i),k=g(Bun._Path()),q=g(Bun._Path(!1)),v=g(Bun._Path(!0));k.win32=v;k.posix=q;var{basename:y,dirname:z,extname:A,format:B,isAbsolute:C,join:D,normalize:E,parse:F,relative:G,resolve:H,toNamespacedPath:I,sep:J,delimiter:K,__esModule:L}=k;k[Symbol.for("CommonJS")]=0;k.__esModule=!0;var O=k;export{v as win32,I as toNamespacedPath,J as sep,H as resolve,G as relative,q as posix,F as parse,E as normalize,D as join,C as isAbsolute,B as format,A as extname,z as dirname,K as delimiter,O as default,m as createModule,y as basename,L as __esModule}; +var i=function(N){var m=g({basename:N.basename.bind(N),dirname:N.dirname.bind(N),extname:N.extname.bind(N),format:N.format.bind(N),isAbsolute:N.isAbsolute.bind(N),join:N.join.bind(N),normalize:N.normalize.bind(N),parse:N.parse.bind(N),relative:N.relative.bind(N),resolve:N.resolve.bind(N),toNamespacedPath:N.toNamespacedPath.bind(N),sep:N.sep,delimiter:N.delimiter});return m.default=m,m},g=(N)=>Object.assign(Object.create(null),N),f=i(Bun._Path()),J=i(Bun._Path(!1)),k=i(Bun._Path(!0));f.win32=k;f.posix=J;var{basename:q,dirname:v,extname:y,format:z,isAbsolute:A,join:B,normalize:K,parse:C,relative:D,resolve:E,toNamespacedPath:F,sep:G,delimiter:H,__esModule:I}=f;f[Symbol.for("CommonJS")]=0;f.__esModule=!0;var O=f;export{k as win32,F as toNamespacedPath,G as sep,E as resolve,D as relative,J as posix,C as parse,K as normalize,B as join,A as isAbsolute,z as format,y as extname,v as dirname,H as delimiter,O as default,g as createModule,q as basename,I as __esModule}; diff --git a/src/js/out/modules/node/stream.web.js b/src/js/out/modules/node/stream.web.js index bb906418c..f91ee03b4 100644 --- a/src/js/out/modules/node/stream.web.js +++ b/src/js/out/modules/node/stream.web.js @@ -1 +1 @@ -var{ReadableStream:c,ReadableStreamDefaultController:j,WritableStream:k,WritableStreamDefaultController:p,WritableStreamDefaultWriter:v,TransformStream:w,TransformStreamDefaultController:x,ByteLengthQueuingStrategy:z,CountQueuingStrategy:A,ReadableStreamBYOBReader:E,ReadableStreamBYOBRequest:F,ReadableStreamDefaultReader:G}=globalThis,H={ReadableStream:c,ReadableStreamDefaultController:j,WritableStream:k,WritableStreamDefaultController:p,WritableStreamDefaultWriter:v,TransformStream:w,TransformStreamDefaultController:x,ByteLengthQueuingStrategy:z,CountQueuingStrategy:A,ReadableStreamBYOBReader:E,ReadableStreamBYOBRequest:F,ReadableStreamDefaultReader:G,[Symbol.for("CommonJS")]:0};export{H as default,v as WritableStreamDefaultWriter,p as WritableStreamDefaultController,k as WritableStream,x as TransformStreamDefaultController,w as TransformStream,G as ReadableStreamDefaultReader,j as ReadableStreamDefaultController,F as ReadableStreamBYOBRequest,E as ReadableStreamBYOBReader,c as ReadableStream,A as CountQueuingStrategy,z as ByteLengthQueuingStrategy}; +var{ReadableStream:k,ReadableStreamDefaultController:v,WritableStream:w,WritableStreamDefaultController:x,WritableStreamDefaultWriter:z,TransformStream:A,TransformStreamDefaultController:E,ByteLengthQueuingStrategy:F,CountQueuingStrategy:c,ReadableStreamBYOBReader:j,ReadableStreamBYOBRequest:G,ReadableStreamDefaultReader:p}=globalThis,H={ReadableStream:k,ReadableStreamDefaultController:v,WritableStream:w,WritableStreamDefaultController:x,WritableStreamDefaultWriter:z,TransformStream:A,TransformStreamDefaultController:E,ByteLengthQueuingStrategy:F,CountQueuingStrategy:c,ReadableStreamBYOBReader:j,ReadableStreamBYOBRequest:G,ReadableStreamDefaultReader:p,[Symbol.for("CommonJS")]:0};export{H as default,z as WritableStreamDefaultWriter,x as WritableStreamDefaultController,w as WritableStream,E as TransformStreamDefaultController,A as TransformStream,p as ReadableStreamDefaultReader,v as ReadableStreamDefaultController,G as ReadableStreamBYOBRequest,j as ReadableStreamBYOBReader,k as ReadableStream,c as CountQueuingStrategy,F as ByteLengthQueuingStrategy}; diff --git a/src/logger.zig b/src/logger.zig index 3279e9fd5..fc25541de 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -421,12 +421,12 @@ pub const Msg = struct { if (err.toError()) |value| { value.toZigException(globalObject, zig_exception_holder.zigException()); } else { - zig_exception_holder.zig_exception.message = JSC.ZigString.fromUTF8(err.toSlice(globalObject, allocator).slice()); + zig_exception_holder.zig_exception.message = err.toBunString(globalObject); } return Msg{ .data = .{ - .text = zig_exception_holder.zigException().message.toSliceClone(allocator).slice(), + .text = try zig_exception_holder.zigException().message.toOwnedSlice(allocator), .location = Location{ .file = file, }, diff --git a/src/string.zig b/src/string.zig index 54af2ba68..3c0c99ce5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -247,6 +247,26 @@ pub const String = extern struct { extern fn BunString__fromLatin1(bytes: [*]const u8, len: usize) String; extern fn BunString__fromBytes(bytes: [*]const u8, len: usize) String; + pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 { + switch (this.tag) { + .ZigString => return try this.value.ZigString.toOwnedSlice(allocator), + .WTFStringImpl => { + var utf8_slice = this.value.WTFStringImpl.toUTF8(allocator); + + if (utf8_slice.allocator.get()) |alloc| { + if (isWTFAllocator(alloc)) { + return @constCast((try utf8_slice.clone(allocator)).slice()); + } + } + + return @constCast(utf8_slice.slice()); + }, + .StaticZigString => return try this.value.StaticZigString.toOwnedSlice(allocator), + .Empty => return &[_]u8{}, + else => unreachable, + } + } + pub fn createLatin1(bytes: []const u8) String { JSC.markBinding(@src()); return BunString__fromLatin1(bytes.ptr, bytes.len); @@ -429,6 +449,10 @@ pub const String = extern struct { return .latin1; } + pub fn githubAction(self: String) ZigString.GithubActionFormatter { + return self.toZigString().githubAction(); + } + pub fn byteSlice(this: String) []const u8 { return switch (this.tag) { .ZigString, .StaticZigString => this.value.ZigString.byteSlice(), -- cgit v1.2.3 From 8481f2922f4f6fe07f8b44067fe478cb1ac1706a Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Wed, 28 Jun 2023 21:45:16 -0700 Subject: Add GC test for errors --- src/bun.js/WebKit | 2 +- src/bun.js/bindings/ZigGlobalObject.cpp | 8 ++++++-- src/bun.js/javascript.zig | 4 ++-- test/js/bun/util/error-gc-test.test.js | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/js/bun/util/error-gc-test.test.js (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index a9e3ab9b8..8a03cf746 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit a9e3ab9b85cf911218e1d51eed8079cd955ec578 +Subproject commit 8a03cf746abef8a48c932ab25f8821390632f2e2 diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9ff3ebf93..7ba0f0b30 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -390,7 +390,11 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector { + for (let i = 0; i < 100; i++) { + var fn = function yo() { + var err = (function innerOne() { + var err = new Error(); + for (let i = 0; i < 1000; i++) { + Bun.inspect(err); + } + Bun.gc(true); + return err; + })(); + err.stack += ""; + }; + + Object.defineProperty(fn, "name", { + value: + "yoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyoyo" + + i, + }); + + fn(); + Bun.gc(true); + } +}); -- cgit v1.2.3 From c7cc618376f2ffc8377cb40e83575ef66d402e9c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 1 Jul 2023 21:58:06 -0700 Subject: Fix leak in fd (#3487) * Fix file descriptor leak * Skip unnecessary clone * Don't break --hot --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/javascript.zig | 2 +- src/bun.js/module_loader.zig | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 572b66716..b696c6cf2 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2065,7 +2065,7 @@ pub const VirtualMachine = struct { )) |mapping| { var log = logger.Log.init(default_allocator); var errorable: ErrorableResolvedSource = undefined; - var original_source = fetchWithoutOnLoadPlugins(this, this.global, bun.String.init(top.source_url), bun.String.empty, &log, &errorable, .print_source) catch return; + var original_source = fetchWithoutOnLoadPlugins(this, this.global, top.source_url, bun.String.empty, &log, &errorable, .print_source) catch return; const code = original_source.source_code.toUTF8(bun.default_allocator); defer code.deinit(); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 6fd4fef99..e7e4d700e 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -982,6 +982,14 @@ pub const ModuleLoader = struct { jsc_vm.bundler.options.macro_remap; var fallback_source: logger.Source = undefined; + + // Usually, we want to close the input file automatically. + // + // If we're re-using the file descriptor from the fs watcher + // Do not close it because that will break the kqueue-based watcher + // + var should_close_input_file_fd = fd == null; + var input_file_fd: StoredFileDescriptorType = 0; var parse_options = Bundler.ParseOptions{ .allocator = allocator, @@ -1002,6 +1010,13 @@ pub const ModuleLoader = struct { jsc_vm.main_hash == hash and strings.eqlLong(jsc_vm.main, path.text, false), }; + defer { + if (should_close_input_file_fd and input_file_fd != 0) { + _ = bun.JSC.Node.Syscall.close(input_file_fd); + input_file_fd = 0; + } + } + if (is_node_override) { if (NodeFallbackModules.contentsFromPath(specifier)) |code| { const fallback_path = Fs.Path.initWithNamespace(specifier, "node"); @@ -1019,6 +1034,7 @@ pub const ModuleLoader = struct { if (jsc_vm.isWatcherEnabled()) { if (input_file_fd != 0) { if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + should_close_input_file_fd = false; jsc_vm.bun_watcher.?.addFile( input_file_fd, path.text, @@ -1059,6 +1075,7 @@ pub const ModuleLoader = struct { if (jsc_vm.isWatcherEnabled()) { if (input_file_fd != 0) { if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + should_close_input_file_fd = false; jsc_vm.bun_watcher.?.addFile( input_file_fd, path.text, -- cgit v1.2.3 From aa8b832ef61ada31176d248e716074ff22bb9dee Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 8 Jul 2023 14:26:19 -0700 Subject: Implement `process.on("beforeExit", cb)` and `process.on("exit", cb)` (#3576) * Support `process.on('beforeExit')` and `process.on('exit')` * [bun:sqlite] Always call sqlite3_close on exit * Update process.test.js --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/Process.cpp | 189 +++++++++++++++++---- src/bun.js/bindings/ZigGlobalObject.h | 2 + src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 86 +++++++--- src/bun.js/bindings/sqlite/JSSQLStatement.h | 15 -- src/bun.js/javascript.zig | 50 ++++++ src/bun.js/node/types.zig | 4 +- src/bun_js.zig | 14 +- test/js/node/process/process-exit-fixture.js | 16 ++ test/js/node/process/process-exitCode-fixture.js | 7 + test/js/node/process/process-exitCode-with-exit.js | 8 + .../node/process/process-onBeforeExit-fixture.js | 7 + .../node/process/process-onBeforeExit-keepAlive.js | 18 ++ test/js/node/process/process.test.js | 64 ++++++- 13 files changed, 402 insertions(+), 78 deletions(-) create mode 100644 test/js/node/process/process-exit-fixture.js create mode 100644 test/js/node/process/process-exitCode-fixture.js create mode 100644 test/js/node/process/process-exitCode-with-exit.js create mode 100644 test/js/node/process/process-onBeforeExit-fixture.js create mode 100644 test/js/node/process/process-onBeforeExit-keepAlive.js (limited to 'src/bun.js/javascript.zig') diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 6320deaf1..1d6b5d33a 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -42,6 +42,35 @@ static JSC_DECLARE_CUSTOM_GETTER(Process_getPID); static JSC_DECLARE_CUSTOM_GETTER(Process_getPPID); static JSC_DECLARE_HOST_FUNCTION(Process_functionCwd); +static bool processIsExiting = false; + +extern "C" uint8_t Bun__getExitCode(void*); +extern "C" uint8_t Bun__setExitCode(void*, uint8_t); +extern "C" void* Bun__getVM(); +extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); + +static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* process, int exitCode) +{ + + if (processIsExiting) + return; + processIsExiting = true; + auto& emitter = process->wrapped(); + auto& vm = globalObject->vm(); + + if (vm.hasTerminationRequest() || vm.hasExceptionsAfterHandlingTraps()) + return; + + auto event = Identifier::fromString(vm, "exit"_s); + if (!emitter.hasEventListeners(event)) { + return; + } + process->putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(true), 0); + + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + emitter.emit(event, arguments); +} static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int fd) { @@ -324,6 +353,29 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, extern "C" uint64_t Bun__readOriginTimer(void*); extern "C" double Bun__readOriginTimerStart(void*); +// https://github.com/nodejs/node/blob/1936160c31afc9780e4365de033789f39b7cbc0c/src/api/hooks.cc#L49 +extern "C" void Process__dispatchOnBeforeExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast(globalObject->processObject()); + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + process->wrapped().emit(Identifier::fromString(globalObject->vm(), "beforeExit"_s), arguments); +} + +extern "C" void Process__dispatchOnExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast(globalObject->processObject()); + dispatchExitInternal(globalObject, process, exitCode); +} + JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -336,14 +388,38 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, JSC_DEFINE_HOST_FUNCTION(Process_functionExit, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - if (callFrame->argumentCount() == 0) { - // TODO: exitCode - Bun__Process__exit(globalObject, 0); + auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); + uint8_t exitCode = 0; + JSValue arg0 = callFrame->argument(0); + if (arg0.isNumber()) { + if (!arg0.isInt32()) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int extiCode32 = arg0.toInt32(globalObject); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); + + if (extiCode32 < 0 || extiCode32 > 127) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer between 0 and 127"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + exitCode = static_cast(extiCode32); + } else if (!arg0.isUndefinedOrNull()) { + throwTypeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); } else { - Bun__Process__exit(globalObject, callFrame->argument(0).toInt32(globalObject)); + exitCode = Bun__getExitCode(Bun__getVM()); + } + + auto* zigGlobal = jsDynamicCast(globalObject); + if (UNLIKELY(!zigGlobal)) { + zigGlobal = Bun__getDefaultGlobal(); } - return JSC::JSValue::encode(JSC::jsUndefined()); + Process__dispatchOnExit(zigGlobal, exitCode); + Bun__Process__exit(zigGlobal, exitCode); } extern "C" uint64_t Bun__readOriginTimer(void*); @@ -391,18 +467,15 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, array->setIndexQuickly(vm, 1, JSC::jsNumber(nanoseconds)); return JSC::JSValue::encode(JSC::JSValue(array)); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionHRTimeBigInt); -static JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, +JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); return JSC::JSValue::encode(JSValue(JSC::JSBigInt::createFrom(globalObject, Bun__readOriginTimer(globalObject->bunVM())))); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionChdir); - -static JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, +JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); @@ -611,6 +684,46 @@ JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecArgvGetter, (JSC::JSGlobalObject * glob return ret; } +JSC_DEFINE_CUSTOM_GETTER(Process__getExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) +{ + Process* process = jsDynamicCast(JSValue::decode(thisValue)); + if (!process) { + return JSValue::encode(jsUndefined()); + } + + return JSValue::encode(jsNumber(Bun__getExitCode(jsCast(process->globalObject())->bunVM()))); +} +JSC_DEFINE_CUSTOM_SETTER(Process__setExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) +{ + Process* process = jsDynamicCast(JSValue::decode(thisValue)); + if (!process) { + return false; + } + + auto throwScope = DECLARE_THROW_SCOPE(process->vm()); + JSValue exitCode = JSValue::decode(value); + if (!exitCode.isNumber()) { + throwTypeError(lexicalGlobalObject, throwScope, "exitCode must be a number"_s); + return false; + } + + if (!exitCode.isInt32()) { + throwRangeError(lexicalGlobalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int exitCodeInt = exitCode.toInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, false); + if (exitCodeInt < 0 || exitCodeInt > 127) { + throwRangeError(lexicalGlobalObject, throwScope, "exitCode must be between 0 and 127"_s); + return false; + } + + void* ptr = jsCast(process->globalObject())->bunVM(); + Bun__setExitCode(ptr, static_cast(exitCodeInt)); + return true; +} + JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecPathGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) { JSC::JSObject* thisObject = JSValue::decode(thisValue).getObject(); @@ -677,39 +790,42 @@ void Process::finishCreation(JSC::VM& vm) vm, clientData->builtinNames().versionsPublicName(), JSC::CustomGetterSetter::create(vm, Process_getVersionsLazy, Process_setVersionsLazy), 0); // this should be transpiled out, but just incase - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "browser"_s), - JSC::JSValue(false)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "browser"_s), + JSC::JSValue(false), PropertyAttribute::DontEnum | 0); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "exitCode"_s), - JSC::JSValue(JSC::jsNumber(0))); + this->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "exitCode"_s), + JSC::CustomGetterSetter::create(vm, + Process__getExitCode, + Process__setExitCode), + 0); - this->putDirect(this->vm(), clientData->builtinNames().versionPublicName(), - JSC::jsString(this->vm(), makeString("v", REPORTED_NODE_VERSION))); + this->putDirect(vm, clientData->builtinNames().versionPublicName(), + JSC::jsString(vm, makeString("v", REPORTED_NODE_VERSION))); // this gives some way of identifying at runtime whether the SSR is happening in node or not. // this should probably be renamed to what the name of the bundler is, instead of "notNodeJS" // but it must be something that won't evaluate to truthy in Node.js - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "isBun"_s), JSC::JSValue(true)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "isBun"_s), JSC::JSValue(true)); #if defined(__APPLE__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("darwin"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("darwin"))); #else - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("linux"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("linux"))); #endif #if defined(__x86_64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x64"))); #elif defined(__i386__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x86"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x86"))); #elif defined(__arm__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm"))); #elif defined(__aarch64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm64"))); #endif JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, @@ -719,7 +835,7 @@ void Process::finishCreation(JSC::VM& vm) MAKE_STATIC_STRING_IMPL("bigint"), Process_functionHRTimeBigInt, ImplementationVisibility::Public); hrtime->putDirect(vm, JSC::Identifier::fromString(vm, "bigint"_s), hrtimeBigInt); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "hrtime"_s), hrtime); + this->putDirect(vm, JSC::Identifier::fromString(vm, "hrtime"_s), hrtime); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "release"_s)), JSC::CustomGetterSetter::create(vm, Process_getterRelease, Process_setterRelease), 0); @@ -733,7 +849,10 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "stdin"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyStdinGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "abort"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), + 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); + + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "argv0"_s)), @@ -745,13 +864,13 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "execArgv"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyExecArgvGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "uptime"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "uptime"_s), 0, Process_functionUptime, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "umask"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "umask"_s), 1, Process_functionUmask, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "binding"_s), + this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(vm, "binding"_s), processObjectInternalsBindingCodeGenerator(vm), 0); @@ -788,7 +907,7 @@ void Process::finishCreation(JSC::VM& vm) config->putDirect(vm, JSC::Identifier::fromString(vm, "variables"_s), variables, 0); this->putDirect(vm, JSC::Identifier::fromString(vm, "config"_s), config, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "emitWarning"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "emitWarning"_s), 1, Process_emitWarning, ImplementationVisibility::Public, NoIntrinsic, 0); JSC::JSFunction* requireDotMainFunction = JSFunction::create( diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index da6ba92a0..f44212da1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -270,6 +270,8 @@ public: JSWeakMap* vmModuleContextMap() { return m_vmModuleContextMap.getInitializedOnMainThread(this); } + bool hasProcessObject() const { return m_processObject.isInitialized(); } + JSC::JSObject* processObject() { return m_processObject.getInitializedOnMainThread(this); diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index a6855fd19..61ac91ba7 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -107,6 +107,50 @@ static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementDeserialize); return JSValue::encode(jsUndefined()); \ } +class VersionSqlite3 { +public: + explicit VersionSqlite3(sqlite3* db) + : db(db) + , version(0) + { + } + sqlite3* db; + std::atomic version; +}; + +class SQLiteSingleton { +public: + Vector databases; + Vector> schema_versions; +}; + +static SQLiteSingleton* _instance = nullptr; + +static Vector& databases() +{ + if (!_instance) { + _instance = new SQLiteSingleton(); + _instance->databases = Vector(); + _instance->databases.reserveInitialCapacity(4); + _instance->schema_versions = Vector>(); + } + + return _instance->databases; +} + +extern "C" void Bun__closeAllSQLiteDatabasesForTermination() +{ + if (!_instance) { + return; + } + auto& dbs = _instance->databases; + + for (auto& db : dbs) { + if (db->db) + sqlite3_close_v2(db->db); + } +} + namespace WebCore { using namespace JSC; @@ -272,10 +316,6 @@ void JSSQLStatement::destroy(JSC::JSCell* cell) void JSSQLStatementConstructor::destroy(JSC::JSCell* cell) { - JSSQLStatementConstructor* thisObject = static_cast(cell); - for (auto version_db : thisObject->databases) { - delete version_db; - } } static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3_stmt* stmt, int i, JSC::JSValue value, JSC::ThrowScope& scope, bool clone) @@ -547,8 +587,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexic return JSValue::encode(JSC::jsUndefined()); } - auto count = thisObject->databases.size(); - thisObject->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -565,12 +605,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexical } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -606,7 +646,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } @@ -620,7 +660,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje auto extensionString = extension.toWTFString(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -661,11 +701,11 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l } int32_t handle = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (thisObject->databases.size() < handle) { + if (databases().size() < handle) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -724,7 +764,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l rc = sqlite3_step(statement); if (!sqlite3_stmt_readonly(statement)) { - thisObject->databases[handle]->version++; + databases()[handle]->version++; } while (rc == SQLITE_ROW) { @@ -765,12 +805,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction, (JSC::JSGlobalOb int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -803,12 +843,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO } int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (!db) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Cannot use a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -848,7 +888,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO auto* structure = JSSQLStatement::createStructure(vm, lexicalGlobalObject, lexicalGlobalObject->objectPrototype()); // auto* structure = JSSQLStatement::createStructure(vm, globalObject(), thisObject->getDirect(vm, vm.propertyNames->prototype)); JSSQLStatement* sqlStatement = JSSQLStatement::create( - structure, reinterpret_cast(lexicalGlobalObject), statement, thisObject->databases[handle]); + structure, reinterpret_cast(lexicalGlobalObject), statement, databases()[handle]); if (bindings.isObject()) { auto* castedThis = sqlStatement; DO_REBIND(bindings) @@ -924,8 +964,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction, (JSC::JSGlobalObje status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); assert(status == SQLITE_OK); - auto count = constructor->databases.size(); - constructor->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -956,12 +996,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj int dbIndex = dbNumber.toInt32(lexicalGlobalObject); - if (dbIndex < 0 || dbIndex >= constructor->databases.size()) { + if (dbIndex < 0 || dbIndex >= databases().size()) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(jsUndefined()); } - sqlite3* db = constructor->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; // no-op if already closed if (!db) { return JSValue::encode(jsUndefined()); @@ -973,7 +1013,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj return JSValue::encode(jsUndefined()); } - constructor->databases[dbIndex]->db = nullptr; + databases()[dbIndex]->db = nullptr; return JSValue::encode(jsUndefined()); } diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.h b/src/bun.js/bindings/sqlite/JSSQLStatement.h index e63b99fbb..8566fcdd9 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.h +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.h @@ -47,17 +47,6 @@ namespace WebCore { -class VersionSqlite3 { -public: - explicit VersionSqlite3(sqlite3* db) - : db(db) - , version(0) - { - } - sqlite3* db; - std::atomic version; -}; - class JSSQLStatementConstructor final : public JSC::JSFunction { public: using Base = JSC::JSFunction; @@ -82,13 +71,9 @@ public: return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - Vector databases; - Vector> schema_versions; - private: JSSQLStatementConstructor(JSC::VM& vm, NativeExecutable* native, JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, native, globalObject, structure) - , databases() { } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index b696c6cf2..7d2435823 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -334,6 +334,33 @@ pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSG jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; } +pub const ExitHandler = struct { + exit_code: u8 = 0, + + pub export fn Bun__getExitCode(vm: *VirtualMachine) u8 { + return vm.exit_handler.exit_code; + } + + pub export fn Bun__setExitCode(vm: *VirtualMachine, code: u8) void { + vm.exit_handler.exit_code = code; + } + + extern fn Process__dispatchOnBeforeExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Process__dispatchOnExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Bun__closeAllSQLiteDatabasesForTermination() void; + + pub fn dispatchOnExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnExit(vm.global, this.exit_code); + Bun__closeAllSQLiteDatabasesForTermination(); + } + + pub fn dispatchOnBeforeExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnBeforeExit(vm.global, this.exit_code); + } +}; + /// TODO: rename this to ScriptExecutionContext /// This is the shared global state for a single JS instance execution /// Today, Bun is one VM per thread, so the name "VirtualMachine" sort of makes sense @@ -376,6 +403,7 @@ pub const VirtualMachine = struct { plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, last_reported_error_for_dedupe: JSValue = .zero, + exit_handler: ExitHandler = .{}, /// Do not access this field directly /// It exists in the VirtualMachine struct so that @@ -620,7 +648,29 @@ pub const VirtualMachine = struct { loop.run(); } + pub fn onBeforeExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnBeforeExit(); + var dispatch = false; + while (true) { + while (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) : (dispatch = true) { + this.tick(); + this.eventLoop().autoTickActive(); + } + + if (dispatch) { + this.exit_handler.dispatchOnBeforeExit(); + dispatch = false; + + if (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) continue; + } + + break; + } + } + pub fn onExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnExit(); + var rare_data = this.rare_data orelse return; var hook = rare_data.cleanup_hook orelse return; hook.execute(); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 96d04636e..553b292d6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -2202,7 +2202,9 @@ pub const Process = struct { } } - pub fn exit(_: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + pub fn exit(globalObject: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + globalObject.bunVM().onExit(); + std.os.exit(@truncate(u8, @intCast(u32, @max(code, 0)))); } diff --git a/src/bun_js.zig b/src/bun_js.zig index 63ffe0611..72b7f8de9 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -248,6 +248,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -279,6 +281,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -315,6 +319,8 @@ pub const Run = struct { vm.eventLoop().autoTickActive(); } + vm.onBeforeExit(); + if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { prev_promise = this.vm.pending_internal_promise; vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); @@ -332,6 +338,8 @@ pub const Run = struct { vm.tick(); vm.eventLoop().autoTickActive(); } + + vm.onBeforeExit(); } if (vm.log.msgs.items.len > 0) { @@ -347,10 +355,14 @@ pub const Run = struct { vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; vm.global.handleRejectedPromises(); + if (this.any_unhandled and this.vm.exit_handler.exit_code == 0) { + this.vm.exit_handler.exit_code = 1; + } + const exit_code = this.vm.exit_handler.exit_code; vm.onExit(); if (!JSC.is_bindgen) JSC.napi.fixDeadCodeElimination(); - Global.exit(@intFromBool(this.any_unhandled)); + Global.exit(exit_code); } }; diff --git a/test/js/node/process/process-exit-fixture.js b/test/js/node/process/process-exit-fixture.js new file mode 100644 index 000000000..c5a492285 --- /dev/null +++ b/test/js/node/process/process-exit-fixture.js @@ -0,0 +1,16 @@ +process.on("beforeExit", () => { + throw new Error("process.on('beforeExit') called"); +}); + +if (process._exiting) { + throw new Error("process._exiting should be undefined"); +} + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("PASS"); +}); + +process.exit(0); diff --git a/test/js/node/process/process-exitCode-fixture.js b/test/js/node/process/process-exitCode-fixture.js new file mode 100644 index 000000000..2d5182d93 --- /dev/null +++ b/test/js/node/process/process-exitCode-fixture.js @@ -0,0 +1,7 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); diff --git a/test/js/node/process/process-exitCode-with-exit.js b/test/js/node/process/process-exitCode-with-exit.js new file mode 100644 index 000000000..610975bc2 --- /dev/null +++ b/test/js/node/process/process-exitCode-with-exit.js @@ -0,0 +1,8 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); +process.exit(); diff --git a/test/js/node/process/process-onBeforeExit-fixture.js b/test/js/node/process/process-onBeforeExit-fixture.js new file mode 100644 index 000000000..8cbdcebf0 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-fixture.js @@ -0,0 +1,7 @@ +process.on("beforeExit", () => { + console.log("beforeExit"); +}); + +process.on("exit", () => { + console.log("exit"); +}); diff --git a/test/js/node/process/process-onBeforeExit-keepAlive.js b/test/js/node/process/process-onBeforeExit-keepAlive.js new file mode 100644 index 000000000..45b20b763 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-keepAlive.js @@ -0,0 +1,18 @@ +let counter = 0; +process.on("beforeExit", () => { + if (process._exiting) { + throw new Error("process._exiting should be undefined"); + } + + console.log("beforeExit:", counter); + if (!counter++) { + setTimeout(() => {}, 1); + } +}); + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("exit:", counter); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 61ac3839c..c4701f664 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1,8 +1,8 @@ -import { resolveSync, which } from "bun"; +import { resolveSync, spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, realpathSync } from "fs"; -import { bunExe } from "harness"; -import { basename, resolve } from "path"; +import { bunEnv, bunExe } from "harness"; +import { basename, join, resolve } from "path"; it("process", () => { // this property isn't implemented yet but it should at least return a string @@ -233,3 +233,61 @@ it("process.argv in testing", () => { // assert we aren't creating a new process.argv each call expect(process.argv).toBe(process.argv); }); + +describe("process.exitCode", () => { + it("validates int", () => { + expect(() => (process.exitCode = "potato")).toThrow("exitCode must be a number"); + expect(() => (process.exitCode = 1.2)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = NaN)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -1)).toThrow("exitCode must be between 0 and 127"); + }); + + it("works with implicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-with-exit.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-fixture.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); +}); + +it("process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("PASS"); +}); + +describe("process.onBeforeExit", () => { + it("emitted", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit\nexit"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-keepAlive.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit: 0\nbeforeExit: 1\nexit: 2"); + }); +}); -- cgit v1.2.3