diff options
Diffstat (limited to 'src/bun.js')
22 files changed, 3168 insertions, 59 deletions
diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h index d642ae40f..f38c22831 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h @@ -2,7 +2,8 @@ std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBlob; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBlobConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForCryptoHasher; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForCryptoHasherConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForDirent; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForDirentConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpect; -std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFileSystemRouter; +std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectAny; +std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFileSystemRouter; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFileSystemRouterConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForListener; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD4; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD4Constructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD5; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h index 93cb9bf5e..80ada929c 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h @@ -2,7 +2,8 @@ std::unique_ptr<IsoSubspace> m_subspaceForBlob; std::unique_ptr<IsoSubspace> m_subspaceForBlobConstructor;std::unique_ptr<IsoSubspace> m_subspaceForCryptoHasher; std::unique_ptr<IsoSubspace> m_subspaceForCryptoHasherConstructor;std::unique_ptr<IsoSubspace> m_subspaceForDirent; std::unique_ptr<IsoSubspace> m_subspaceForDirentConstructor;std::unique_ptr<IsoSubspace> m_subspaceForExpect; -std::unique_ptr<IsoSubspace> m_subspaceForExpectConstructor;std::unique_ptr<IsoSubspace> m_subspaceForFileSystemRouter; +std::unique_ptr<IsoSubspace> m_subspaceForExpectConstructor;std::unique_ptr<IsoSubspace> m_subspaceForExpectAny; +std::unique_ptr<IsoSubspace> m_subspaceForFileSystemRouter; std::unique_ptr<IsoSubspace> m_subspaceForFileSystemRouterConstructor;std::unique_ptr<IsoSubspace> m_subspaceForListener; std::unique_ptr<IsoSubspace> m_subspaceForMD4; std::unique_ptr<IsoSubspace> m_subspaceForMD4Constructor;std::unique_ptr<IsoSubspace> m_subspaceForMD5; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h index ab75a9949..ae3e4233a 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h @@ -22,6 +22,12 @@ JSC::Structure* JSExpectStructure() { return m_JSExpect.getInitializedOnMainThre JSC::LazyClassStructure m_JSExpect; bool hasJSExpectSetterValue { false }; mutable JSC::WriteBarrier<JSC::Unknown> m_JSExpectSetterValue; +JSC::Structure* JSExpectAnyStructure() { return m_JSExpectAny.getInitializedOnMainThread(this); } + JSC::JSObject* JSExpectAnyConstructor() { return m_JSExpectAny.constructorInitializedOnMainThread(this); } + JSC::JSValue JSExpectAnyPrototype() { return m_JSExpectAny.prototypeInitializedOnMainThread(this); } + JSC::LazyClassStructure m_JSExpectAny; + bool hasJSExpectAnySetterValue { false }; + mutable JSC::WriteBarrier<JSC::Unknown> m_JSExpectAnySetterValue; 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 aed941a2e..a90753aa8 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h @@ -23,6 +23,12 @@ void GlobalObject::initGeneratedLazyClasses() { init.setStructure(WebCore::JSExpect::createStructure(init.vm, init.global, init.prototype)); init.setConstructor(WebCore::JSExpect::createConstructor(init.vm, init.global, init.prototype)); }); + m_JSExpectAny.initLater( + [](LazyClassStructure::Initializer& init) { + init.setPrototype(WebCore::JSExpectAny::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global))); + init.setStructure(WebCore::JSExpectAny::createStructure(init.vm, init.global, init.prototype)); + + }); m_JSFileSystemRouter.initLater( [](LazyClassStructure::Initializer& init) { init.setPrototype(WebCore::JSFileSystemRouter::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global))); @@ -163,6 +169,7 @@ void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& thisObject->m_JSCryptoHasher.visit(visitor); visitor.append(thisObject->m_JSCryptoHasherSetterValue); thisObject->m_JSDirent.visit(visitor); visitor.append(thisObject->m_JSDirentSetterValue); thisObject->m_JSExpect.visit(visitor); visitor.append(thisObject->m_JSExpectSetterValue); + thisObject->m_JSExpectAny.visit(visitor); visitor.append(thisObject->m_JSExpectAnySetterValue); 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 716f683e5..a388490b4 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -2342,6 +2342,175 @@ void JSExpect::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor) } DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpect); +class JSExpectAnyPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSExpectAnyPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSExpectAnyPrototype* ptr = new (NotNull, JSC::allocateCell<JSExpectAnyPrototype>(vm)) JSExpectAnyPrototype(vm, globalObject, structure); + ptr->finishCreation(vm, globalObject); + return ptr; + } + + DECLARE_INFO; + template<typename CellType, JSC::SubspaceAccess> + 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: + JSExpectAnyPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); +}; + +extern "C" void ExpectAnyClass__finalize(void*); +extern "C" JSC_DECLARE_HOST_FUNCTION(ExpectAnyClass__call); + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSExpectAnyPrototype, JSExpectAnyPrototype::Base); + +static const HashTableValue JSExpectAnyPrototypeTableValues[] = {}; + +const ClassInfo JSExpectAnyPrototype::s_info = { "ExpectAny"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSExpectAnyPrototype) }; + +extern "C" void ExpectAnyPrototype__constructorValueSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast<JSExpectAny*>(JSValue::decode(thisValue)); + thisObject->m_constructorValue.set(vm, thisObject, JSValue::decode(value)); +} + +extern "C" EncodedJSValue ExpectAnyPrototype__constructorValueGetCachedValue(JSC::EncodedJSValue thisValue) +{ + auto* thisObject = jsCast<JSExpectAny*>(JSValue::decode(thisValue)); + return JSValue::encode(thisObject->m_constructorValue.get()); +} + +void JSExpectAnyPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSExpectAny::~JSExpectAny() +{ + if (m_ctx) { + ExpectAnyClass__finalize(m_ctx); + } +} +void JSExpectAny::destroy(JSCell* cell) +{ + static_cast<JSExpectAny*>(cell)->JSExpectAny::~JSExpectAny(); +} + +const ClassInfo JSExpectAny::s_info = { "ExpectAny"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSExpectAny) }; + +void JSExpectAny::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSExpectAny* JSExpectAny::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) +{ + JSExpectAny* ptr = new (NotNull, JSC::allocateCell<JSExpectAny>(vm)) JSExpectAny(vm, structure, ctx); + ptr->finishCreation(vm); + return ptr; +} + +extern "C" void* ExpectAny__fromJS(JSC::EncodedJSValue value) +{ + JSC::JSValue decodedValue = JSC::JSValue::decode(value); + if (decodedValue.isEmpty() || !decodedValue.isCell()) + return nullptr; + + JSC::JSCell* cell = decodedValue.asCell(); + JSExpectAny* object = JSC::jsDynamicCast<JSExpectAny*>(cell); + + if (!object) + return nullptr; + + return object->wrapped(); +} + +extern "C" bool ExpectAny__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr) +{ + JSExpectAny* object = JSC::jsDynamicCast<JSExpectAny*>(JSValue::decode(value)); + if (!object) + return false; + + object->m_ctx = ptr; + return true; +} + +extern "C" const size_t ExpectAny__ptrOffset = JSExpectAny::offsetOfWrapped(); + +void JSExpectAny::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast<JSExpectAny*>(cell); + if (void* wrapped = thisObject->wrapped()) { + // if (thisObject->scriptExecutionContext()) + // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string()); + } + Base::analyzeHeap(cell, analyzer); +} + +JSObject* JSExpectAny::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) +{ + return JSExpectAnyPrototype::create(vm, globalObject, JSExpectAnyPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +extern "C" EncodedJSValue ExpectAny__create(Zig::GlobalObject* globalObject, void* ptr) +{ + auto& vm = globalObject->vm(); + JSC::Structure* structure = globalObject->JSExpectAnyStructure(); + JSExpectAny* instance = JSExpectAny::create(vm, globalObject, structure, ptr); + + return JSValue::encode(instance); +} + +template<typename Visitor> +void JSExpectAny::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSExpectAny* thisObject = jsCast<JSExpectAny*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_constructorValue); +} + +DEFINE_VISIT_CHILDREN(JSExpectAny); + +template<typename Visitor> +void JSExpectAny::visitAdditionalChildren(Visitor& visitor) +{ + JSExpectAny* thisObject = this; + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + visitor.append(thisObject->m_constructorValue); + + ; +} + +DEFINE_VISIT_ADDITIONAL_CHILDREN(JSExpectAny); + +template<typename Visitor> +void JSExpectAny::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor) +{ + JSExpectAny* thisObject = jsCast<JSExpectAny*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + thisObject->visitAdditionalChildren<Visitor>(visitor); +} + +DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpectAny); class JSFileSystemRouterPrototype final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index acf1dc140..76420193a 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -236,6 +236,62 @@ public: mutable JSC::WriteBarrier<JSC::Unknown> m_resultValue; }; +class JSExpectAny final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static JSExpectAny* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); + + DECLARE_EXPORT_INFO; + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl<JSExpectAny, WebCore::UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForExpectAny.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForExpectAny = std::forward<decltype(space)>(space); }, + [](auto& spaces) { return spaces.m_subspaceForExpectAny.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForExpectAny = std::forward<decltype(space)>(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<JSC::JSType>(0b11101110), StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); + ; + + ~JSExpectAny(); + + void* wrapped() const { return m_ctx; } + + void detach() + { + m_ctx = nullptr; + } + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSExpectAny, m_ctx); } + + void* m_ctx { nullptr }; + + JSExpectAny(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + : Base(vm, structure) + { + m_ctx = sinkPtr; + } + + void finishCreation(JSC::VM&); + + DECLARE_VISIT_CHILDREN; + template<typename Visitor> void visitAdditionalChildren(Visitor&); + DECLARE_VISIT_OUTPUT_CONSTRAINTS; + + mutable JSC::WriteBarrier<JSC::Unknown> m_constructorValue; +}; + class JSFileSystemRouter final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 5c9cba7f0..d431a287f 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3227,13 +3227,21 @@ void JSC__VM__releaseWeakRefs(JSC__VM* arg0) static auto function_string_view = MAKE_STATIC_STRING_IMPL("Function"); void JSC__JSValue__getClassName(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, ZigString* arg2) { - JSC::JSCell* cell = JSC::JSValue::decode(JSValue0).asCell(); + JSValue value = JSValue::decode(JSValue0); + JSC::JSCell* cell = value.asCell(); if (cell == nullptr) { arg2->len = 0; return; } - const char* ptr = cell->className(); + JSObject* obj = value.toObject(arg1); + StringView calculated = StringView(JSObject::calculatedClassName(obj)); + if (calculated.length() > 0) { + *arg2 = Zig::toZigString(calculated); + return; + } + + const char* ptr = cell->classInfo()->className; auto view = WTF::StringView(ptr, strlen(ptr)); // Fallback to .name if className is empty @@ -3784,6 +3792,12 @@ void JSC__JSValue__forEachPropertyOrdered(JSC__JSValue JSValue0, JSC__JSGlobalOb } } +bool JSC__JSValue__isConstructor(JSC__JSValue JSValue0) +{ + JSValue value = JSValue::decode(JSValue0); + return value.isConstructor(); +} + extern "C" JSC__JSValue JSC__JSValue__createRopeString(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject) { return JSValue::encode(JSC::jsString(globalObject, JSC::JSValue::decode(JSValue0).toString(globalObject), JSC::JSValue::decode(JSValue1).toString(globalObject))); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 40fc43091..8a63058a5 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -17,6 +17,8 @@ const JSC = @import("bun").JSC; const Shimmer = JSC.Shimmer; const FFI = @import("./FFI.zig"); const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator; +const MutableString = bun.MutableString; +const JestPrettyFormat = @import("../test/pretty_format.zig").JestPrettyFormat; pub const JSObject = extern struct { pub const shim = Shimmer("JSC", "JSObject", @This()); @@ -2442,6 +2444,18 @@ pub const JSGlobalObject = extern struct { this.vm().throwError(this, this.createErrorInstance(fmt, args)); } + pub fn throwPretty( + this: *JSGlobalObject, + comptime fmt: string, + args: anytype, + ) void { + if (Output.enable_ansi_colors) { + this.vm().throwError(this, this.createErrorInstance(Output.prettyFmt(fmt, true), args)); + } else { + this.vm().throwError(this, this.createErrorInstance(Output.prettyFmt(fmt, false), args)); + } + } + pub fn queueMicrotask( this: *JSGlobalObject, function: JSValue, @@ -3175,6 +3189,93 @@ pub const JSValue = enum(JSValueReprInt) { return JSBuffer__bufferFromLength(globalObject, @intCast(i64, len)); } + pub fn jestSnapshotPrettyFormat(this: JSValue, out: *MutableString, globalObject: *JSGlobalObject) !void { + var buffered_writer = MutableString.BufferedWriter{ .context = out }; + var writer = buffered_writer.writer(); + const Writer = @TypeOf(writer); + + const fmt_options = JestPrettyFormat.FormatOptions{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .quote_strings = true, + }; + + JestPrettyFormat.format( + .Debug, + globalObject, + @ptrCast([*]const JSValue, &this), + 1, + Writer, + Writer, + writer, + fmt_options, + ); + + try buffered_writer.flush(); + + const count: usize = brk: { + var total: usize = 0; + var remain = out.list.items; + while (strings.indexOfChar(remain, '`')) |i| { + total += 1; + remain = remain[i + 1 ..]; + } + break :brk total; + }; + + if (count > 0) { + var result = try out.allocator.alloc(u8, count + out.list.items.len); + var input = out.list.items; + + var input_i: usize = 0; + var result_i: usize = 0; + while (strings.indexOfChar(input[input_i..], '`')) |i| { + bun.copy(u8, result[result_i..], input[input_i .. input_i + i]); + result_i += i; + result[result_i] = '\\'; + result[result_i + 1] = '`'; + result_i += 2; + input_i += i + 1; + } + + if (result_i != result.len) { + bun.copy(u8, result[result_i..], input[input_i..]); + } + + out.deinit(); + out.list.items = result; + out.list.capacity = result.len; + } + } + + pub fn jestPrettyFormat(this: JSValue, out: *MutableString, globalObject: *JSGlobalObject) !void { + var buffered_writer = MutableString.BufferedWriter{ .context = out }; + var writer = buffered_writer.writer(); + const Writer = @TypeOf(writer); + + const fmt_options = JSC.ZigConsoleClient.FormatOptions{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .ordered_properties = true, + .quote_strings = true, + }; + + JSC.ZigConsoleClient.format( + .Debug, + globalObject, + @ptrCast([*]const JSValue, &this), + 1, + Writer, + Writer, + writer, + fmt_options, + ); + + try buffered_writer.flush(); + } + extern fn JSBuffer__bufferFromLength(*JSGlobalObject, i64) JSValue; /// Must come from globally-allocated memory if allocator is not null @@ -3496,6 +3597,11 @@ pub const JSValue = enum(JSValueReprInt) { return cppFn("isClass", .{ this, global }); } + pub fn isConstructor(this: JSValue) bool { + if (!this.isCell()) return false; + return cppFn("isConstructor", .{this}); + } + pub fn getNameProperty(this: JSValue, global: *JSGlobalObject, ret: *ZigString) void { if (this.isEmptyOrUndefinedOrNull()) { return; @@ -4015,6 +4121,7 @@ pub const JSValue = enum(JSValueReprInt) { "toWTFString", "toZigException", "toZigString", + "isConstructor", }; }; diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index f9a9a3467..704dd6ae0 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -1426,6 +1426,7 @@ pub const ZigConsoleClient = struct { JSValue.JSType.Float64Array, JSValue.JSType.BigInt64Array, JSValue.JSType.BigUint64Array, + .DataView, => .TypedArray, .HeapBigInt => .BigInt, @@ -1598,11 +1599,11 @@ pub const ZigConsoleClient = struct { comptime Writer: type, writer: Writer, ) !void { - const indent = @min(this.indent, 8); - var buf = [_]u8{' '} ** 32; + const indent = @min(this.indent, 32); + var buf = [_]u8{' '} ** 64; var total_remain: usize = indent; while (total_remain > 0) { - const written = @min(16, total_remain); + const written = @min(32, total_remain); try writer.writeAll(buf[0 .. written * 2]); total_remain -|= written; } @@ -1752,14 +1753,14 @@ pub const ZigConsoleClient = struct { this.addForNewLine(key.len + 1); writer.print( - comptime Output.prettyFmt("{}<d>:<r> ", enable_ansi_colors), + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), .{key}, ); } else if (key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned())) { this.addForNewLine(key.len + 1); writer.print( - comptime Output.prettyFmt("{}<d>:<r> ", enable_ansi_colors), + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), .{key}, ); } else if (key.is16Bit()) { @@ -1941,6 +1942,30 @@ pub const ZigConsoleClient = struct { writer.print(comptime Output.prettyFmt("<r><yellow>{s}n<r>", enable_ansi_colors), .{out_str}); }, .Double => { + if (value.isCell()) { + var number_name = ZigString.Empty; + value.getClassName(this.globalThis, &number_name); + + var number_value = ZigString.Empty; + value.toZigString(&number_value, this.globalThis); + + if (!strings.eqlComptime(number_name.slice(), "Number")) { + this.addForNewLine(number_name.len + number_value.len + "[Number ():]".len); + writer.print(comptime Output.prettyFmt("<r><yellow>[Number ({s}): {s}]<r>", enable_ansi_colors), .{ + number_name, + number_value, + }); + return; + } + + this.addForNewLine(number_name.len + number_value.len + 4); + writer.print(comptime Output.prettyFmt("<r><yellow>[{s}: {s}]<r>", enable_ansi_colors), .{ + number_name, + number_value, + }); + return; + } + const num = value.asNumber(); if (std.math.isPositiveInf(num)) { @@ -1981,7 +2006,6 @@ pub const ZigConsoleClient = struct { value, null, null, - Writer, writer_, enable_ansi_colors, @@ -2091,13 +2115,13 @@ pub const ZigConsoleClient = struct { }, .Private => { if (value.as(JSC.WebCore.Response)) |response| { - response.writeFormat(this, writer_, enable_ansi_colors) catch {}; + response.writeFormat(ZigConsoleClient.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.WebCore.Request)) |request| { - request.writeFormat(this, writer_, enable_ansi_colors) catch {}; + request.writeFormat(ZigConsoleClient.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.WebCore.Blob)) |blob| { - blob.writeFormat(this, writer_, enable_ansi_colors) catch {}; + blob.writeFormat(ZigConsoleClient.Formatter, this, writer_, enable_ansi_colors) catch {}; return; } else if (value.as(JSC.DOMFormData) != null) { const toJSONFunction = value.get(this.globalThis, "toJSON").?; @@ -2162,6 +2186,7 @@ pub const ZigConsoleClient = struct { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; } + writer.writeAll("Promise { " ++ comptime Output.prettyFmt("<r><cyan>", enable_ansi_colors)); switch (JSPromise.status(@ptrCast(*JSPromise, value.asObjectRef().?), this.globalThis.vm())) { @@ -2179,16 +2204,36 @@ pub const ZigConsoleClient = struct { writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors) ++ " }"); }, .Boolean => { + if (value.isCell()) { + var bool_name = ZigString.Empty; + value.getClassName(this.globalThis, &bool_name); + var bool_value = ZigString.Empty; + value.toZigString(&bool_value, this.globalThis); + + if (!strings.eqlComptime(bool_name.slice(), "Boolean")) { + this.addForNewLine(bool_value.len + bool_name.len + "[Boolean (): ]".len); + writer.print(comptime Output.prettyFmt("<r><yellow>[Boolean ({s}): {s}]<r>", enable_ansi_colors), .{ + bool_name, + bool_value, + }); + return; + } + this.addForNewLine(bool_value.len + "[Boolean: ]".len); + writer.print(comptime Output.prettyFmt("<r><yellow>[Boolean: {s}]<r>", enable_ansi_colors), .{bool_value}); + return; + } if (value.toBoolean()) { - this.addForNewLine(5); + this.addForNewLine(4); writer.writeAll(comptime Output.prettyFmt("<r><yellow>true<r>", enable_ansi_colors)); } else { - this.addForNewLine(4); + this.addForNewLine(5); writer.writeAll(comptime Output.prettyFmt("<r><yellow>false<r>", enable_ansi_colors)); } }, .GlobalObject => { - writer.writeAll(comptime Output.prettyFmt("<cyan>[this.globalThis]<r>", enable_ansi_colors)); + const fmt = "[this.globalThis]"; + this.addForNewLine(fmt.len); + writer.writeAll(comptime Output.prettyFmt("<cyan>" ++ fmt ++ "<r>", enable_ansi_colors)); }, .Map => { const length_value = value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); @@ -2198,11 +2243,13 @@ pub const ZigConsoleClient = struct { this.quote_strings = true; defer this.quote_strings = prev_quote_strings; + const map_name = if (value.jsType() == .JSWeakMap) "WeakMap" else "Map"; + if (length == 0) { - return writer.writeAll("Map {}"); + return writer.print("{s} {{}}", .{map_name}); } - writer.print("Map({d}) {{\n", .{length}); + writer.print("{s}({d}) {{\n", .{ map_name, length }); { this.indent += 1; defer this.indent -|= 1; @@ -2224,10 +2271,14 @@ pub const ZigConsoleClient = struct { defer this.quote_strings = prev_quote_strings; this.writeIndent(Writer, writer_) catch {}; + + const set_name = if (value.jsType() == .JSWeakSet) "WeakSet" else "Set"; + if (length == 0) { - return writer.writeAll("Set {}"); + return writer.print("{s} {{}}", .{set_name}); } - writer.print("Set({d}) {{\n", .{length}); + + writer.print("{s}({d}) {{\n", .{ set_name, length }); { this.indent += 1; defer this.indent -|= 1; @@ -2590,9 +2641,6 @@ pub const ZigConsoleClient = struct { } else { if (iter.always_newline) { this.indent -|= 1; - } - - if (iter.always_newline) { writer.writeAll("\n"); this.writeIndent(Writer, writer_) catch {}; writer.writeAll("}"); @@ -2605,11 +2653,11 @@ pub const ZigConsoleClient = struct { }, .TypedArray => { const arrayBuffer = value.asArrayBuffer(this.globalThis).?; - const slice = arrayBuffer.byteSlice(); - writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); + writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); writer.print("({d}) [ ", .{arrayBuffer.len}); + if (slice.len > 0) { switch (jsType) { .Int8Array => this.writeTypedArray( @@ -2668,13 +2716,15 @@ pub const ZigConsoleClient = struct { @alignCast(std.meta.alignment([]i64), std.mem.bytesAsSlice(i64, slice)), enable_ansi_colors, ), - .BigUint64Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - u64, - @alignCast(std.meta.alignment([]u64), std.mem.bytesAsSlice(u64, slice)), - enable_ansi_colors, - ), + .BigUint64Array => { + this.writeTypedArray( + *@TypeOf(writer), + &writer, + u64, + @alignCast(std.meta.alignment([]u64), std.mem.bytesAsSlice(u64, slice)), + enable_ansi_colors, + ); + }, // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer else => this.writeTypedArray(*@TypeOf(writer), &writer, u8, slice, enable_ansi_colors), @@ -2687,7 +2737,7 @@ pub const ZigConsoleClient = struct { } } - fn writeTypedArray(this: *ZigConsoleClient.Formatter, comptime Writer: type, writer: Writer, comptime Number: type, slice: []const Number, comptime enable_ansi_colors: bool) void { + fn writeTypedArray(this: *ZigConsoleClient.Formatter, comptime WriterWrapped: type, writer: WriterWrapped, comptime Number: type, slice: []const Number, comptime enable_ansi_colors: bool) void { const fmt_ = if (Number == i64 or Number == u64) "<r><yellow>{d}n<r>" else diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 015c740ec..f786e3399 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -616,6 +616,87 @@ pub const JSExpect = struct { } } }; +pub const JSExpectAny = struct { + const ExpectAny = Classes.ExpectAny; + const GetterType = fn (*ExpectAny, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const GetterTypeWithThisValue = fn (*ExpectAny, JSC.JSValue, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const SetterType = fn (*ExpectAny, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const SetterTypeWithThisValue = fn (*ExpectAny, JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const CallbackType = fn (*ExpectAny, *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) ?*ExpectAny { + JSC.markBinding(@src()); + return ExpectAny__fromJS(value); + } + + extern fn ExpectAnyPrototype__constructorValueSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; + + extern fn ExpectAnyPrototype__constructorValueGetCachedValue(JSC.JSValue) JSC.JSValue; + + /// `ExpectAny.constructorValue` setter + /// This value will be visited by the garbage collector. + pub fn constructorValueSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + JSC.markBinding(@src()); + ExpectAnyPrototype__constructorValueSetCachedValue(thisValue, globalObject, value); + } + + /// `ExpectAny.constructorValue` getter + /// This value will be visited by the garbage collector. + pub fn constructorValueGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { + JSC.markBinding(@src()); + const result = ExpectAnyPrototype__constructorValueGetCachedValue(thisValue); + if (result == .zero) + return null; + + return result; + } + + /// Create a new instance of ExpectAny + pub fn toJS(this: *ExpectAny, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(@src()); + if (comptime Environment.allow_assert) { + const value__ = ExpectAny__create(globalObject, this); + std.debug.assert(value__.as(ExpectAny).? == this); // If this fails, likely a C ABI issue. + return value__; + } else { + return ExpectAny__create(globalObject, this); + } + } + + /// Modify the internal ptr to point to a new instance of ExpectAny. + pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*ExpectAny) bool { + JSC.markBinding(@src()); + return ExpectAny__dangerouslySetPtr(value, ptr); + } + + /// Detach the ptr from the thisValue + pub fn detachPtr(_: *ExpectAny, value: JSC.JSValue) void { + JSC.markBinding(@src()); + std.debug.assert(ExpectAny__dangerouslySetPtr(value, null)); + } + + extern fn ExpectAny__fromJS(JSC.JSValue) ?*ExpectAny; + extern fn ExpectAny__getConstructor(*JSC.JSGlobalObject) JSC.JSValue; + + extern fn ExpectAny__create(globalObject: *JSC.JSGlobalObject, ptr: ?*ExpectAny) JSC.JSValue; + + extern fn ExpectAny__dangerouslySetPtr(JSC.JSValue, ?*ExpectAny) bool; + + comptime { + if (@TypeOf(ExpectAny.finalize) != (fn (*ExpectAny) callconv(.C) void)) { + @compileLog("ExpectAny.finalize is not a finalizer"); + } + + if (@TypeOf(ExpectAny.call) != StaticCallbackType) + @compileLog("Expected ExpectAny.call to be a static callback"); + if (!JSC.is_bindgen) { + @export(ExpectAny.call, .{ .name = "ExpectAnyClass__call" }); + @export(ExpectAny.finalize, .{ .name = "ExpectAnyClass__finalize" }); + } + } +}; pub const JSFileSystemRouter = struct { const FileSystemRouter = Classes.FileSystemRouter; const GetterType = fn (*FileSystemRouter, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; @@ -3765,6 +3846,7 @@ comptime { _ = JSCryptoHasher; _ = JSDirent; _ = JSExpect; + _ = JSExpectAny; _ = JSFileSystemRouter; _ = JSListener; _ = JSMD4; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index d779a4ff1..3c895e219 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -20,6 +20,7 @@ pub const Classes = struct { pub const TLSSocket = JSC.API.TLSSocket; pub const Listener = JSC.API.Listener; pub const Expect = JSC.Jest.Expect; + pub const ExpectAny = JSC.Jest.ExpectAny; pub const FileSystemRouter = JSC.API.FileSystemRouter; pub const MatchedRoute = JSC.API.MatchedRoute; pub const Dirent = JSC.Node.Dirent; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 20b7fbedb..8312706e1 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -319,6 +319,7 @@ CPP_DECL bool JSC__JSValue__isBigInt32(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isBoolean(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isCallable(JSC__JSValue JSValue0, JSC__VM* arg1); CPP_DECL bool JSC__JSValue__isClass(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1); +CPP_DECL bool JSC__JSValue__isConstructor(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isCustomGetterSetter(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isError(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isException(JSC__JSValue JSValue0, JSC__VM* arg1); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index ce84d1e73..ee0c4fb54 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -232,6 +232,7 @@ pub extern fn JSC__JSValue__isBigInt32(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isBoolean(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isCallable(JSValue0: JSC__JSValue, arg1: *bindings.VM) bool; pub extern fn JSC__JSValue__isClass(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject) bool; +pub extern fn JSC__JSValue__isConstructor(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isCustomGetterSetter(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isError(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isException(JSValue0: JSC__JSValue, arg1: *bindings.VM) bool; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d56d0bdeb..f54dd5fb8 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1628,7 +1628,6 @@ pub const VirtualMachine = struct { // If there were multiple errors, it could be contained in an AggregateError. // In that case, this function becomes recursive. // In all other cases, we will convert it to a ZigException. - const errors_property = ZigString.init("errors"); pub fn printErrorlikeObject( this: *VirtualMachine, value: JSValue, diff --git a/src/bun.js/scripts/generate-classes.ts b/src/bun.js/scripts/generate-classes.ts index d39ea027b..f13132f8f 100644 --- a/src/bun.js/scripts/generate-classes.ts +++ b/src/bun.js/scripts/generate-classes.ts @@ -292,9 +292,7 @@ export function generateHashTable(nameToUse, symbolName, typeName, obj, props = // { "CLOSED"_s, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::ConstantInteger, NoIntrinsic, { HashTableValue::ConstantType, 3 } }, // }; return ` - static const HashTableValue ${nameToUse}TableValues[] = { -${rows.join(" ,\n")} - }; + static const HashTableValue ${nameToUse}TableValues[] = {${rows.length > 0 ? "\n" + rows.join(" ,\n") + "\n" : ""}}; `; } @@ -348,7 +346,11 @@ ${renderFieldsImpl(protoSymbolName, typeName, obj, protoFields, obj.values || [] void ${proto}::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { Base::finishCreation(vm); - reifyStaticProperties(vm, ${className(typeName)}::info(), ${proto}TableValues, *this);${specialSymbols} + ${ + Object.keys(protoFields).length > 0 + ? `reifyStaticProperties(vm, ${className(typeName)}::info(), ${proto}TableValues, *this);` + : "" + }${specialSymbols} JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } @@ -1144,9 +1146,9 @@ function generateImpl(typeName, obj) { return [ generatePrototypeHeader(typeName), !obj.noConstructor ? generateConstructorHeader(typeName).trim() + "\n" : null, - Object.keys(proto).length > 0 && generatePrototype(typeName, obj).trim(), + generatePrototype(typeName, obj).trim(), !obj.noConstructor ? generateConstructorImpl(typeName, obj).trim() : null, - Object.keys(proto).length > 0 && generateClassImpl(typeName, obj).trim(), + generateClassImpl(typeName, obj).trim(), ] .filter(Boolean) .join("\n\n"); diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index 78848b287..9182c8cc6 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -2,6 +2,18 @@ import { define } from "../scripts/class-definitions"; export default [ define({ + name: "ExpectAny", + construct: false, + noConstructor: true, + call: true, + finalize: true, + JSType: "0b11101110", + values: ["constructorValue"], + configurable: false, + klass: {}, + proto: {}, + }), + define({ name: "Expect", construct: true, call: true, diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 2cb86c4f7..d8d4fb246 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1,5 +1,7 @@ const std = @import("std"); const bun = @import("bun"); +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; const Api = @import("../../api/schema.zig").Api; const RequestContext = @import("../../http.zig").RequestContext; const MimeType = @import("../../http.zig").MimeType; @@ -64,12 +66,75 @@ fn notImplementedProp( } pub const DiffFormatter = struct { - received: JSValue, - expected: JSValue, + received_string: ?string = null, + expected_string: ?string = null, + received: ?JSValue = null, + expected: ?JSValue = null, globalObject: *JSC.JSGlobalObject, not: bool = false, pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (this.expected_string != null and this.received_string != null) { + const received = this.received_string.?; + const expected = this.expected_string.?; + + var dmp = DiffMatchPatch.default; + dmp.diff_timeout = 200; + var diffs = try dmp.diff(default_allocator, received, expected, false); + defer diffs.deinit(default_allocator); + + const equal_fmt = "<d>{s}<r>"; + const delete_fmt = "<red>{s}<r>"; + const insert_fmt = "<green>{s}<r>"; + + try writer.writeAll("Expected: "); + for (diffs.items) |df| { + switch (df.operation) { + .delete => continue, + .insert => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(insert_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + + try writer.writeAll("\nReceived: "); + for (diffs.items) |df| { + switch (df.operation) { + .insert => continue, + .delete => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(delete_fmt, false), .{df.text}); + } + }, + .equal => { + if (Output.enable_ansi_colors) { + try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}); + } else { + try writer.print(Output.prettyFmt(equal_fmt, false), .{df.text}); + } + }, + } + } + return; + } + + if (this.received == null or this.expected == null) return; + + const received = this.received.?; + const expected = this.expected.?; var received_buf = MutableString.init(default_allocator, 0) catch unreachable; var expected_buf = MutableString.init(default_allocator, 0) catch unreachable; defer { @@ -94,7 +159,7 @@ pub const DiffFormatter = struct { JSC.ZigConsoleClient.format( .Debug, this.globalObject, - @ptrCast([*]const JSValue, &this.received), + @ptrCast([*]const JSValue, &received), 1, Writer, Writer, @@ -131,21 +196,21 @@ pub const DiffFormatter = struct { return; } - switch (this.received.determineDiffMethod(this.expected, this.globalObject)) { + switch (received.determineDiffMethod(expected, this.globalObject)) { .none => { const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>"; var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true }; if (Output.enable_ansi_colors) { try writer.print(Output.prettyFmt(fmt, true), .{ - this.expected.toFmt(this.globalObject, &formatter), - this.received.toFmt(this.globalObject, &formatter), + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), }); return; } try writer.print(Output.prettyFmt(fmt, true), .{ - this.expected.toFmt(this.globalObject, &formatter), - this.received.toFmt(this.globalObject, &formatter), + expected.toFmt(this.globalObject, &formatter), + received.toFmt(this.globalObject, &formatter), }); return; }, @@ -305,6 +370,8 @@ pub const TestRunner = struct { /// This silences TestNotRunningError when expect() is used to halt a running test. did_pending_test_fail: bool = false, + snapshots: Snapshots, + pub const Drainer = JSC.AnyTask.New(TestRunner, drain); pub fn enqueue(this: *TestRunner, task: *TestRunnerTask) void { @@ -427,6 +494,274 @@ pub const TestRunner = struct { }; }; +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 = std.hash.Wyhash.hash(0, 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.ok) parse_result.ast else return error.ParseError; + defer ast.deinit(); + + if (ast.exports_ref == null) 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) |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 = std.hash.Wyhash.hash(0, 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; @@ -465,6 +800,54 @@ pub const Jest = struct { } }; +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 = "<d>expect.<r>any<d>(<r>constructor<d>)<r>\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(); + JSC.Jest.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 { @@ -481,6 +864,49 @@ pub const Expect = struct { 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 { @@ -2080,6 +2506,215 @@ pub const Expect = struct { 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\n<b>Matcher error<r>: Snapshot matchers cannot be used with <b>not<r>\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", "<green>properties<r><d>, <r>hint", false); + const fmt = signature ++ "\n\nMatcher error: Expected <green>properties<r> 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", "<green>properties<r><d>, <r>hint", false); + const fmt = signature ++ "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n"; + globalObject.throwPretty(fmt, .{}); + return .zero; + } + + if (property_matchers) |_prop_matchers| { + var prop_matchers = _prop_matchers; + + var itr = PropertyMatcherIterator{ + .received_object = value, + .failed = false, + }; + + prop_matchers.forEachProperty(globalObject, &itr, PropertyMatcherIterator.forEach); + + if (itr.failed) { + // TODO: print diff with properties from propertyMatchers + const signature = comptime getSignature("toMatchSnapshot", "<green>propertyMatchers<r>", false); + const fmt = signature ++ "\n\nExpected <green>propertyMatchers<r> 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", "<green>expected<r>", 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 const PropertyMatcherIterator = struct { + received_object: JSValue, + failed: bool, + i: usize = 0, + + pub fn forEach( + globalObject: *JSGlobalObject, + ctx_ptr: ?*anyopaque, + key_: [*c]ZigString, + value: JSValue, + _: bool, + ) callconv(.C) void { + const key: ZigString = key_.?[0]; + if (key.eqlComptime("constructor")) return; + if (key.eqlComptime("call")) return; + + var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); + defer ctx.i += 1; + var received_object: JSValue = ctx.received_object; + + if (received_object.get(globalObject, key.slice())) |received_value| { + if (JSC.Jest.ExpectAny.fromJS(value)) |_| { + var constructor_value = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse { + globalObject.throw("Internal consistency error: the expect.any(constructor value) was garbage collected but it should not have been!", .{}); + ctx.failed = true; + return; + }; + + if (received_value.isCell() and received_value.isInstanceOf(globalObject, constructor_value)) { + received_object.put(globalObject, &key, value); + return; + } + + // check primitives + // TODO: check the constructor for primitives by reading it from JSGlobalObject through a binding. + var constructor_name = ZigString.Empty; + constructor_value.getNameProperty(globalObject, &constructor_name); + if (received_value.isNumber() and constructor_name.eqlComptime("Number")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isBoolean() and constructor_name.eqlComptime("Boolean")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isString() and constructor_name.eqlComptime("String")) { + received_object.put(globalObject, &key, value); + return; + } + if (received_value.isBigInt() and constructor_name.eqlComptime("BigInt")) { + received_object.put(globalObject, &key, value); + return; + } + + ctx.failed = true; + return; + } + + if (value.isObject()) { + if (received_object.get(globalObject, key.slice())) |new_object| { + var itr = PropertyMatcherIterator{ + .received_object = new_object, + .failed = false, + }; + value.forEachProperty(globalObject, &itr, PropertyMatcherIterator.forEach); + if (itr.failed) { + ctx.failed = true; + } + } else { + ctx.failed = true; + } + + return; + } + + if (value.isSameValue(received_value, globalObject)) return; + } + + ctx.failed = true; + } + }; + pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { defer this.postMatch(globalObject); @@ -2098,7 +2733,6 @@ pub const Expect = struct { } active_test_expectation_counter.actual += 1; - const expected_value = arguments[0]; if (!expected_value.jsType().isFunction()) { var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; @@ -2156,7 +2790,6 @@ pub const Expect = struct { pub const toContainEqual = notImplementedJSCFn; pub const toMatch = notImplementedJSCFn; pub const toMatchObject = notImplementedJSCFn; - pub const toMatchSnapshot = notImplementedJSCFn; pub const toMatchInlineSnapshot = notImplementedJSCFn; pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn; pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn; @@ -2179,9 +2812,12 @@ pub const Expect = struct { pub const getResolves = notImplementedJSCProp; pub const getRejects = notImplementedJSCProp; + pub fn any(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + return ExpectAny.call(globalObject, callFrame); + } + pub const extend = notImplementedStaticFn; pub const anything = notImplementedStaticFn; - pub const any = notImplementedStaticFn; pub const arrayContaining = notImplementedStaticFn; pub const assertions = notImplementedStaticFn; pub const hasAssertions = notImplementedStaticFn; @@ -2225,6 +2861,7 @@ pub const TestScope = struct { ran: bool = false, task: ?*TestRunnerTask = null, skipped: bool = false, + snapshot_count: usize = 0, pub const Class = NewClass( void, diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig new file mode 100644 index 000000000..dd5814aca --- /dev/null +++ b/src/bun.js/test/pretty_format.zig @@ -0,0 +1,1963 @@ +const std = @import("std"); +const bun = @import("bun"); +const Output = bun.Output; +const JSC = bun.JSC; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; +const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; +const default_allocator = bun.default_allocator; +const CAPI = JSC.C; +const ZigString = JSC.ZigString; +const strings = bun.strings; +const string = bun.string; +const JSLexer = bun.js_lexer; +const JSPrinter = bun.js_printer; +const JSPrivateDataPtr = JSC.JSPrivateDataPtr; +const JS = @import("../javascript.zig"); +const JSPromise = JSC.JSPromise; + +pub const EventType = enum(u8) { + Event, + MessageEvent, + CloseEvent, + ErrorEvent, + OpenEvent, + unknown = 254, + _, + + pub const map = bun.ComptimeStringMap(EventType, .{ + .{ EventType.Event.label(), EventType.Event }, + .{ EventType.MessageEvent.label(), EventType.MessageEvent }, + .{ EventType.CloseEvent.label(), EventType.CloseEvent }, + .{ EventType.ErrorEvent.label(), EventType.ErrorEvent }, + .{ EventType.OpenEvent.label(), EventType.OpenEvent }, + }); + + pub fn label(this: EventType) string { + return switch (this) { + .Event => "event", + .MessageEvent => "message", + .CloseEvent => "close", + .ErrorEvent => "error", + .OpenEvent => "open", + else => "event", + }; + } +}; + +pub const JestPrettyFormat = struct { + pub const Type = *anyopaque; + const Counter = std.AutoHashMapUnmanaged(u64, u32); + + counts: Counter = .{}, + + pub const MessageLevel = enum(u32) { + Log = 0, + Warning = 1, + Error = 2, + Debug = 3, + Info = 4, + _, + }; + + pub const MessageType = enum(u32) { + Log = 0, + Dir = 1, + DirXML = 2, + Table = 3, + Trace = 4, + StartGroup = 5, + StartGroupCollapsed = 6, + EndGroup = 7, + Clear = 8, + Assert = 9, + Timing = 10, + Profile = 11, + ProfileEnd = 12, + Image = 13, + _, + }; + + pub const FormatOptions = struct { + enable_colors: bool, + add_newline: bool, + flush: bool, + quote_strings: bool = false, + }; + + pub fn format( + level: MessageLevel, + global: *JSGlobalObject, + vals: [*]const JSValue, + len: usize, + comptime RawWriter: type, + comptime Writer: type, + writer: Writer, + options: FormatOptions, + ) void { + var fmt: JestPrettyFormat.Formatter = undefined; + defer { + if (fmt.map_node) |node| { + node.data = fmt.map; + node.data.clearRetainingCapacity(); + node.release(); + } + } + + if (len == 1) { + fmt = JestPrettyFormat.Formatter{ + .remaining_values = &[_]JSValue{}, + .globalThis = global, + .quote_strings = options.quote_strings, + }; + const tag = JestPrettyFormat.Formatter.Tag.get(vals[0], global); + + var unbuffered_writer = if (comptime Writer != RawWriter) + writer.context.unbuffered_writer.context.writer() + else + writer; + + if (tag.tag == .String) { + if (options.enable_colors) { + if (level == .Error) { + unbuffered_writer.writeAll(comptime Output.prettyFmt("<r><red>", true)) catch unreachable; + } + fmt.format( + tag, + @TypeOf(unbuffered_writer), + unbuffered_writer, + vals[0], + global, + true, + ); + if (level == .Error) { + unbuffered_writer.writeAll(comptime Output.prettyFmt("<r>", true)) catch unreachable; + } + } else { + fmt.format( + tag, + @TypeOf(unbuffered_writer), + unbuffered_writer, + vals[0], + global, + false, + ); + } + if (options.add_newline) _ = unbuffered_writer.write("\n") catch 0; + } else { + defer { + if (comptime Writer != RawWriter) { + if (options.flush) writer.context.flush() catch {}; + } + } + if (options.enable_colors) { + fmt.format( + tag, + Writer, + writer, + vals[0], + global, + true, + ); + } else { + fmt.format( + tag, + Writer, + writer, + vals[0], + global, + false, + ); + } + if (options.add_newline) _ = writer.write("\n") catch 0; + } + + return; + } + + defer { + if (comptime Writer != RawWriter) { + if (options.flush) writer.context.flush() catch {}; + } + } + + var this_value: JSValue = vals[0]; + fmt = JestPrettyFormat.Formatter{ + .remaining_values = vals[0..len][1..], + .globalThis = global, + .quote_strings = options.quote_strings, + }; + var tag: JestPrettyFormat.Formatter.Tag.Result = undefined; + + var any = false; + if (options.enable_colors) { + if (level == .Error) { + writer.writeAll(comptime Output.prettyFmt("<r><red>", true)) catch unreachable; + } + while (true) { + if (any) { + _ = writer.write(" ") catch 0; + } + any = true; + + tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + if (tag.tag == .String and fmt.remaining_values.len > 0) { + tag.tag = .StringPossiblyFormatted; + } + + fmt.format(tag, Writer, writer, this_value, global, true); + if (fmt.remaining_values.len == 0) { + break; + } + + this_value = fmt.remaining_values[0]; + fmt.remaining_values = fmt.remaining_values[1..]; + } + if (level == .Error) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)) catch unreachable; + } + } else { + while (true) { + if (any) { + _ = writer.write(" ") catch 0; + } + any = true; + tag = JestPrettyFormat.Formatter.Tag.get(this_value, global); + if (tag.tag == .String and fmt.remaining_values.len > 0) { + tag.tag = .StringPossiblyFormatted; + } + + fmt.format(tag, Writer, writer, this_value, global, false); + if (fmt.remaining_values.len == 0) + break; + + this_value = fmt.remaining_values[0]; + fmt.remaining_values = fmt.remaining_values[1..]; + } + } + + if (options.add_newline) _ = writer.write("\n") catch 0; + } + + pub const Formatter = struct { + remaining_values: []const JSValue = &[_]JSValue{}, + map: Visited.Map = undefined, + map_node: ?*Visited.Pool.Node = null, + hide_native: bool = false, + globalThis: *JSGlobalObject, + indent: u32 = 0, + quote_strings: bool = false, + failed: bool = false, + estimated_line_length: usize = 0, + always_newline_scope: bool = false, + + pub fn goodTimeForANewLine(this: *@This()) bool { + if (this.estimated_line_length > 80) { + this.resetLine(); + return true; + } + return false; + } + + pub fn resetLine(this: *@This()) void { + this.estimated_line_length = this.indent * 2; + } + + pub fn addForNewLine(this: *@This(), len: usize) void { + this.estimated_line_length +|= len; + } + + pub const ZigFormatter = struct { + formatter: *JestPrettyFormat.Formatter, + global: *JSGlobalObject, + value: JSValue, + + pub const WriteError = error{UhOh}; + pub fn format(self: ZigFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + self.formatter.remaining_values = &[_]JSValue{self.value}; + defer { + self.formatter.remaining_values = &[_]JSValue{}; + } + self.formatter.globalThis = self.global; + self.formatter.format( + Tag.get(self.value, self.global), + @TypeOf(writer), + writer, + self.value, + self.formatter.globalThis, + false, + ); + } + }; + + // For detecting circular references + pub const Visited = struct { + const ObjectPool = @import("../../pool.zig").ObjectPool; + pub const Map = std.AutoHashMap(JSValue.Type, void); + pub const Pool = ObjectPool( + Map, + struct { + pub fn init(allocator: std.mem.Allocator) anyerror!Map { + return Map.init(allocator); + } + }.init, + true, + 16, + ); + }; + + pub const Tag = enum { + StringPossiblyFormatted, + String, + Undefined, + Double, + Integer, + Null, + Boolean, + Array, + Object, + Function, + Class, + Error, + TypedArray, + Map, + Set, + Symbol, + BigInt, + + GlobalObject, + Private, + Promise, + + JSON, + NativeCode, + ArrayBuffer, + + JSX, + Event, + + pub fn isPrimitive(this: Tag) bool { + return switch (this) { + .String, + .StringPossiblyFormatted, + .Undefined, + .Double, + .Integer, + .Null, + .Boolean, + .Symbol, + .BigInt, + => true, + else => false, + }; + } + + pub inline fn canHaveCircularReferences(tag: Tag) bool { + return tag == .Array or tag == .Object or tag == .Map or tag == .Set; + } + + const Result = struct { + tag: Tag, + cell: JSValue.JSType = JSValue.JSType.Cell, + }; + + pub fn get(value: JSValue, globalThis: *JSGlobalObject) Result { + switch (@enumToInt(value)) { + 0, 0xa => return Result{ + .tag = .Undefined, + }, + 0x2 => return Result{ + .tag = .Null, + }, + else => {}, + } + + if (value.isInt32()) { + return .{ + .tag = .Integer, + }; + } else if (value.isNumber()) { + return .{ + .tag = .Double, + }; + } else if (value.isBoolean()) { + return .{ + .tag = .Boolean, + }; + } + + if (!value.isCell()) + return .{ + .tag = .NativeCode, + }; + + const js_type = value.jsType(); + + if (js_type.isHidden()) return .{ + .tag = .NativeCode, + .cell = js_type, + }; + + // Cell is the "unknown" type + // if we call JSObjectGetPrivate, it can segfault + if (js_type == .Cell) { + return .{ + .tag = .NativeCode, + .cell = js_type, + }; + } + + if (js_type == .DOMWrapper) { + return .{ + .tag = .Private, + .cell = js_type, + }; + } + + if (CAPI.JSObjectGetPrivate(value.asObjectRef()) != null) + return .{ + .tag = .Private, + .cell = js_type, + }; + + // If we check an Object has a method table and it does not + // it will crash + const callable = js_type != .Object and value.isCallable(globalThis.vm()); + + if (value.isClass(globalThis) and !callable) { + return .{ + .tag = .Object, + .cell = js_type, + }; + } + + if (callable and js_type == .JSFunction) { + return .{ + .tag = .Function, + .cell = js_type, + }; + } else if (callable and js_type == .InternalFunction) { + return .{ + .tag = .Object, + .cell = js_type, + }; + } + + if (js_type == .PureForwardingProxy) { + return Tag.get( + JSC.JSValue.c(JSC.C.JSObjectGetProxyTarget(value.asObjectRef())), + globalThis, + ); + } + + // Is this a react element? + if (js_type.isObject()) { + if (value.get(globalThis, "$$typeof")) |typeof_symbol| { + var reactElement = ZigString.init("react.element"); + var react_fragment = ZigString.init("react.fragment"); + + if (JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &reactElement), globalThis) or JSValue.isSameValue(typeof_symbol, JSValue.symbolFor(globalThis, &react_fragment), globalThis)) { + return .{ .tag = .JSX, .cell = js_type }; + } + } + } + + return .{ + .tag = switch (js_type) { + JSValue.JSType.ErrorInstance => .Error, + JSValue.JSType.NumberObject => .Double, + JSValue.JSType.DerivedArray, JSValue.JSType.Array => .Array, + JSValue.JSType.DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, + JSValue.JSType.RegExpObject => .String, + JSValue.JSType.Symbol => .Symbol, + JSValue.JSType.BooleanObject => .Boolean, + JSValue.JSType.JSFunction => .Function, + JSValue.JSType.JSWeakMap, JSValue.JSType.JSMap => .Map, + JSValue.JSType.JSWeakSet, JSValue.JSType.JSSet => .Set, + JSValue.JSType.JSDate => .JSON, + JSValue.JSType.JSPromise => .Promise, + JSValue.JSType.Object, + JSValue.JSType.FinalObject, + .ModuleNamespaceObject, + .GlobalObject, + => .Object, + + .ArrayBuffer, + JSValue.JSType.Int8Array, + JSValue.JSType.Uint8Array, + JSValue.JSType.Uint8ClampedArray, + JSValue.JSType.Int16Array, + JSValue.JSType.Uint16Array, + JSValue.JSType.Int32Array, + JSValue.JSType.Uint32Array, + JSValue.JSType.Float32Array, + JSValue.JSType.Float64Array, + JSValue.JSType.BigInt64Array, + JSValue.JSType.BigUint64Array, + .DataView, + => .TypedArray, + + .HeapBigInt => .BigInt, + + // None of these should ever exist here + // But we're going to check anyway + .GetterSetter, + .CustomGetterSetter, + .APIValueWrapper, + .NativeExecutable, + .ProgramExecutable, + .ModuleProgramExecutable, + .EvalExecutable, + .FunctionExecutable, + .UnlinkedFunctionExecutable, + .UnlinkedProgramCodeBlock, + .UnlinkedModuleProgramCodeBlock, + .UnlinkedEvalCodeBlock, + .UnlinkedFunctionCodeBlock, + .CodeBlock, + .JSImmutableButterfly, + .JSSourceCode, + .JSScriptFetcher, + .JSScriptFetchParameters, + .JSCallee, + .GlobalLexicalEnvironment, + .LexicalEnvironment, + .ModuleEnvironment, + .StrictEvalActivation, + .WithScope, + => .NativeCode, + + .Event => .Event, + + else => .JSON, + }, + .cell = js_type, + }; + } + }; + + const CellType = CAPI.CellType; + threadlocal var name_buf: [512]u8 = undefined; + + fn writeWithFormatting( + this: *JestPrettyFormat.Formatter, + comptime Writer: type, + writer_: Writer, + comptime Slice: type, + slice_: Slice, + globalThis: *JSGlobalObject, + comptime enable_ansi_colors: bool, + ) void { + var writer = WrappedWriter(Writer){ .ctx = writer_ }; + var slice = slice_; + var i: u32 = 0; + var len: u32 = @truncate(u32, slice.len); + var any_non_ascii = false; + while (i < len) : (i += 1) { + switch (slice[i]) { + '%' => { + i += 1; + if (i >= len) + break; + + const token = switch (slice[i]) { + 's' => Tag.String, + 'f' => Tag.Double, + 'o' => Tag.Undefined, + 'O' => Tag.Object, + 'd', 'i' => Tag.Integer, + else => continue, + }; + + // Flush everything up to the % + const end = slice[0 .. i - 1]; + if (!any_non_ascii) + writer.writeAll(end) + else + writer.writeAll(end); + any_non_ascii = false; + slice = slice[@min(slice.len, i + 1)..]; + i = 0; + len = @truncate(u32, slice.len); + const next_value = this.remaining_values[0]; + this.remaining_values = this.remaining_values[1..]; + switch (token) { + Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + + // undefined is overloaded to mean the '%o" field + Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors), + + else => unreachable, + } + if (this.remaining_values.len == 0) break; + }, + '\\' => { + i += 1; + if (i >= len) + break; + if (slice[i] == '%') i += 2; + }, + 128...255 => { + any_non_ascii = true; + }, + else => {}, + } + } + + if (slice.len > 0) writer.writeAll(slice); + } + + pub fn WrappedWriter(comptime Writer: type) type { + return struct { + ctx: Writer, + failed: bool = false, + estimated_line_length: *usize = undefined, + + pub fn print(self: *@This(), comptime fmt: string, args: anytype) void { + self.ctx.print(fmt, args) catch { + self.failed = true; + }; + } + + pub fn writeLatin1(self: *@This(), buf: []const u8) void { + var remain = buf; + while (remain.len > 0) { + if (strings.firstNonASCII(remain)) |i| { + if (i > 0) { + self.ctx.writeAll(remain[0..i]) catch { + self.failed = true; + return; + }; + } + self.ctx.writeAll(&strings.latin1ToCodepointBytesAssumeNotASCII(remain[i])) catch { + self.failed = true; + }; + remain = remain[i + 1 ..]; + } else { + break; + } + } + + self.ctx.writeAll(remain) catch return; + } + + pub inline fn writeAll(self: *@This(), buf: []const u8) void { + self.ctx.writeAll(buf) catch { + self.failed = true; + }; + } + + pub inline fn writeString(self: *@This(), str: ZigString) void { + self.print("{}", .{str}); + } + + pub inline fn write16Bit(self: *@This(), input: []const u16) void { + strings.formatUTF16Type([]const u16, input, self.ctx) catch { + self.failed = true; + }; + } + }; + } + + pub fn writeIndent( + this: *JestPrettyFormat.Formatter, + comptime Writer: type, + writer: Writer, + ) !void { + const indent = @min(this.indent, 32); + var buf = [_]u8{' '} ** 64; + var total_remain: usize = indent; + while (total_remain > 0) { + const written = @min(32, total_remain); + try writer.writeAll(buf[0 .. written * 2]); + total_remain -|= written; + } + } + + pub fn printComma(this: *JestPrettyFormat.Formatter, comptime Writer: type, writer: Writer, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime Output.prettyFmt("<r><d>,<r>", enable_ansi_colors)); + this.estimated_line_length += 1; + } + + pub fn MapIterator(comptime Writer: type, comptime enable_ansi_colors: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + pub fn forEach(_: [*c]JSC.VM, globalObject: [*c]JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { + var this: *@This() = bun.cast(*@This(), ctx orelse return); + const key = JSC.JSObject.getIndex(nextValue, globalObject, 0); + const value = JSC.JSObject.getIndex(nextValue, globalObject, 1); + this.formatter.writeIndent(Writer, this.writer) catch unreachable; + const key_tag = Tag.get(key, globalObject); + + this.formatter.format( + key_tag, + Writer, + this.writer, + key, + this.formatter.globalThis, + enable_ansi_colors, + ); + this.writer.writeAll(" => ") catch unreachable; + const value_tag = Tag.get(value, globalObject); + this.formatter.format( + value_tag, + Writer, + this.writer, + value, + this.formatter.globalThis, + enable_ansi_colors, + ); + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; + this.writer.writeAll("\n") catch unreachable; + } + }; + } + + pub fn SetIterator(comptime Writer: type, comptime enable_ansi_colors: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + pub fn forEach(_: [*c]JSC.VM, globalObject: [*c]JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { + var this: *@This() = bun.cast(*@This(), ctx orelse return); + this.formatter.writeIndent(Writer, this.writer) catch {}; + const key_tag = Tag.get(nextValue, globalObject); + this.formatter.format( + key_tag, + Writer, + this.writer, + nextValue, + this.formatter.globalThis, + enable_ansi_colors, + ); + + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; + this.writer.writeAll("\n") catch unreachable; + } + }; + } + + pub fn PropertyIterator(comptime Writer: type, comptime enable_ansi_colors_: bool) type { + return struct { + formatter: *JestPrettyFormat.Formatter, + writer: Writer, + i: usize = 0, + always_newline: bool = false, + parent: JSValue, + const enable_ansi_colors = enable_ansi_colors_; + pub fn handleFirstProperty(this: *@This(), globalThis: *JSC.JSGlobalObject, value: JSValue) void { + if (!value.jsType().isFunction() and !value.isClass(globalThis)) { + var writer = WrappedWriter(Writer){ + .ctx = this.writer, + .failed = false, + }; + var name_str = ZigString.init(""); + + value.getNameProperty(globalThis, &name_str); + if (name_str.len > 0 and !strings.eqlComptime(name_str.slice(), "Object")) { + writer.print("{} ", .{name_str}); + } else { + value.getPrototype(globalThis).getNameProperty(globalThis, &name_str); + if (name_str.len > 0 and !strings.eqlComptime(name_str.slice(), "Object")) { + writer.print("{} ", .{name_str}); + } + } + } + + this.always_newline = true; + this.formatter.estimated_line_length = this.formatter.indent * 2 + 1; + + if (this.formatter.indent == 0) this.writer.writeAll("\n") catch {}; + var classname = ZigString.Empty; + value.getClassName(globalThis, &classname); + if (!strings.eqlComptime(classname.slice(), "Object")) { + this.writer.print("{} ", .{classname}) catch {}; + } + + this.writer.writeAll("{\n") catch {}; + this.formatter.indent += 1; + this.formatter.writeIndent(Writer, this.writer) catch {}; + } + + pub fn forEach( + globalThis: *JSGlobalObject, + ctx_ptr: ?*anyopaque, + key_: [*c]ZigString, + value: JSValue, + is_symbol: bool, + ) callconv(.C) void { + const key = key_.?[0]; + if (key.eqlComptime("constructor")) return; + if (key.eqlComptime("call")) return; + + var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); + var this = ctx.formatter; + var writer_ = ctx.writer; + var writer = WrappedWriter(Writer){ + .ctx = writer_, + .failed = false, + }; + + const tag = Tag.get(value, globalThis); + + if (tag.cell.isHidden()) return; + if (ctx.i == 0) { + handleFirstProperty(ctx, globalThis, ctx.parent); + } else { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + + defer ctx.i += 1; + if (ctx.i > 0) { + if (ctx.always_newline or this.always_newline_scope or this.goodTimeForANewLine()) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + this.resetLine(); + } else { + this.estimated_line_length += 1; + writer.writeAll(" "); + } + } + + if (!is_symbol) { + + // TODO: make this one pass? + if (!key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.slice()), key.slice())) { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), + .{key}, + ); + } else if (key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned())) { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("\"{}\"<d>:<r> ", enable_ansi_colors), + .{key}, + ); + } else if (key.is16Bit()) { + var utf16Slice = key.utf16SliceAligned(); + + this.addForNewLine(utf16Slice.len + 2); + + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + + writer.writeAll("'"); + + while (strings.indexOfAny16(utf16Slice, "\"")) |j| { + writer.write16Bit(utf16Slice[0..j]); + writer.writeAll("\""); + utf16Slice = utf16Slice[j + 1 ..]; + } + + writer.write16Bit(utf16Slice); + + writer.print( + comptime Output.prettyFmt("\"<r><d>:<r> ", enable_ansi_colors), + .{}, + ); + } else { + this.addForNewLine(key.len + 1); + + writer.print( + comptime Output.prettyFmt("{s}<d>:<r> ", enable_ansi_colors), + .{JSPrinter.formatJSONString(key.slice())}, + ); + } + } else { + this.addForNewLine(1 + "[Symbol()]:".len + key.len); + writer.print( + comptime Output.prettyFmt("<r><d>[<r><blue>Symbol({any})<r><d>]:<r> ", enable_ansi_colors), + .{ + key, + }, + ); + } + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + } + + this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + } + }; + } + + pub fn printAs( + this: *JestPrettyFormat.Formatter, + comptime Format: JestPrettyFormat.Formatter.Tag, + comptime Writer: type, + writer_: Writer, + value: JSValue, + jsType: JSValue.JSType, + comptime enable_ansi_colors: bool, + ) void { + if (this.failed) + return; + var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length }; + defer { + if (writer.failed) { + this.failed = true; + } + } + if (comptime Format.canHaveCircularReferences()) { + if (this.map_node == null) { + this.map_node = Visited.Pool.get(default_allocator); + this.map_node.?.data.clearRetainingCapacity(); + this.map = this.map_node.?.data; + } + + var entry = this.map.getOrPut(@enumToInt(value)) catch unreachable; + if (entry.found_existing) { + writer.writeAll(comptime Output.prettyFmt("<r><cyan>[Circular]<r>", enable_ansi_colors)); + return; + } + } + + defer { + if (comptime Format.canHaveCircularReferences()) { + _ = this.map.remove(@enumToInt(value)); + } + } + + switch (comptime Format) { + .StringPossiblyFormatted => { + var str = value.toSlice(this.globalThis, bun.default_allocator); + defer str.deinit(); + this.addForNewLine(str.len); + const slice = str.slice(); + this.writeWithFormatting(Writer, writer_, @TypeOf(slice), slice, this.globalThis, enable_ansi_colors); + }, + .String => { + var str = ZigString.init(""); + value.toZigString(&str, this.globalThis); + this.addForNewLine(str.len); + + if (value.jsType() == .StringObject or value.jsType() == .DerivedStringObject) { + if (str.len == 0) { + writer.writeAll("String {}"); + return; + } + if (this.indent == 0 and str.len > 0) { + writer.writeAll("\n"); + } + writer.writeAll("String {\n"); + this.indent += 1; + defer this.indent -|= 1; + this.resetLine(); + this.writeIndent(Writer, writer_) catch unreachable; + const length = str.len; + for (str.slice(), 0..) |c, i| { + writer.print("\"{d}\": \"{c}\",\n", .{ i, c }); + if (i != length - 1) this.writeIndent(Writer, writer_) catch unreachable; + } + this.resetLine(); + writer.writeAll("}\n"); + return; + } + + if (this.quote_strings and jsType != .RegExpObject) { + if (str.len == 0) { + writer.writeAll("\"\""); + return; + } + + if (comptime enable_ansi_colors) { + writer.writeAll(Output.prettyFmt("<r><green>", true)); + } + + defer if (comptime enable_ansi_colors) + writer.writeAll(Output.prettyFmt("<r>", true)); + + if (str.is16Bit()) { + this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); + return; + } + + var has_newline = false; + if (strings.indexOfAny(str.slice(), "\n\r")) |_| { + has_newline = true; + writer.writeAll("\n"); + } + + writer.writeAll("\""); + var remaining = str.slice(); + while (strings.indexOfAny(remaining, "\\\r")) |i| { + switch (remaining[i]) { + '\\' => { + writer.print("{s}\\", .{remaining[0 .. i + 1]}); + remaining = remaining[i + 1 ..]; + }, + '\r' => { + if (i + 1 < remaining.len and remaining[i + 1] == '\n') { + writer.print("{s}", .{remaining[0..i]}); + } else { + writer.print("{s}\n", .{remaining[0..i]}); + } + remaining = remaining[i + 1 ..]; + }, + else => unreachable, + } + } + + writer.writeAll(remaining); + writer.writeAll("\""); + if (has_newline) writer.writeAll("\n"); + return; + } + + if (jsType == .RegExpObject and enable_ansi_colors) { + writer.print(comptime Output.prettyFmt("<r><red>", enable_ansi_colors), .{}); + } + + if (str.is16Bit()) { + // streaming print + writer.print("{s}", .{str}); + } else if (strings.isAllASCII(str.slice())) { + // fast path + writer.writeAll(str.slice()); + } else if (str.len > 0) { + // slow path + var buf = strings.allocateLatin1IntoUTF8(bun.default_allocator, []const u8, str.slice()) catch &[_]u8{}; + if (buf.len > 0) { + defer bun.default_allocator.free(buf); + writer.writeAll(buf); + } + } + + if (jsType == .RegExpObject and enable_ansi_colors) { + writer.print(comptime Output.prettyFmt("<r>", enable_ansi_colors), .{}); + } + }, + .Integer => { + const int = value.toInt64(); + if (int < std.math.maxInt(u32)) { + var i = int; + const is_negative = i < 0; + if (is_negative) { + i = -i; + } + const digits = if (i != 0) + bun.fmt.fastDigitCount(@intCast(usize, i)) + @as(usize, @boolToInt(is_negative)) + else + 1; + this.addForNewLine(digits); + } else { + this.addForNewLine(bun.fmt.count("{d}", .{int})); + } + writer.print(comptime Output.prettyFmt("<r><yellow>{d}<r>", enable_ansi_colors), .{int}); + }, + .BigInt => { + var out_str = value.getZigString(this.globalThis).slice(); + this.addForNewLine(out_str.len); + + writer.print(comptime Output.prettyFmt("<r><yellow>{s}n<r>", enable_ansi_colors), .{out_str}); + }, + .Double => { + if (value.isCell()) { + this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + return; + } + + const num = value.asNumber(); + + if (std.math.isPositiveInf(num)) { + this.addForNewLine("Infinity".len); + writer.print(comptime Output.prettyFmt("<r><yellow>Infinity<r>", enable_ansi_colors), .{}); + } else if (std.math.isNegativeInf(num)) { + this.addForNewLine("-Infinity".len); + writer.print(comptime Output.prettyFmt("<r><yellow>-Infinity<r>", enable_ansi_colors), .{}); + } else if (std.math.isNan(num)) { + this.addForNewLine("NaN".len); + writer.print(comptime Output.prettyFmt("<r><yellow>NaN<r>", enable_ansi_colors), .{}); + } else { + this.addForNewLine(std.fmt.count("{d}", .{num})); + writer.print(comptime Output.prettyFmt("<r><yellow>{d}<r>", enable_ansi_colors), .{num}); + } + }, + .Undefined => { + this.addForNewLine(9); + writer.print(comptime Output.prettyFmt("<r><d>undefined<r>", enable_ansi_colors), .{}); + }, + .Null => { + this.addForNewLine(4); + writer.print(comptime Output.prettyFmt("<r><yellow>null<r>", enable_ansi_colors), .{}); + }, + .Symbol => { + const description = value.getDescription(this.globalThis); + this.addForNewLine("Symbol".len); + + if (description.len > 0) { + this.addForNewLine(description.len + "()".len); + writer.print(comptime Output.prettyFmt("<r><blue>Symbol({any})<r>", enable_ansi_colors), .{description}); + } else { + writer.print(comptime Output.prettyFmt("<r><blue>Symbol<r>", enable_ansi_colors), .{}); + } + }, + .Error => { + var classname = ZigString.Empty; + value.getClassName(this.globalThis, &classname); + var message_string = ZigString.Empty; + if (value.get(this.globalThis, "message")) |message_prop| { + message_prop.toZigString(&message_string, this.globalThis); + } + if (message_string.len == 0) { + writer.print("[{s}]", .{classname}); + return; + } + writer.print("[{s}: {s}]", .{ classname, message_string }); + return; + }, + .Class => { + var printable = ZigString.init(&name_buf); + value.getClassName(this.globalThis, &printable); + this.addForNewLine(printable.len); + + if (printable.len == 0) { + writer.print(comptime Output.prettyFmt("[class]", enable_ansi_colors), .{}); + } else { + writer.print(comptime Output.prettyFmt("[class <cyan>{}<r>]", enable_ansi_colors), .{printable}); + } + }, + .Function => { + writer.writeAll("[Function]"); + }, + .Array => { + const len = @truncate(u32, value.getLengthOfArray(this.globalThis)); + if (len == 0) { + writer.writeAll("[]"); + this.addForNewLine(2); + return; + } + + if (this.indent == 0) { + writer.writeAll("\n"); + } + + var was_good_time = this.always_newline_scope; + { + this.indent += 1; + defer this.indent -|= 1; + + this.addForNewLine(2); + + var ref = value.asObjectRef(); + + var prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + { + const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, 0, null)); + const tag = Tag.get(element, this.globalThis); + + was_good_time = was_good_time or !tag.tag.isPrimitive() or this.goodTimeForANewLine(); + + this.resetLine(); + writer.writeAll("["); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + this.addForNewLine(1); + + this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if (len == 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + } + + var i: u32 = 1; + while (i < len) : (i += 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + + const element = JSValue.fromRef(CAPI.JSObjectGetPropertyAtIndex(this.globalThis, ref, i, null)); + const tag = Tag.get(element, this.globalThis); + + this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if (i == len - 1) { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + } + } + } + + this.resetLine(); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("]"); + if (this.indent == 0) { + writer.writeAll("\n"); + } + this.resetLine(); + this.addForNewLine(1); + }, + .Private => { + if (value.as(JSC.WebCore.Response)) |response| { + response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.WebCore.Request)) |request| { + request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.WebCore.Blob)) |blob| { + blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } else if (value.as(JSC.DOMFormData) != null) { + const toJSONFunction = value.get(this.globalThis, "toJSON").?; + + this.addForNewLine("FormData (entries) ".len); + writer.writeAll(comptime Output.prettyFmt("<r><blue>FormData<r> <d>(entries)<r> ", enable_ansi_colors)); + + return this.printAs( + .Object, + Writer, + writer_, + toJSONFunction.callWithThis(this.globalThis, value, &.{}), + .Object, + enable_ansi_colors, + ); + } else if (value.as(JSC.API.Bun.Timer.TimerObject)) |timer| { + this.addForNewLine("Timeout(# ) ".len + bun.fmt.fastDigitCount(@intCast(u64, @max(timer.id, 0)))); + if (timer.kind == .setInterval) { + this.addForNewLine("repeats ".len + bun.fmt.fastDigitCount(@intCast(u64, @max(timer.id, 0)))); + writer.print(comptime Output.prettyFmt("<r><blue>Timeout<r> <d>(#<yellow>{d}<r><d>, repeats)<r>", enable_ansi_colors), .{ + timer.id, + }); + } else { + writer.print(comptime Output.prettyFmt("<r><blue>Timeout<r> <d>(#<yellow>{d}<r><d>)<r>", enable_ansi_colors), .{ + timer.id, + }); + } + + return; + } else if (jsType != .DOMWrapper) { + if (CAPI.JSObjectGetPrivate(value.asRef())) |private_data_ptr| { + const priv_data = JSPrivateDataPtr.from(private_data_ptr); + switch (priv_data.tag()) { + .BuildError => { + const build_error = priv_data.as(JS.BuildError); + build_error.msg.writeFormat(writer_, enable_ansi_colors) catch {}; + return; + }, + .ResolveError => { + const resolve_error = priv_data.as(JS.ResolveError); + resolve_error.msg.writeFormat(writer_, enable_ansi_colors) catch {}; + return; + }, + else => {}, + } + } + + if (value.isCallable(this.globalThis.vm())) { + return this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors); + } + + return this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors); + } + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + }, + .NativeCode => { + this.addForNewLine("[native code]".len); + writer.writeAll("[native code]"); + }, + .Promise => { + if (this.goodTimeForANewLine()) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + } + writer.writeAll("Promise {}"); + }, + .Boolean => { + if (value.isCell()) { + this.printAs(.Object, Writer, writer_, value, .Object, enable_ansi_colors); + return; + } + if (value.toBoolean()) { + this.addForNewLine(4); + writer.writeAll(comptime Output.prettyFmt("<r><yellow>true<r>", enable_ansi_colors)); + } else { + this.addForNewLine(5); + writer.writeAll(comptime Output.prettyFmt("<r><yellow>false<r>", enable_ansi_colors)); + } + }, + .GlobalObject => { + const fmt = "[this.globalThis]"; + this.addForNewLine(fmt.len); + writer.writeAll(comptime Output.prettyFmt("<cyan>" ++ fmt ++ "<r>", enable_ansi_colors)); + }, + .Map => { + const length_value = value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length = length_value.toInt32(); + + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + const map_name = if (value.jsType() == .JSWeakMap) "WeakMap" else "Map"; + + if (length == 0) { + return writer.print("{s} {{}}", .{map_name}); + } + + writer.print("\n{s} {{\n", .{map_name}); + { + this.indent += 1; + defer this.indent -|= 1; + var iter = MapIterator(Writer, enable_ansi_colors){ + .formatter = this, + .writer = writer_, + }; + value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); + } + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + writer.writeAll("\n"); + }, + .Set => { + const length_value = value.get(this.globalThis, "size") orelse JSC.JSValue.jsNumberFromInt32(0); + const length = length_value.toInt32(); + + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + this.writeIndent(Writer, writer_) catch {}; + + const set_name = if (value.jsType() == .JSWeakSet) "WeakSet" else "Set"; + + if (length == 0) { + return writer.print("{s} {{}}", .{set_name}); + } + + writer.print("\n{s} {{\n", .{set_name}); + { + this.indent += 1; + defer this.indent -|= 1; + var iter = SetIterator(Writer, enable_ansi_colors){ + .formatter = this, + .writer = writer_, + }; + value.forEach(this.globalThis, &iter, @TypeOf(iter).forEach); + } + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + writer.writeAll("\n"); + }, + .JSON => { + var str = ZigString.init(""); + value.jsonStringify(this.globalThis, this.indent, &str); + this.addForNewLine(str.len); + if (jsType == JSValue.JSType.JSDate) { + // in the code for printing dates, it never exceeds this amount + var iso_string_buf: [36]u8 = undefined; + var out_buf: []const u8 = std.fmt.bufPrint(&iso_string_buf, "{}", .{str}) catch ""; + if (out_buf.len > 2) { + // trim the quotes + out_buf = out_buf[1 .. out_buf.len - 1]; + } + + writer.print(comptime Output.prettyFmt("<r><magenta>{s}<r>", enable_ansi_colors), .{out_buf}); + return; + } + + writer.print("{}", .{str}); + }, + .Event => { + const event_type = EventType.map.getWithEql(value.get(this.globalThis, "type").?.getZigString(this.globalThis), ZigString.eqlComptime) orelse EventType.unknown; + if (event_type != .MessageEvent and event_type != .ErrorEvent) { + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + } + + writer.print( + comptime Output.prettyFmt("<r><cyan>{s}<r> {{\n", enable_ansi_colors), + .{ + @tagName(event_type), + }, + ); + { + this.indent += 1; + defer this.indent -|= 1; + const old_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = old_quote_strings; + this.writeIndent(Writer, writer_) catch unreachable; + + writer.print( + comptime Output.prettyFmt("<r>type: <green>\"{s}\"<r><d>,<r>\n", enable_ansi_colors), + .{ + event_type.label(), + }, + ); + this.writeIndent(Writer, writer_) catch unreachable; + + switch (event_type) { + .MessageEvent => { + writer.print( + comptime Output.prettyFmt("<r><blue>data<d>:<r> ", enable_ansi_colors), + .{}, + ); + const data = value.get(this.globalThis, "data").?; + const tag = Tag.get(data, this.globalThis); + if (tag.cell.isStringLike()) { + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + } else { + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + } + }, + .ErrorEvent => { + writer.print( + comptime Output.prettyFmt("<r><blue>error<d>:<r>\n", enable_ansi_colors), + .{}, + ); + + const data = value.get(this.globalThis, "error").?; + const tag = Tag.get(data, this.globalThis); + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + }, + else => unreachable, + } + writer.writeAll("\n"); + } + + this.writeIndent(Writer, writer_) catch unreachable; + writer.writeAll("}"); + }, + .JSX => { + writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + + writer.writeAll("<"); + + var needs_space = false; + var tag_name_str = ZigString.init(""); + + var tag_name_slice: ZigString.Slice = ZigString.Slice.empty; + var is_tag_kind_primitive = false; + + defer if (tag_name_slice.isAllocated()) tag_name_slice.deinit(); + + if (value.get(this.globalThis, "type")) |type_value| { + const _tag = Tag.get(type_value, this.globalThis); + + if (_tag.cell == .Symbol) {} else if (_tag.cell.isStringLike()) { + type_value.toZigString(&tag_name_str, this.globalThis); + is_tag_kind_primitive = true; + } else if (_tag.cell.isObject() or type_value.isCallable(this.globalThis.vm())) { + type_value.getNameProperty(this.globalThis, &tag_name_str); + if (tag_name_str.len == 0) { + tag_name_str = ZigString.init("NoName"); + } + } else { + type_value.toZigString(&tag_name_str, this.globalThis); + } + + tag_name_slice = tag_name_str.toSlice(default_allocator); + needs_space = true; + } else { + tag_name_slice = ZigString.init("unknown").toSlice(default_allocator); + + needs_space = true; + } + + if (!is_tag_kind_primitive) + writer.writeAll(comptime Output.prettyFmt("<cyan>", enable_ansi_colors)) + else + writer.writeAll(comptime Output.prettyFmt("<green>", enable_ansi_colors)); + writer.writeAll(tag_name_slice.slice()); + if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + + if (value.get(this.globalThis, "key")) |key_value| { + if (!key_value.isUndefinedOrNull()) { + if (needs_space) + writer.writeAll(" key=") + else + writer.writeAll("key="); + + const old_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = old_quote_strings; + + this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); + + needs_space = true; + } + } + + if (value.get(this.globalThis, "props")) |props| { + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + var props_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = true, + + .include_value = true, + }).init(this.globalThis, props.asObjectRef()); + defer props_iter.deinit(); + + var children_prop = props.get(this.globalThis, "children"); + if (props_iter.len > 0) { + { + this.indent += 1; + defer this.indent -|= 1; + const count_without_children = props_iter.len - @as(usize, @boolToInt(children_prop != null)); + + while (props_iter.next()) |prop| { + if (prop.eqlComptime("children")) + continue; + + var property_value = props_iter.value; + const tag = Tag.get(property_value, this.globalThis); + + if (tag.cell.isHidden()) continue; + + if (needs_space) writer.writeAll(" "); + needs_space = false; + + writer.print( + comptime Output.prettyFmt("<r><blue>{s}<d>=<r>", enable_ansi_colors), + .{prop.trunc(128)}, + ); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r><green>", true)); + } + } + + this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); + + if (tag.cell.isStringLike()) { + if (comptime enable_ansi_colors) { + writer.writeAll(comptime Output.prettyFmt("<r>", true)); + } + } + + if ( + // count_without_children is necessary to prevent printing an extra newline + // if there are children and one prop and the child prop is the last prop + props_iter.i + 1 < count_without_children and + // 3 is arbitrary but basically + // <input type="text" value="foo" /> + // ^ should be one line + // <input type="text" value="foo" bar="true" baz={false} /> + // ^ should be multiple lines + props_iter.i > 3) + { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } else if (props_iter.i + 1 < count_without_children) { + writer.writeAll(" "); + } + } + } + + if (children_prop) |children| { + const tag = Tag.get(children, this.globalThis); + + const print_children = switch (tag.tag) { + .String, .JSX, .Array => true, + else => false, + }; + + if (print_children) { + print_children: { + switch (tag.tag) { + .String => { + var children_string = children.getZigString(this.globalThis); + if (children_string.len == 0) break :print_children; + if (comptime enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", true)); + + writer.writeAll(">"); + if (children_string.len < 128) { + writer.writeString(children_string); + } else { + this.indent += 1; + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + this.indent -|= 1; + writer.writeString(children_string); + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } + }, + .JSX => { + writer.writeAll(">\n"); + + { + this.indent += 1; + this.writeIndent(Writer, writer_) catch unreachable; + defer this.indent -|= 1; + this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); + } + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + }, + .Array => { + const length = children.getLengthOfArray(this.globalThis); + if (length == 0) break :print_children; + writer.writeAll(">\n"); + + { + this.indent += 1; + this.writeIndent(Writer, writer_) catch unreachable; + const _prev_quote_strings = this.quote_strings; + this.quote_strings = false; + defer this.quote_strings = _prev_quote_strings; + + defer this.indent -|= 1; + + var j: usize = 0; + while (j < length) : (j += 1) { + const child = JSC.JSObject.getIndex(children, this.globalThis, @intCast(u32, j)); + this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); + if (j + 1 < length) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + } + } + } + + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch unreachable; + }, + else => unreachable, + } + + writer.writeAll("</"); + if (!is_tag_kind_primitive) + writer.writeAll(comptime Output.prettyFmt("<r><cyan>", enable_ansi_colors)) + else + writer.writeAll(comptime Output.prettyFmt("<r><green>", enable_ansi_colors)); + writer.writeAll(tag_name_slice.slice()); + if (enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", enable_ansi_colors)); + writer.writeAll(">"); + } + + return; + } + } + } + } + + writer.writeAll(" />"); + }, + .Object => { + const prev_quote_strings = this.quote_strings; + this.quote_strings = true; + defer this.quote_strings = prev_quote_strings; + + const Iterator = PropertyIterator(Writer, enable_ansi_colors); + + // We want to figure out if we should print this object + // on one line or multiple lines + // + // The 100% correct way would be to print everything to + // a temporary buffer and then check how long each line was + // + // But it's important that console.log() is fast. So we + // do a small compromise to avoid multiple passes over input + // + // We say: + // + // If the object has at least 2 properties and ANY of the following conditions are met: + // - total length of all the property names is more than + // 14 characters + // - the parent object is printing each property on a new line + // - The first property is a DOM object, ESM namespace, Map, Set, or Blob + // + // Then, we print it each property on a new line, recursively. + // + const prev_always_newline_scope = this.always_newline_scope; + defer this.always_newline_scope = prev_always_newline_scope; + var iter = Iterator{ + .formatter = this, + .writer = writer_, + .always_newline = this.always_newline_scope or this.goodTimeForANewLine(), + .parent = value, + }; + + value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach); + + if (iter.i == 0) { + var object_name = ZigString.Empty; + value.getClassName(this.globalThis, &object_name); + + if (!strings.eqlComptime(object_name.slice(), "Object")) { + if (value.as(JSC.Jest.ExpectAny)) |_| { + var constructor = JSC.Jest.ExpectAny.constructorValueGetCached(value) orelse unreachable; + var constructor_name = ZigString.Empty; + constructor.getNameProperty(this.globalThis, &constructor_name); + writer.print("Any<{s}>", .{constructor_name}); + } else { + writer.print("{s} {{}}", .{object_name}); + } + } else { + // don't write "Object" + writer.writeAll("{}"); + } + } else { + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + + if (iter.always_newline) { + this.indent -|= 1; + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + this.estimated_line_length += 1; + } else { + this.estimated_line_length += 2; + writer.writeAll(" }"); + } + + if (this.indent == 0) { + writer.writeAll("\n"); + } + } + }, + .TypedArray => { + const arrayBuffer = value.asArrayBuffer(this.globalThis).?; + const slice = arrayBuffer.byteSlice(); + + if (this.indent == 0 and slice.len > 0) { + writer.writeAll("\n"); + } + + if (jsType == .Uint8Array) { + var buffer_name = ZigString.Empty; + value.getClassName(this.globalThis, &buffer_name); + if (strings.eqlComptime(buffer_name.slice(), "Buffer")) { + // special formatting for 'Buffer' snapshots only + if (slice.len == 0 and this.indent == 0) writer.writeAll("\n"); + writer.writeAll("{\n"); + this.indent += 1; + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("\"data\": ["); + + this.indent += 1; + for (slice) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + this.indent -|= 1; + + if (slice.len > 0) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("],\n"); + } else { + writer.writeAll("],\n"); + } + + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("\"type\": \"Buffer\",\n"); + + this.indent -|= 1; + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("}"); + + if (this.indent == 0) { + writer.writeAll("\n"); + } + + return; + } + writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); + } else { + writer.writeAll(bun.asByteSlice(@tagName(arrayBuffer.typed_array_type))); + } + + writer.writeAll(" ["); + + if (slice.len > 0) { + switch (jsType) { + .Int8Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i8), std.mem.bytesAsSlice(i8, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Int16Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i16), std.mem.bytesAsSlice(i16, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Uint16Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u16), std.mem.bytesAsSlice(u16, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Int32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i32), std.mem.bytesAsSlice(i32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Uint32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u32), std.mem.bytesAsSlice(u32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Float32Array => { + const slice_with_type = @alignCast(std.meta.alignment([]f32), std.mem.bytesAsSlice(f32, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .Float64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]f64), std.mem.bytesAsSlice(f64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .BigInt64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]i64), std.mem.bytesAsSlice(i64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + .BigUint64Array => { + const slice_with_type = @alignCast(std.meta.alignment([]u64), std.mem.bytesAsSlice(u64, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + + // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer + else => { + var slice_with_type = @alignCast(std.meta.alignment([]u8), std.mem.bytesAsSlice(u8, slice)); + this.indent += 1; + defer this.indent -|= 1; + for (slice_with_type) |el| { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.print("{d},", .{el}); + } + }, + } + } + + if (slice.len > 0) { + writer.writeAll("\n"); + this.writeIndent(Writer, writer_) catch {}; + writer.writeAll("]"); + if (this.indent == 0) { + writer.writeAll("\n"); + } + } else { + writer.writeAll("]"); + } + }, + else => {}, + } + } + + pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) void { + if (comptime is_bindgen) { + return; + } + var prevGlobalThis = this.globalThis; + defer this.globalThis = prevGlobalThis; + this.globalThis = globalThis; + + // This looks incredibly redundant. We make the JestPrettyFormat.Formatter.Tag a + // comptime var so we have to repeat it here. The rationale there is + // it _should_ limit the stack usage because each version of the + // function will be relatively small + return switch (result.tag) { + .StringPossiblyFormatted => this.printAs(.StringPossiblyFormatted, Writer, writer, value, result.cell, enable_ansi_colors), + .String => this.printAs(.String, Writer, writer, value, result.cell, enable_ansi_colors), + .Undefined => this.printAs(.Undefined, Writer, writer, value, result.cell, enable_ansi_colors), + .Double => this.printAs(.Double, Writer, writer, value, result.cell, enable_ansi_colors), + .Integer => this.printAs(.Integer, Writer, writer, value, result.cell, enable_ansi_colors), + .Null => this.printAs(.Null, Writer, writer, value, result.cell, enable_ansi_colors), + .Boolean => this.printAs(.Boolean, Writer, writer, value, result.cell, enable_ansi_colors), + .Array => this.printAs(.Array, Writer, writer, value, result.cell, enable_ansi_colors), + .Object => this.printAs(.Object, Writer, writer, value, result.cell, enable_ansi_colors), + .Function => this.printAs(.Function, Writer, writer, value, result.cell, enable_ansi_colors), + .Class => this.printAs(.Class, Writer, writer, value, result.cell, enable_ansi_colors), + .Error => this.printAs(.Error, Writer, writer, value, result.cell, enable_ansi_colors), + .ArrayBuffer, .TypedArray => this.printAs(.TypedArray, Writer, writer, value, result.cell, enable_ansi_colors), + .Map => this.printAs(.Map, Writer, writer, value, result.cell, enable_ansi_colors), + .Set => this.printAs(.Set, Writer, writer, value, result.cell, enable_ansi_colors), + .Symbol => this.printAs(.Symbol, Writer, writer, value, result.cell, enable_ansi_colors), + .BigInt => this.printAs(.BigInt, Writer, writer, value, result.cell, enable_ansi_colors), + .GlobalObject => this.printAs(.GlobalObject, Writer, writer, value, result.cell, enable_ansi_colors), + .Private => this.printAs(.Private, Writer, writer, value, result.cell, enable_ansi_colors), + .Promise => this.printAs(.Promise, Writer, writer, value, result.cell, enable_ansi_colors), + .JSON => this.printAs(.JSON, Writer, writer, value, result.cell, enable_ansi_colors), + .NativeCode => this.printAs(.NativeCode, Writer, writer, value, result.cell, enable_ansi_colors), + .JSX => this.printAs(.JSX, Writer, writer, value, result.cell, enable_ansi_colors), + .Event => this.printAs(.Event, Writer, writer, value, result.cell, enable_ansi_colors), + }; + } + }; +}; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 5a41f5f8c..b670a3aaa 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -307,7 +307,7 @@ pub const Blob = struct { }, ); } - pub fn writeFormat(this: *const Blob, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + pub fn writeFormat(this: *const Blob, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); if (this.isDetached()) { diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index d4bd490c1..509d02989 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -75,7 +75,7 @@ pub const Body = struct { }; } - pub fn writeFormat(this: *const Body, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + pub fn writeFormat(this: *const Body, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try formatter.writeIndent(Writer, writer); @@ -98,7 +98,7 @@ pub const Body = struct { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - try this.value.Blob.writeFormat(formatter, writer, enable_ansi_colors); + try this.value.Blob.writeFormat(Formatter, formatter, writer, enable_ansi_colors); } else if (this.value == .InternalBlob) { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index e1fa4a9bb..6581649fc 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -116,7 +116,7 @@ pub const Request = struct { }; } - pub fn writeFormat(this: *Request, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + pub fn writeFormat(this: *Request, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try writer.print("Request ({}) {{\n", .{bun.fmt.size(this.body.slice().len)}); { @@ -139,12 +139,12 @@ pub const Request = struct { if (this.body == .Blob) { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); - try this.body.Blob.writeFormat(formatter, writer, enable_ansi_colors); + try this.body.Blob.writeFormat(Formatter, formatter, writer, enable_ansi_colors); } else if (this.body == .InternalBlob) { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); if (this.body.size() == 0) { - try Blob.initEmpty(undefined).writeFormat(formatter, writer, enable_ansi_colors); + try Blob.initEmpty(undefined).writeFormat(Formatter, formatter, writer, enable_ansi_colors); } else { try Blob.writeFormatForSize(this.body.size(), writer, enable_ansi_colors); } diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 06323d790..f36af88cf 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -113,7 +113,7 @@ pub const Response = struct { pub const Props = struct {}; - pub fn writeFormat(this: *const Response, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + pub fn writeFormat(this: *const Response, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())}); { @@ -145,7 +145,7 @@ pub const Response = struct { formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); formatter.resetLine(); - try this.body.writeFormat(formatter, writer, enable_ansi_colors); + try this.body.writeFormat(Formatter, formatter, writer, enable_ansi_colors); } try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); |