diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e143d69342..9feff27121 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -290,7 +290,7 @@ $ xcode-select --install Bun defaults to linking `libatomic` statically, as not all systems have it. If you are building on a distro that does not have a static libatomic available, you can run the following command to enable dynamic linking: ```bash -$ bun setup -DUSE_STATIC_LIBATOMIC=OFF +$ bun run build -DUSE_STATIC_LIBATOMIC=OFF ``` The built version of Bun may not work on other systems if compiled this way. diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 36137c50cb..726a94a4b4 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -138,7 +138,7 @@ if(CMAKE_HOST_LINUX AND NOT WIN32 AND NOT APPLE) OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) - if(LINUX_DISTRO MATCHES "NAME=\"(Arch|Manjaro|Artix) Linux\"|NAME=\"openSUSE Tumbleweed\"") + if(LINUX_DISTRO MATCHES "NAME=\"(Arch|Manjaro|Artix) Linux( ARM)?\"|NAME=\"openSUSE Tumbleweed\"") set(DEFAULT_STATIC_LIBATOMIC OFF) endif() endif() diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 7cc8057156..0651256cd5 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1474,6 +1474,12 @@ fn NewSocket(comptime ssl: bool) type { if (vm.isShuttingDown()) { return; } + this.ref(); + defer this.deref(); + this.internalFlush(); + // is not writable if we have buffered data or if we are already detached + if (this.buffered_data_for_node_net.len > 0 or this.socket.isDetached()) return; + vm.eventLoop().enter(); defer vm.eventLoop().exit(); @@ -2363,15 +2369,10 @@ fn NewSocket(comptime ssl: bool) type { }; } - pub fn flush( - this: *This, - _: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) JSValue { - JSC.markBinding(@src()); + fn internalFlush(this: *This) void { if (this.buffered_data_for_node_net.len > 0) { const written: usize = @intCast(@max(this.socket.write(this.buffered_data_for_node_net.slice(), false), 0)); - + this.bytes_written += written; if (written > 0) { if (this.buffered_data_for_node_net.len > written) { const remaining = this.buffered_data_for_node_net.slice()[written..]; @@ -2385,6 +2386,15 @@ fn NewSocket(comptime ssl: bool) type { } this.socket.flush(); + } + + pub fn flush( + this: *This, + _: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) JSValue { + JSC.markBinding(@src()); + this.internalFlush(); return JSValue.jsUndefined(); } @@ -2706,6 +2716,7 @@ fn NewSocket(comptime ssl: bool) type { ) JSValue { return JSC.JSValue.jsNumber(this.bytes_written + this.buffered_data_for_node_net.len); } + pub fn getALPNProtocol( this: *This, globalObject: *JSC.JSGlobalObject, diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 0efe72a66b..b3a1933734 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1453,7 +1453,9 @@ pub const ServerConfig = struct { } if (arg.getTruthy(global, "tls")) |tls| { - if (tls.jsType().isArray()) { + if (tls.isFalsey()) { + args.ssl_config = null; + } else if (tls.jsType().isArray()) { var value_iter = tls.arrayIterator(global); if (value_iter.len == 1) { JSC.throwInvalidArguments("tls option expects at least 1 tls object", .{}, global, exception); diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index ee3e60e1df..a3a06da9d8 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -83,9 +83,6 @@ function generate(ssl) { alpnProtocol: { getter: "getALPNProtocol", }, - bytesWritten: { - getter: "getBytesWritten", - }, write: { fn: "write", length: 3, @@ -169,7 +166,6 @@ function generate(ssl) { bytesWritten: { getter: "getBytesWritten", }, - setServername: { fn: "setServername", length: 1, diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 692ad34a3e..ed8185e191 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -45,6 +45,7 @@ export default [ ["ERR_BUFFER_OUT_OF_BOUNDS", RangeError, "RangeError"], ["ERR_UNKNOWN_SIGNAL", TypeError, "TypeError"], ["ERR_SOCKET_BAD_PORT", RangeError, "RangeError"], + ["ERR_STREAM_RELEASE_LOCK", Error, "AbortError"], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError, "TypeError"], diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 13ec88ee17..30288d4a18 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3889,10 +3889,6 @@ bool JSC__JSValue__symbolKeyFor(JSC__JSValue symbolValue_, JSC__JSGlobalObject* return true; } -bool JSC__JSValue__toBoolean(JSC__JSValue JSValue0) -{ - return JSC::JSValue::decode(JSValue0).asBoolean(); -} int32_t JSC__JSValue__toInt32(JSC__JSValue JSValue0) { return JSC::JSValue::decode(JSValue0).asInt32(); @@ -5004,6 +5000,7 @@ enum class BuiltinNamesMap : uint8_t { message, error, defaultKeyword, + encoding, }; static const JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigned char name) @@ -5065,6 +5062,9 @@ static const JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, u case BuiltinNamesMap::defaultKeyword: { return vm.propertyNames->defaultKeyword; } + case BuiltinNamesMap::encoding: { + return clientData->builtinNames().encodingPublicName(); + } default: { ASSERT_NOT_REACHED(); return Identifier(); @@ -5100,9 +5100,10 @@ extern "C" JSC__JSValue JSC__JSValue__fastGetOwn(JSC__JSValue JSValue0, JSC__JSG return JSValue::encode({}); } -bool JSC__JSValue__toBooleanSlow(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject) +bool JSC__JSValue__toBoolean(JSC__JSValue JSValue0) { - return JSValue::decode(JSValue0).toBoolean(globalObject); + // We count masquerades as undefined as true. + return JSValue::decode(JSValue0).pureToBoolean() != TriState::False; } template diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index aec7351cd9..401227ab3e 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4019,6 +4019,12 @@ pub const JSValue = enum(JSValueReprInt) { return JSC__JSValue__getDirectIndex(this, globalThis, i); } + pub fn isFalsey(this: JSValue) bool { + return !this.toBoolean(); + } + + pub const isTruthy = toBoolean; + const PropertyIteratorFn = *const fn ( globalObject_: *JSGlobalObject, ctx_ptr: ?*anyopaque, @@ -4069,7 +4075,7 @@ pub const JSValue = enum(JSValueReprInt) { pub fn coerce(this: JSValue, comptime T: type, globalThis: *JSC.JSGlobalObject) T { return switch (T) { ZigString => this.getZigString(globalThis), - bool => this.toBooleanSlow(globalThis), + bool => this.toBoolean(), f64 => { if (this.isDouble()) { return this.asDouble(); @@ -5177,6 +5183,7 @@ pub const JSValue = enum(JSValueReprInt) { message, @"error", default, + encoding, pub fn has(property: []const u8) bool { return bun.ComptimeEnumMap(BuiltinName).has(property); @@ -5725,16 +5732,8 @@ pub const JSValue = enum(JSValueReprInt) { return fromPtrAddress(@intFromPtr(addr)); } - pub fn toBooleanSlow(this: JSValue, global: *JSGlobalObject) bool { - return cppFn("toBooleanSlow", .{ this, global }); - } - pub fn toBoolean(this: JSValue) bool { - if (isEmptyOrUndefinedOrNull(this)) { - return false; - } - - return asBoolean(this); + return this != .zero and cppFn("toBoolean", .{this}); } pub fn asBoolean(this: JSValue) bool { @@ -6024,7 +6023,6 @@ pub const JSValue = enum(JSValueReprInt) { "symbolFor", "symbolKeyFor", "toBoolean", - "toBooleanSlow", "toError_", "toInt32", "toInt64", diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 6d5efdb701..7cc6afdecd 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -394,7 +394,6 @@ CPP_DECL bool JSC__JSValue__stringIncludes(JSC__JSValue JSValue0, JSC__JSGlobalO CPP_DECL JSC__JSValue JSC__JSValue__symbolFor(JSC__JSGlobalObject* arg0, ZigString* arg1); CPP_DECL bool JSC__JSValue__symbolKeyFor(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, ZigString* arg2); CPP_DECL bool JSC__JSValue__toBoolean(JSC__JSValue JSValue0); -CPP_DECL bool JSC__JSValue__toBooleanSlow(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__JSValue JSC__JSValue__toError_(JSC__JSValue JSValue0); CPP_DECL int32_t JSC__JSValue__toInt32(JSC__JSValue JSValue0); CPP_DECL int64_t JSC__JSValue__toInt64(JSC__JSValue JSValue0); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index ec358ceb3e..5718a8c39b 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -289,7 +289,6 @@ pub extern fn JSC__JSValue__stringIncludes(JSValue0: JSC__JSValue, arg1: *bindin pub extern fn JSC__JSValue__symbolFor(arg0: *bindings.JSGlobalObject, arg1: [*c]ZigString) JSC__JSValue; pub extern fn JSC__JSValue__symbolKeyFor(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: [*c]ZigString) bool; pub extern fn JSC__JSValue__toBoolean(JSValue0: JSC__JSValue) bool; -pub extern fn JSC__JSValue__toBooleanSlow(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject) bool; pub extern fn JSC__JSValue__toError_(JSValue0: JSC__JSValue) JSC__JSValue; pub extern fn JSC__JSValue__toInt32(JSValue0: JSC__JSValue) i32; pub extern fn JSC__JSValue__toInt64(JSValue0: JSC__JSValue) i64; diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 43febb0bc1..cef77ceb12 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -828,6 +828,7 @@ extern "C" napi_status napi_has_named_property(napi_env env, napi_value object, auto globalObject = toJS(env); auto& vm = globalObject->vm(); + auto scope = DECLARE_CATCH_SCOPE(vm); JSObject* target = toJS(object).toObject(globalObject); NAPI_RETURN_IF_EXCEPTION(env); diff --git a/src/bun.js/modules/NodeBufferModule.h b/src/bun.js/modules/NodeBufferModule.h index a86ebb3a12..e53e29950c 100644 --- a/src/bun.js/modules/NodeBufferModule.h +++ b/src/bun.js/modules/NodeBufferModule.h @@ -211,8 +211,10 @@ DEFINE_NATIVE_MODULE(NodeBuffer) put(JSC::Identifier::fromString(vm, "transcode"_s), transcode); - auto* resolveObjectURL = InternalFunction::createFunctionThatMasqueradesAsUndefined( + auto* resolveObjectURL = JSC::JSFunction::create( vm, globalObject, 1, "resolveObjectURL"_s, + jsFunctionResolveObjectURL, + ImplementationVisibility::Public, NoIntrinsic, jsFunctionResolveObjectURL); put(JSC::Identifier::fromString(vm, "resolveObjectURL"_s), resolveObjectURL); diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 6210f09257..99a497e48e 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -1985,17 +1985,17 @@ pub const Arguments = struct { const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { - if (next_val.isCallable(ctx.ptr().vm())) break :brk false; + if (next_val.isCallable(ctx.vm())) break :brk false; arguments.eat(); - if (next_val.getOptional(ctx.ptr(), "throwIfNoEntry", bool) catch { + if (next_val.getOptional(ctx, "throwIfNoEntry", bool) catch { path.deinit(); return null; }) |throw_if_no_entry_val| { throw_if_no_entry = throw_if_no_entry_val; } - if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch { + if (next_val.getOptional(ctx, "bigint", bool) catch { path.deinit(); return null; }) |big_int| { @@ -2048,10 +2048,10 @@ pub const Arguments = struct { const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { - if (next_val.isCallable(ctx.ptr().vm())) break :brk false; + if (next_val.isCallable(ctx.vm())) break :brk false; arguments.eat(); - if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch false) |big_int| { + if (next_val.getOptional(ctx, "bigint", bool) catch false) |big_int| { break :brk big_int; } } @@ -2193,7 +2193,7 @@ pub const Arguments = struct { // will automatically be normalized to absolute path. if (next_val.isString()) { arguments.eat(); - var str = next_val.toBunString(ctx.ptr()); + var str = next_val.toBunString(ctx); defer str.deref(); if (str.eqlComptime("dir")) break :link_type .dir; if (str.eqlComptime("file")) break :link_type .file; @@ -2263,18 +2263,23 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; + // Exception handled below. + encoding = Encoding.assert(val, ctx, encoding) catch encoding; }, else => { if (val.isObject()) { - if (val.get(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; - } + // Exception handled below. + encoding = getEncoding(val, ctx, encoding) catch encoding; } }, } } + if (ctx.hasException()) { + path.deinit(); + return null; + } + return Readlink{ .path = path, .encoding = encoding }; } }; @@ -2315,22 +2320,35 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; + // Exception handled below. + encoding = Encoding.assert(val, ctx, encoding) catch encoding; }, else => { if (val.isObject()) { - if (val.get(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; - } + // Exception handled below. + encoding = getEncoding(val, ctx, encoding) catch encoding; } }, } } + if (ctx.hasException()) { + path.deinit(); + return null; + } + return Realpath{ .path = path, .encoding = encoding }; } }; + fn getEncoding(object: JSC.JSValue, globalObject: *JSC.JSGlobalObject, default: Encoding) !Encoding { + if (object.fastGet(globalObject, .encoding)) |value| { + return Encoding.assert(value, globalObject, default); + } + + return default; + } + pub const Unlink = struct { path: PathLike, @@ -2411,14 +2429,14 @@ pub const Arguments = struct { arguments.eat(); if (val.isObject()) { - if (val.getOptional(ctx.ptr(), "recursive", bool) catch { + if (val.getOptional(ctx, "recursive", bool) catch { path.deinit(); return null; }) |boolean| { recursive = boolean; } - if (val.getOptional(ctx.ptr(), "force", bool) catch { + if (val.getOptional(ctx, "force", bool) catch { path.deinit(); return null; }) |boolean| { @@ -2483,14 +2501,14 @@ pub const Arguments = struct { arguments.eat(); if (val.isObject()) { - if (val.getOptional(ctx.ptr(), "recursive", bool) catch { + if (val.getOptional(ctx, "recursive", bool) catch { path.deinit(); return null; }) |boolean| { recursive = boolean; } - if (val.get(ctx.ptr(), "mode")) |mode_| { + if (val.get(ctx, "mode")) |mode_| { mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse mode; if (exception.* != null) return null; } @@ -2547,18 +2565,22 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; + // exception handled below. + encoding = Encoding.assert(val, ctx, encoding) catch encoding; }, else => { if (val.isObject()) { - if (val.get(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; - } + encoding = getEncoding(val, ctx, encoding) catch encoding; } }, } } + if (ctx.hasException()) { + prefix.deinit(); + return null; + } + return MkdirTemp{ .prefix = prefix, .encoding = encoding, @@ -2618,22 +2640,28 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.assert(val, ctx, encoding) catch { + path.deinit(); + return null; + }; }, else => { if (val.isObject()) { - if (val.get(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; + encoding = getEncoding(val, ctx, encoding) catch encoding; + + if (ctx.hasException()) { + path.deinit(); + return null; } - if (val.getOptional(ctx.ptr(), "recursive", bool) catch { + if (val.getOptional(ctx, "recursive", bool) catch { path.deinit(); return null; }) |recursive_| { recursive = recursive_; } - if (val.getOptional(ctx.ptr(), "withFileTypes", bool) catch { + if (val.getOptional(ctx, "withFileTypes", bool) catch { path.deinit(); return null; }) |with_file_types_| { @@ -2727,12 +2755,12 @@ pub const Arguments = struct { arguments.eat(); if (val.isObject()) { - if (val.getTruthy(ctx.ptr(), "flags")) |flags_| { + if (val.getTruthy(ctx, "flags")) |flags_| { flags = FileSystemFlags.fromJS(ctx, flags_, exception) orelse flags; if (exception.* != null) return null; } - if (val.getTruthy(ctx.ptr(), "mode")) |mode_| { + if (val.getTruthy(ctx, "mode")) |mode_| { mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse mode; if (exception.* != null) return null; } @@ -2934,7 +2962,7 @@ pub const Arguments = struct { if (exception.* != null) return null; const buffer_value = arguments.next(); - const buffer = StringOrBuffer.fromJS(ctx.ptr(), bun.default_allocator, buffer_value orelse { + const buffer = StringOrBuffer.fromJS(ctx, bun.default_allocator, buffer_value orelse { if (exception.* == null) { JSC.throwInvalidArguments( "data is required", @@ -2981,7 +3009,10 @@ pub const Arguments = struct { } if (current.isString()) { - args.encoding = Encoding.fromJS(current, ctx.ptr()) orelse Encoding.utf8; + args.encoding = Encoding.assert(current, ctx, args.encoding) catch { + args.deinit(); + return null; + }; arguments.eat(); } }, @@ -3058,7 +3089,7 @@ pub const Arguments = struct { if (exception.* != null) return null; const buffer_value = arguments.next(); - const buffer: Buffer = Buffer.fromJS(ctx.ptr(), buffer_value orelse { + const buffer: Buffer = Buffer.fromJS(ctx, buffer_value orelse { if (exception.* == null) { JSC.throwInvalidArguments( "buffer is required", @@ -3115,20 +3146,20 @@ pub const Arguments = struct { } } } else if (current.isObject()) { - if (current.getTruthy(ctx.ptr(), "offset")) |num| { + if (current.getTruthy(ctx, "offset")) |num| { if (num.isNumber() or num.isBigInt()) { args.offset = num.to(u52); } } - if (current.getTruthy(ctx.ptr(), "length")) |num| { + if (current.getTruthy(ctx, "length")) |num| { if (num.isNumber() or num.isBigInt()) { args.length = num.to(u52); } defined_length = true; } - if (current.getTruthy(ctx.ptr(), "position")) |num| { + if (current.getTruthy(ctx, "position")) |num| { if (num.isNumber() or num.isBigInt()) { args.position = num.to(i52); } @@ -3194,35 +3225,19 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } + encoding = Encoding.assert(arg, ctx, encoding) catch { + path.deinit(); return null; }; } else if (arg.isObject()) { - if (arg.get(ctx.ptr(), "encoding")) |encoding_| { - if (!encoding_.isUndefinedOrNull()) { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } + encoding = getEncoding(arg, ctx, encoding) catch encoding; + + if (ctx.hasException()) { + path.deinit(); + return null; } - if (arg.getTruthy(ctx.ptr(), "flag")) |flag_| { + if (arg.getTruthy(ctx, "flag")) |flag_| { flag = FileSystemFlags.fromJS(ctx, flag_, exception) orelse { if (exception.* == null) { JSC.throwInvalidArguments( @@ -3235,6 +3250,11 @@ pub const Arguments = struct { return null; }; } + + if (ctx.hasException()) { + path.deinit(); + return null; + } } } @@ -3314,35 +3334,16 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { - defer file.deinit(); - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; + encoding = Encoding.assert(arg, ctx, encoding) catch encoding; } else if (arg.isObject()) { - if (arg.getTruthy(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { - defer file.deinit(); - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; + encoding = getEncoding(arg, ctx, encoding) catch encoding; + + if (ctx.hasException()) { + file.deinit(); + return null; } - if (arg.getTruthy(ctx.ptr(), "flag")) |flag_| { + if (arg.getTruthy(ctx, "flag")) |flag_| { flag = FileSystemFlags.fromJS(ctx, flag_, exception) orelse { defer file.deinit(); if (exception.* == null) { @@ -3357,7 +3358,7 @@ pub const Arguments = struct { }; } - if (arg.getTruthy(ctx.ptr(), "mode")) |mode_| { + if (arg.getTruthy(ctx, "mode")) |mode_| { mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse { defer file.deinit(); if (exception.* == null) { @@ -3374,7 +3375,12 @@ pub const Arguments = struct { } } - const data = StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx.ptr(), bun.default_allocator, data_value, encoding, arguments.will_be_async) orelse { + if (ctx.hasException()) { + file.deinit(); + return null; + } + + const data = StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx, bun.default_allocator, data_value, encoding, arguments.will_be_async) orelse { defer file.deinit(); if (exception.* == null) { JSC.throwInvalidArguments( @@ -3433,35 +3439,18 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; + encoding = Encoding.assert(arg, ctx, encoding) catch encoding; } else if (arg.isObject()) { - if (arg.get(ctx.ptr(), "encoding")) |encoding_| { - if (!encoding_.isUndefinedOrNull()) { - encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } + if (getEncoding(arg, ctx)) |encoding_| { + encoding = encoding_; } - if (arg.get(ctx.ptr(), "bufferSize")) |buffer_size_| { + if (ctx.hasException()) { + path.deinit(); + return null; + } + + if (arg.get(ctx, "bufferSize")) |buffer_size_| { buffer_size = buffer_size_.toInt32(); if (buffer_size < 0) { if (exception.* == null) { @@ -3478,6 +3467,11 @@ pub const Arguments = struct { } } + if (ctx.hasException()) { + path.deinit(); + return null; + } + return OpenDir{ .path = path, .encoding = encoding, @@ -3571,275 +3565,6 @@ pub const Arguments = struct { } }; - pub const CreateReadStream = struct { - file: PathOrFileDescriptor, - flags: FileSystemFlags = FileSystemFlags.r, - encoding: Encoding = Encoding.utf8, - mode: Mode = default_permission, - autoClose: bool = true, - emitClose: bool = true, - start: i32 = 0, - end: i32 = std.math.maxInt(i32), - highwater_mark: u32 = 64 * 1024, - global_object: *JSC.JSGlobalObject, - - pub fn deinit(this: CreateReadStream) void { - this.file.deinit(); - } - - pub fn copyToState(this: CreateReadStream, state: *JSC.Node.Readable.State) void { - state.encoding = this.encoding; - state.highwater_mark = this.highwater_mark; - state.start = this.start; - state.end = this.end; - } - - pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?CreateReadStream { - const path = PathLike.fromJS(ctx, arguments, exception); - if (exception.* != null) return null; - if (path == null) arguments.eat(); - - var stream = CreateReadStream{ - .file = undefined, - .global_object = ctx.ptr(), - }; - var fd = FileDescriptor.invalid; - - if (arguments.next()) |arg| { - arguments.eat(); - if (arg.isString()) { - stream.encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } else if (arg.isObject()) { - if (arg.get(ctx.ptr(), "mode")) |mode_| { - stream.mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid mode", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "encoding")) |encoding| { - stream.encoding = Encoding.fromJS(encoding, ctx.ptr()) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.getTruthy(ctx.ptr(), "flags")) |flags| { - stream.flags = FileSystemFlags.fromJS(ctx, flags, exception) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid flags", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "fd")) |flags| { - fd = JSC.Node.fileDescriptorFromJS(ctx, flags, exception) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid file descriptor", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "autoClose")) |autoClose| { - stream.autoClose = autoClose.toBoolean(); - } - - if (arg.get(ctx.ptr(), "emitClose")) |emitClose| { - stream.emitClose = emitClose.toBoolean(); - } - - if (arg.get(ctx.ptr(), "start")) |start| { - stream.start = start.coerce(i32, ctx); - } - - if (arg.get(ctx.ptr(), "end")) |end| { - stream.end = end.coerce(i32, ctx); - } - - if (arg.get(ctx.ptr(), "highWaterMark")) |highwaterMark| { - stream.highwater_mark = highwaterMark.toU32(); - } - } - } - - if (fd.isValid()) { - stream.file = .{ .fd = fd }; - } else if (path) |path_| { - stream.file = .{ .path = path_ }; - } else { - JSC.throwInvalidArguments("Missing path or file descriptor", .{}, ctx, exception); - return null; - } - return stream; - } - }; - - pub const CreateWriteStream = struct { - file: PathOrFileDescriptor, - flags: FileSystemFlags = FileSystemFlags.w, - encoding: Encoding = Encoding.utf8, - mode: Mode = default_permission, - autoClose: bool = true, - emitClose: bool = true, - start: i32 = 0, - highwater_mark: u32 = 256 * 1024, - global_object: *JSC.JSGlobalObject, - - pub fn deinit(this: @This()) void { - this.file.deinit(); - } - - pub fn copyToState(this: CreateWriteStream, state: *JSC.Node.Writable.State) void { - state.encoding = this.encoding; - state.highwater_mark = this.highwater_mark; - state.start = this.start; - state.emit_close = this.emitClose; - } - - pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?CreateWriteStream { - const path = PathLike.fromJS(ctx, arguments, exception); - if (exception.* != null) return null; - if (path == null) arguments.eat(); - - var stream = CreateWriteStream{ - .file = undefined, - .global_object = ctx.ptr(), - }; - var fd: FileDescriptor = bun.invalid_fd; - - if (arguments.next()) |arg| { - arguments.eat(); - if (arg.isString()) { - stream.encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } else if (arg.isObject()) { - if (arg.get(ctx.ptr(), "mode")) |mode_| { - stream.mode = JSC.Node.modeFromJS(ctx, mode_, exception) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid mode", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "encoding")) |encoding| { - stream.encoding = Encoding.fromJS(encoding, ctx.ptr()) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid encoding", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.getTruthy(ctx.ptr(), "flags")) |flags| { - stream.flags = FileSystemFlags.fromJS(ctx, flags, exception) orelse { - if (exception.* == null) { - JSC.throwInvalidArguments( - "Invalid flags", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "fd")) |flags| { - fd = JSC.Node.fileDescriptorFromJS(ctx, flags, exception) orelse { - if (exception.* != null) { - JSC.throwInvalidArguments( - "Invalid file descriptor", - .{}, - ctx, - exception, - ); - } - return null; - }; - } - - if (arg.get(ctx.ptr(), "autoClose")) |autoClose| { - stream.autoClose = autoClose.toBoolean(); - } - - if (arg.get(ctx.ptr(), "emitClose")) |emitClose| { - stream.emitClose = emitClose.toBoolean(); - } - - if (arg.get(ctx.ptr(), "start")) |start| { - stream.start = start.toInt32(); - } - } - } - - if (fd != bun.invalid_fd) { - stream.file = .{ .fd = fd }; - } else if (path) |path_| { - stream.file = .{ .path = path_ }; - } else { - JSC.throwInvalidArguments("Missing path or file descriptor", .{}, ctx, exception); - return null; - } - return stream; - } - }; - pub const FdataSync = struct { fd: FileDescriptor, @@ -6617,14 +6342,6 @@ pub const NodeFS = struct { return args.createFSWatcher(); } - pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) { - return Maybe(Return.CreateReadStream).todo(); - } - - pub fn createWriteStream(_: *NodeFS, _: Arguments.CreateWriteStream, comptime _: Flavor) Maybe(Return.CreateWriteStream) { - return Maybe(Return.CreateWriteStream).todo(); - } - /// This function is `cpSync`, but only if you pass `{ recursive: ..., force: ..., errorOnExist: ..., mode: ... }' /// The other options like `filter` use a JS fallback, see `src/js/internal/fs/cp.ts` pub fn cp(this: *NodeFS, args: Arguments.Cp, comptime flavor: Flavor) Maybe(Return.Cp) { diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 4f10e96b8a..c0dc296e5d 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -60,6 +60,8 @@ fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { if (exception1 != .zero) { globalObject.throwValue(exception1); return .zero; + } else if (globalObject.hasException()) { + return .zero; } var result = Function( &this.node_fs, @@ -110,13 +112,13 @@ fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { if (exception1 != .zero) { globalObject.throwValue(exception1); - + slice.deinit(); + return .zero; + } else if (globalObject.hasException()) { slice.deinit(); return .zero; } - // TODO: handle globalObject.throwValue - const Task = @field(JSC.Node.Async, @tagName(FunctionEnum)); if (comptime FunctionEnum == .cp) { return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena); diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 209d4c85f9..8eeb4acd95 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -353,6 +353,8 @@ pub const FSWatcher = struct { } return null; }; + var should_deinit_path = true; + defer if (should_deinit_path) path.deinit(); if (exception.* != null) return null; var listener: JSC.JSValue = .zero; @@ -365,7 +367,7 @@ pub const FSWatcher = struct { // options if (options_or_callable.isObject()) { - if (options_or_callable.get(ctx, "persistent")) |persistent_| { + if (options_or_callable.getTruthy(ctx, "persistent")) |persistent_| { if (!persistent_.isBoolean()) { JSC.throwInvalidArguments( "persistent must be a boolean.", @@ -378,7 +380,7 @@ pub const FSWatcher = struct { persistent = persistent_.toBoolean(); } - if (options_or_callable.get(ctx, "verbose")) |verbose_| { + if (options_or_callable.getTruthy(ctx, "verbose")) |verbose_| { if (!verbose_.isBoolean()) { JSC.throwInvalidArguments( "verbose must be a boolean.", @@ -391,30 +393,11 @@ pub const FSWatcher = struct { verbose = verbose_.toBoolean(); } - if (options_or_callable.get(ctx, "encoding")) |encoding_| { - if (!encoding_.isString()) { - JSC.throwInvalidArguments( - "encoding must be a string.", - .{}, - ctx, - exception, - ); - return null; - } - if (JSC.Node.Encoding.fromJS(encoding_, ctx.ptr())) |node_encoding| { - encoding = node_encoding; - } else { - JSC.throwInvalidArguments( - "invalid encoding.", - .{}, - ctx, - exception, - ); - return null; - } + if (options_or_callable.fastGet(ctx, .encoding)) |encoding_| { + encoding = JSC.Node.Encoding.assert(encoding_, ctx, encoding) catch return null; } - if (options_or_callable.get(ctx, "recursive")) |recursive_| { + if (options_or_callable.getTruthy(ctx, "recursive")) |recursive_| { if (!recursive_.isBoolean()) { JSC.throwInvalidArguments( "recursive must be a boolean.", @@ -428,7 +411,7 @@ pub const FSWatcher = struct { } // abort signal - if (options_or_callable.get(ctx, "signal")) |signal_| { + if (options_or_callable.getTruthy(ctx, "signal")) |signal_| { if (JSC.AbortSignal.fromJS(signal_)) |signal_obj| { //Keep it alive signal_.ensureStillAlive(); @@ -466,6 +449,8 @@ pub const FSWatcher = struct { return null; } + should_deinit_path = false; + return Arguments{ .path = path, .listener = listener, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 8ff71ffc01..65f584f85a 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -669,7 +669,6 @@ pub const Encoding = enum(u8) { }; } - /// Caller must verify the value is a string pub fn fromJS(value: JSC.JSValue, global: *JSC.JSGlobalObject) ?Encoding { return map.fromJSCaseInsensitive(global, value); } @@ -679,6 +678,40 @@ pub const Encoding = enum(u8) { return strings.inMapCaseInsensitive(slice, map); } + pub fn assert(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject, default: Encoding) !Encoding { + if (value.isFalsey()) { + return default; + } + + if (!value.isString()) { + throwEncodingError(globalObject, value); + return error.JSError; + } + + return fromJSWithDefaultOnEmpty(value, globalObject, default) orelse { + throwEncodingError(globalObject, value); + return error.JSError; + }; + } + + pub fn fromJSWithDefaultOnEmpty(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject, default: Encoding) ?Encoding { + const str = bun.String.tryFromJS(value, globalObject) orelse return null; + defer str.deref(); + if (str.isEmpty()) { + return default; + } + return str.inMapCaseInsensitive(Encoding.map); + } + + pub fn throwEncodingError(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + globalObject.ERR_INVALID_ARG_VALUE( + "encoding '{}' is an invalid encoding", + .{ + value.fmtString(globalObject), + }, + ).throw(); + } + pub fn encodeWithSize(encoding: Encoding, globalObject: *JSC.JSGlobalObject, comptime size: usize, input: *const [size]u8) JSC.JSValue { switch (encoding) { .base64 => { diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index d1c42a9750..1d13dd0172 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -1493,7 +1493,7 @@ pub const Expect = struct { const not = this.flags.not; var pass = false; - const truthy = value.toBooleanSlow(globalThis); + const truthy = value.toBoolean(); if (truthy) pass = true; if (not) pass = !pass; @@ -1649,7 +1649,7 @@ pub const Expect = struct { const not = this.flags.not; var pass = false; - const truthy = value.toBooleanSlow(globalThis); + const truthy = value.toBoolean(); if (!truthy) pass = true; if (not) pass = !pass; @@ -2439,7 +2439,7 @@ pub const Expect = struct { // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. if (expected_value.get(globalThis, "test")) |test_fn| { const matches = test_fn.call(globalThis, expected_value, &.{received_message}) catch |err| globalThis.takeException(err); - if (!matches.toBooleanSlow(globalThis)) return .undefined; + if (!matches.toBoolean()) return .undefined; } this.throw(globalThis, signature, "\n\nExpected pattern: not {any}\nReceived message: {any}\n", .{ @@ -2524,7 +2524,7 @@ pub const Expect = struct { // TODO: REMOVE THIS GETTER! Expose a binding to call .test on the RegExp object directly. if (expected_value.get(globalThis, "test")) |test_fn| { const matches = test_fn.call(globalThis, expected_value, &.{received_message}) catch |err| globalThis.takeException(err); - if (matches.toBooleanSlow(globalThis)) return .undefined; + if (matches.toBoolean()) return .undefined; } } @@ -4699,7 +4699,7 @@ pub const Expect = struct { const is_valid = valid: { if (result.isObject()) { if (result.get(globalThis, "pass")) |pass_value| { - pass = pass_value.toBooleanSlow(globalThis); + pass = pass_value.toBoolean(); if (globalThis.hasException()) return false; if (result.fastGet(globalThis, .message)) |message_value| { diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index ace7a2172a..6c3d092489 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1853,7 +1853,7 @@ inline fn createIfScope( } const name = ZigString.static(property); - const value = args[0].toBooleanSlow(globalThis); + const value = args[0].toBoolean(); const truthy_falsey = comptime switch (tag) { .pass => .{ Scope.skip, Scope.call }, diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 9c2f589f37..7e9addca18 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -2958,9 +2958,10 @@ pub fn ReadableStreamSource( } pub fn updateRef(this: *ReadableStreamSourceType, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix JSC.markBinding(@src()); this.this_jsvalue = callFrame.this(); - const ref_or_unref = callFrame.argument(0).toBooleanSlow(globalObject); + const ref_or_unref = callFrame.argument(0).toBoolean(); this.setRef(ref_or_unref); return .undefined; diff --git a/src/cli.zig b/src/cli.zig index d6a863a97e..19f64c55e1 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -769,9 +769,11 @@ pub const Arguments = struct { ctx.bundler_options.transform_only = args.flag("--no-bundle"); ctx.bundler_options.bytecode = args.flag("--bytecode"); - if (args.flag("--app")) { - ctx.bundler_options.bake = true; - ctx.bundler_options.bake_debug_dump_server = args.flag("--debug-dump-server-files"); + if (comptime FeatureFlags.bake) { + if (args.flag("--app")) { + ctx.bundler_options.bake = true; + ctx.bundler_options.bake_debug_dump_server = args.flag("--debug-dump-server-files"); + } } // TODO: support --format=esm @@ -933,19 +935,21 @@ pub const Arguments = struct { ctx.bundler_options.asset_naming = try strings.concat(allocator, &.{ "./", bun.strings.removeLeadingDotSlash(asset_naming) }); } - if (args.flag("--server-components")) { - if (!bun.FeatureFlags.cli_server_components) { - // TODO: i want to disable this in non-canary - // but i also want to have tests that can run for PRs - } - ctx.bundler_options.server_components = true; - if (opts.target) |target| { - if (!bun.options.Target.from(target).isServerSide()) { - bun.Output.errGeneric("Cannot use client-side --target={s} with --server-components", .{@tagName(target)}); - Global.crash(); + if (comptime FeatureFlags.bake) { + if (args.flag("--server-components")) { + if (!bun.FeatureFlags.cli_server_components) { + // TODO: i want to disable this in non-canary + // but i also want to have tests that can run for PRs + } + ctx.bundler_options.server_components = true; + if (opts.target) |target| { + if (!bun.options.Target.from(target).isServerSide()) { + bun.Output.errGeneric("Cannot use client-side --target={s} with --server-components", .{@tagName(target)}); + Global.crash(); + } + } else { + opts.target = .bun; } - } else { - opts.target = .bun; } } diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index b62f3028b3..506596a53f 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -195,6 +195,10 @@ export function getStdinStream(fd) { } } } catch (err) { + if (err?.code === "ERR_STREAM_RELEASE_LOCK") { + // Not a bug. Happens in unref(). + return; + } stream.destroy(err); } } @@ -212,6 +216,7 @@ export function getStdinStream(fd) { $debug('on("resume");'); ref(); stream._undestroy(); + stream_destroyed = false; }); stream._readableState.reading = false; diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index ed45aaab37..6e7e2d5951 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -108,6 +108,7 @@ export function initializeReadableStream( $linkTimeConstant; export function readableStreamToArray(stream: ReadableStream): Promise { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); // this is a direct stream var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); if (underlyingSource !== undefined) { @@ -119,6 +120,7 @@ export function readableStreamToArray(stream: ReadableStream): Promise { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); // this is a direct stream var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); if (underlyingSource !== undefined) { @@ -137,6 +139,7 @@ export function readableStreamToText(stream: ReadableStream): Promise { $linkTimeConstant; export function readableStreamToArrayBuffer(stream: ReadableStream): Promise | ArrayBuffer { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); // this is a direct stream var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); if (underlyingSource !== undefined) { @@ -216,6 +219,7 @@ export function readableStreamToArrayBuffer(stream: ReadableStream) $linkTimeConstant; export function readableStreamToBytes(stream: ReadableStream): Promise | Uint8Array { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); // this is a direct stream var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); @@ -297,6 +301,7 @@ export function readableStreamToFormData( stream: ReadableStream, contentType: string | ArrayBuffer | ArrayBufferView, ): Promise { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); return Bun.readableStreamToBlob(stream).then(blob => { return FormData.from(blob, contentType); @@ -305,6 +310,7 @@ export function readableStreamToFormData( $linkTimeConstant; export function readableStreamToJSON(stream: ReadableStream): unknown { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); let result = $tryUseReadableStreamBufferedFastPath(stream, "json"); if (result) { @@ -326,6 +332,7 @@ export function readableStreamToJSON(stream: ReadableStream): unknown { $linkTimeConstant; export function readableStreamToBlob(stream: ReadableStream): Promise { + if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); if ($isReadableStreamLocked(stream)) return Promise.$reject($makeTypeError("ReadableStream is locked")); return ( @@ -422,7 +429,15 @@ export function pipeThrough(this, streams, options) { if ($isWritableStreamLocked(internalWritable)) throw $makeTypeError("WritableStream is locked"); - $readableStreamPipeToWritableStream(this, internalWritable, preventClose, preventAbort, preventCancel, signal); + const promise = $readableStreamPipeToWritableStream( + this, + internalWritable, + preventClose, + preventAbort, + preventCancel, + signal, + ); + $markPromiseAsHandled(promise); return readable; } diff --git a/src/js/builtins/ReadableStreamDefaultController.ts b/src/js/builtins/ReadableStreamDefaultController.ts index 7c1c9ace77..6a04addc33 100644 --- a/src/js/builtins/ReadableStreamDefaultController.ts +++ b/src/js/builtins/ReadableStreamDefaultController.ts @@ -33,8 +33,9 @@ export function initializeReadableStreamDefaultController(this, stream, underlyi export function enqueue(this, chunk) { if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "enqueue"); - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) - throw new TypeError("ReadableStreamDefaultController is not in a state where chunk can be enqueued"); + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw $ERR_INVALID_STATE("ReadableStreamDefaultController is not in a state where chunk can be enqueued"); + } return $readableStreamDefaultControllerEnqueue(this, chunk); } diff --git a/src/js/builtins/ReadableStreamDefaultReader.ts b/src/js/builtins/ReadableStreamDefaultReader.ts index 2ff8e385f0..9ddb3e3f38 100644 --- a/src/js/builtins/ReadableStreamDefaultReader.ts +++ b/src/js/builtins/ReadableStreamDefaultReader.ts @@ -172,10 +172,7 @@ export function releaseLock(this) { if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; - if ($getByIdDirectPrivate(this, "readRequests")?.isNotEmpty()) - throw new TypeError("There are still pending read requests, cannot release the lock"); - - $readableStreamReaderGenericRelease(this); + $readableStreamDefaultReaderRelease(this); } $getter; diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts index 7f95f39ee9..72b42a2c76 100644 --- a/src/js/builtins/ReadableStreamInternals.ts +++ b/src/js/builtins/ReadableStreamInternals.ts @@ -331,7 +331,10 @@ export function pipeToDoReadWrite(pipeState) { pipeState.pendingReadPromiseCapability.resolve.$call(undefined, canWrite); if (!canWrite) return; - pipeState.pendingWritePromise = $writableStreamDefaultWriterWrite(pipeState.writer, result.value); + pipeState.pendingWritePromise = $writableStreamDefaultWriterWrite(pipeState.writer, result.value).$then( + undefined, + () => {}, + ); }, e => { pipeState.pendingReadPromiseCapability.resolve.$call(undefined, false); @@ -396,7 +399,7 @@ export function pipeToClosingMustBePropagatedForward(pipeState) { action(); return; } - $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").promise.$then(action, undefined); + $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").promise.$then(action, () => {}); } export function pipeToClosingMustBePropagatedBackward(pipeState) { @@ -1367,20 +1370,18 @@ export function readableStreamError(stream, error) { if (!reader) return; + $getByIdDirectPrivate(reader, "closedPromiseCapability").reject.$call(undefined, error); + const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").promise; + $markPromiseAsHandled(promise); + if ($isReadableStreamDefaultReader(reader)) { - const requests = $getByIdDirectPrivate(reader, "readRequests"); - $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); - for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); + $readableStreamDefaultReaderErrorReadRequests(reader, error); } else { $assert($isReadableStreamBYOBReader(reader)); const requests = $getByIdDirectPrivate(reader, "readIntoRequests"); $putByIdDirectPrivate(reader, "readIntoRequests", $createFIFO()); for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); } - - $getByIdDirectPrivate(reader, "closedPromiseCapability").reject.$call(undefined, error); - const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").promise; - $markPromiseAsHandled(promise); } export function readableStreamDefaultControllerShouldCallPull(controller) { @@ -1608,6 +1609,15 @@ export function isReadableStreamDisturbed(stream) { return stream.$disturbed; } +$visibility = "Private"; +export function readableStreamDefaultReaderRelease(reader) { + $readableStreamReaderGenericRelease(reader); + $readableStreamDefaultReaderErrorReadRequests( + reader, + $ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()"), + ); +} + $visibility = "Private"; export function readableStreamReaderGenericRelease(reader) { $assert(!!$getByIdDirectPrivate(reader, "ownerReadableStream")); @@ -1616,11 +1626,11 @@ export function readableStreamReaderGenericRelease(reader) { if ($getByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "state") === $streamReadable) $getByIdDirectPrivate(reader, "closedPromiseCapability").reject.$call( undefined, - $makeTypeError("releasing lock of reader whose stream is still in readable state"), + $ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()"), ); else $putByIdDirectPrivate(reader, "closedPromiseCapability", { - promise: $newHandledRejectedPromise($makeTypeError("reader released lock")), + promise: $newHandledRejectedPromise($ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()")), }); const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").promise; @@ -1636,6 +1646,12 @@ export function readableStreamReaderGenericRelease(reader) { $putByIdDirectPrivate(reader, "ownerReadableStream", undefined); } +export function readableStreamDefaultReaderErrorReadRequests(reader, error) { + const requests = $getByIdDirectPrivate(reader, "readRequests"); + $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); + for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); +} + export function readableStreamDefaultControllerCanCloseOrEnqueue(controller) { if ($getByIdDirectPrivate(controller, "closeRequested")) { return false; diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 63003ae684..fd80b0783b 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -241,7 +241,7 @@ const Socket = (function (InternalSocket) { if (callback) { const writeChunk = self._pendingData; - if (socket.$write(writeChunk || "", "utf8")) { + if (!writeChunk || socket.$write(writeChunk || "", self._pendingEncoding || "utf8")) { self._pendingData = self.#writeCallback = null; callback(null); } else { @@ -856,12 +856,12 @@ const Socket = (function (InternalSocket) { if (!socket) { // detached but connected? wait for the socket to be attached this.#writeCallback = callback; - this._pendingEncoding = "buffer"; - this._pendingData = Buffer.from(chunk, encoding); + this._pendingEncoding = encoding; + this._pendingData = chunk; return; } - const success = socket?.$write(chunk, encoding); + const success = socket.$write(chunk, encoding); this[kBytesWritten] = socket.bytesWritten; if (success) { callback(); diff --git a/src/js/thirdparty/node-fetch.ts b/src/js/thirdparty/node-fetch.ts index d2b5831690..d0676ee418 100644 --- a/src/js/thirdparty/node-fetch.ts +++ b/src/js/thirdparty/node-fetch.ts @@ -126,7 +126,7 @@ var ResponsePrototype = Response.prototype; const kUrl = Symbol("kUrl"); class Request extends WebRequest { - [kUrl]: string; + [kUrl]?: string; constructor(input, init) { // node-fetch is relaxed with the URL, for example, it allows "/" as a valid URL. @@ -137,12 +137,11 @@ class Request extends WebRequest { this[kUrl] = input; } else { super(input, init); - this[kUrl] = input.url; } } get url() { - return this[kUrl]; + return this[kUrl] ?? super.url; } } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index f6ebd8a57a..8cc8eb20ff 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -100,7 +100,7 @@ describe("Server", () => { }); test("should not allow Bun.serve with invalid tls option", () => { - [1, "string", true, Symbol("symbol"), false].forEach(value => { + [1, "string", true, Symbol("symbol")].forEach(value => { expect(() => { using server = Bun.serve({ //@ts-ignore diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 81570275e9..6913d38191 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -96,6 +96,117 @@ it("writing to 1, 2 are possible", () => { expect(fs.writeSync(2, Buffer.from("\nhello-stderr-test\n"))).toBe(19); }); +describe("test-fs-assert-encoding-error", () => { + const testPath = join(tmpdirSync(), "assert-encoding-error"); + const options = "test"; + const expectedError = expect.objectContaining({ + code: "ERR_INVALID_ARG_VALUE", + name: "TypeError", + }); + + it("readFile throws on invalid encoding", () => { + expect(() => { + fs.readFile(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("readFileSync throws on invalid encoding", () => { + expect(() => { + fs.readFileSync(testPath, options); + }).toThrow(expectedError); + }); + + it("readdir throws on invalid encoding", () => { + expect(() => { + fs.readdir(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("readdirSync throws on invalid encoding", () => { + expect(() => { + fs.readdirSync(testPath, options); + }).toThrow(expectedError); + }); + + it("readlink throws on invalid encoding", () => { + expect(() => { + fs.readlink(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("readlinkSync throws on invalid encoding", () => { + expect(() => { + fs.readlinkSync(testPath, options); + }).toThrow(expectedError); + }); + + it("writeFile throws on invalid encoding", () => { + expect(() => { + fs.writeFile(testPath, "data", options, () => {}); + }).toThrow(expectedError); + }); + + it("writeFileSync throws on invalid encoding", () => { + expect(() => { + fs.writeFileSync(testPath, "data", options); + }).toThrow(expectedError); + }); + + it("appendFile throws on invalid encoding", () => { + expect(() => { + fs.appendFile(testPath, "data", options, () => {}); + }).toThrow(expectedError); + }); + + it("appendFileSync throws on invalid encoding", () => { + expect(() => { + fs.appendFileSync(testPath, "data", options); + }).toThrow(expectedError); + }); + + it("watch throws on invalid encoding", () => { + expect(() => { + fs.watch(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("realpath throws on invalid encoding", () => { + expect(() => { + fs.realpath(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("realpathSync throws on invalid encoding", () => { + expect(() => { + fs.realpathSync(testPath, options); + }).toThrow(expectedError); + }); + + it("mkdtemp throws on invalid encoding", () => { + expect(() => { + fs.mkdtemp(testPath, options, () => {}); + }).toThrow(expectedError); + }); + + it("mkdtempSync throws on invalid encoding", () => { + expect(() => { + fs.mkdtempSync(testPath, options); + }).toThrow(expectedError); + }); + + it.todo("ReadStream throws on invalid encoding", () => { + expect(() => { + fs.ReadStream(testPath, options); + }).toThrow(expectedError); + }); + + it.todo("WriteStream throws on invalid encoding", () => { + expect(() => { + fs.WriteStream(testPath, options); + }).toThrow(expectedError); + }); +}); + // TODO: port node.js tests for these it("fs.readv returns object", async done => { const fd = await promisify(fs.open)(import.meta.path, "r"); diff --git a/test/js/third_party/prompts/prompts.test.ts b/test/js/third_party/prompts/prompts.test.ts index 4d58ee36af..00765fe76d 100644 --- a/test/js/third_party/prompts/prompts.test.ts +++ b/test/js/third_party/prompts/prompts.test.ts @@ -9,7 +9,11 @@ test("works with prompts", async () => { stdin: "pipe", }); - await Bun.sleep(100); + const reader = child.stdout.getReader(); + + await reader.read(); + reader.releaseLock(); + child.stdin.write("dylan\n"); await Bun.sleep(100); child.stdin.write("999\n"); diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 4f769a5420..1caa6eb7ae 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -756,6 +756,55 @@ it("ReadableStream for empty file closes immediately", async () => { expect(chunks.length).toBe(0); }); +it("ReadableStream errors the stream on pull rejection", async () => { + let stream = new ReadableStream({ + pull(controller) { + return Promise.reject("pull rejected"); + }, + }); + + let reader = stream.getReader(); + let closed = reader.closed.catch(err => `closed: ${err}`); + let read = reader.read().catch(err => `read: ${err}`); + expect(await Promise.race([closed, read])).toBe("closed: pull rejected"); + expect(await read).toBe("read: pull rejected"); +}); + +it("ReadableStream rejects pending reads when the lock is released", async () => { + let { resolve, promise } = Promise.withResolvers(); + let stream = new ReadableStream({ + async pull(controller) { + controller.enqueue("123"); + await promise; + controller.enqueue("456"); + controller.close(); + }, + }); + + let reader = stream.getReader(); + expect((await reader.read()).value).toBe("123"); + + let read = reader.read(); + reader.releaseLock(); + expect(read).rejects.toThrow( + expect.objectContaining({ + name: "AbortError", + code: "ERR_STREAM_RELEASE_LOCK", + }), + ); + expect(reader.closed).rejects.toThrow( + expect.objectContaining({ + name: "AbortError", + code: "ERR_STREAM_RELEASE_LOCK", + }), + ); + + resolve(); + + reader = stream.getReader(); + expect((await reader.read()).value).toBe("456"); +}); + it("new Response(stream).arrayBuffer() (bytes)", async () => { var queue = [Buffer.from("abdefgh")]; var stream = new ReadableStream({ @@ -1053,3 +1102,42 @@ it("fs.createReadStream(filename) should be able to break inside async loop", as expect(true).toBe(true); } }); + +it("pipeTo doesn't cause unhandled rejections on readable errors", async () => { + // https://github.com/WebKit/WebKit/blob/3a75b5d2de94aa396a99b454ac47f3be9e0dc726/LayoutTests/streams/pipeTo-unhandled-promise.html + let unhandledRejectionCaught = false; + + const catchUnhandledRejection = () => { + unhandledRejectionCaught = true; + }; + process.on("unhandledRejection", catchUnhandledRejection); + + const writable = new WritableStream(); + const readable = new ReadableStream({ start: c => c.error("error") }); + readable.pipeTo(writable).catch(() => {}); + + await Bun.sleep(15); + + process.off("unhandledRejection", catchUnhandledRejection); + + expect(unhandledRejectionCaught).toBe(false); +}); + +it("pipeThrough doesn't cause unhandled rejections on readable errors", async () => { + let unhandledRejectionCaught = false; + + const catchUnhandledRejection = () => { + unhandledRejectionCaught = true; + }; + process.on("unhandledRejection", catchUnhandledRejection); + + const readable = new ReadableStream({ start: c => c.error("error") }); + const ts = new TransformStream(); + readable.pipeThrough(ts); + + await Bun.sleep(15); + + process.off("unhandledRejection", catchUnhandledRejection); + + expect(unhandledRejectionCaught).toBe(false); +}); diff --git a/test/regression/issue/014865.test.ts b/test/regression/issue/014865.test.ts new file mode 100644 index 0000000000..37e6b1bfbb --- /dev/null +++ b/test/regression/issue/014865.test.ts @@ -0,0 +1,8 @@ +import { test, expect } from "bun:test"; +import { Request } from "node-fetch"; + +test("node fetch Request URL field is set even with a valid URL", () => { + expect(new Request("/").url).toBe("/"); + expect(new Request("https://bun.sh/").url).toBe("https://bun.sh/"); + expect(new Request(new URL("https://bun.sh/")).url).toBe("https://bun.sh/"); +});