diff --git a/.prettierignore b/.prettierignore index 6347a9bd76af9..d7360d9d2f2ad 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,6 @@ src/bun.js/WebKit src/deps test/snapshots test/js/deno +test/node.js src/react-refresh.js *.min.js diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 9133bc3c611cc..c09f9e9164b20 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -176,7 +176,7 @@ pub const Subprocess = struct { }; } }; - process: *Process = undefined, + process: *Process, stdin: Writable, stdout: Readable, stderr: Readable, @@ -208,6 +208,7 @@ pub const Subprocess = struct { is_sync: bool = false, killed: bool = false, has_stdin_destructor_called: bool = false, + finalized: bool = false, }; pub const SignalCode = bun.SignalCode; @@ -672,7 +673,13 @@ pub const Subprocess = struct { this.flags.has_stdin_destructor_called = true; this.weak_file_sink_stdin_ptr = null; - this.updateHasPendingActivity(); + if (this.flags.finalized) { + // if the process has already been garbage collected, we can free the memory now + bun.default_allocator.destroy(this); + } else { + // otherwise update the pending activity flag + this.updateHasPendingActivity(); + } } pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { @@ -1514,7 +1521,12 @@ pub const Subprocess = struct { this.process.detach(); this.process.deref(); - bun.default_allocator.destroy(this); + + this.flags.finalized = true; + if (this.weak_file_sink_stdin_ptr == null) { + // if no file sink exists we can free immediately + bun.default_allocator.destroy(this); + } } pub fn getExited( diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index f022a8427546c..525e05c6a8fba 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -560,13 +560,25 @@ pub const ServerConfig = struct { } if (obj.getTruthy(global, "requestCert")) |request_cert| { - result.request_cert = if (request_cert.asBoolean()) 1 else 0; - any = true; + if (request_cert.isBoolean()) { + result.request_cert = if (request_cert.asBoolean()) 1 else 0; + any = true; + } else { + global.throw("Expected requestCert to be a boolean", .{}); + result.deinit(); + return null; + } } if (obj.getTruthy(global, "rejectUnauthorized")) |reject_unauthorized| { - result.reject_unauthorized = if (reject_unauthorized.asBoolean()) 1 else 0; - any = true; + if (reject_unauthorized.isBoolean()) { + result.reject_unauthorized = if (reject_unauthorized.asBoolean()) 1 else 0; + any = true; + } else { + global.throw("Expected rejectUnauthorized to be a boolean", .{}); + result.deinit(); + return null; + } } if (obj.getTruthy(global, "ciphers")) |ssl_ciphers| { @@ -720,8 +732,14 @@ pub const ServerConfig = struct { } if (obj.get(global, "lowMemoryMode")) |low_memory_mode| { - result.low_memory_mode = low_memory_mode.toBoolean(); - any = true; + if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { + result.low_memory_mode = low_memory_mode.toBoolean(); + any = true; + } else { + global.throw("Expected lowMemoryMode to be a boolean", .{}); + result.deinit(); + return null; + } } } @@ -4191,6 +4209,11 @@ pub const ServerWebSocket = struct { return .zero; } + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("publish expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); if (message_value.isEmptyOrUndefinedOrNull()) { @@ -4268,6 +4291,11 @@ pub const ServerWebSocket = struct { var topic_slice = topic_value.toSlice(globalThis, bun.default_allocator); defer topic_slice.deinit(); + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("publishText expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); if (message_value.isEmptyOrUndefinedOrNull() or !message_value.isString()) { @@ -4329,6 +4357,11 @@ pub const ServerWebSocket = struct { return .zero; } + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("publishBinary expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); if (message_value.isEmptyOrUndefinedOrNull()) { @@ -4497,6 +4530,11 @@ pub const ServerWebSocket = struct { const message_value = args.ptr[0]; const compress_value = args.ptr[1]; + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("send expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); if (message_value.isEmptyOrUndefinedOrNull()) { @@ -4566,6 +4604,11 @@ pub const ServerWebSocket = struct { const message_value = args.ptr[0]; const compress_value = args.ptr[1]; + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("sendText expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); if (message_value.isEmptyOrUndefinedOrNull() or !message_value.isString()) { @@ -4645,6 +4688,11 @@ pub const ServerWebSocket = struct { const message_value = args.ptr[0]; const compress_value = args.ptr[1]; + if (!compress_value.isBoolean() and !compress_value.isUndefined() and !compress_value.isEmpty()) { + globalThis.throw("sendBinary expects compress to be a boolean", .{}); + return .zero; + } + const compress = args.len > 1 and compress_value.toBoolean(); const buffer = message_value.asArrayBuffer(globalThis) orelse { diff --git a/src/bun.js/bindings/BunWorkerGlobalScope.h b/src/bun.js/bindings/BunWorkerGlobalScope.h index f8e0be52e9687..e1ac3636c679b 100644 --- a/src/bun.js/bindings/BunWorkerGlobalScope.h +++ b/src/bun.js/bindings/BunWorkerGlobalScope.h @@ -18,7 +18,7 @@ class MessagePortChannelProviderImpl; class GlobalScope : public RefCounted, public EventTargetWithInlineData { WTF_MAKE_ISO_ALLOCATED(GlobalScope); - uint32_t m_messageEventCount; + uint32_t m_messageEventCount{0}; static void onDidChangeListenerImpl(EventTarget&, const AtomString&, OnDidChangeListenerKind); diff --git a/src/cli.zig b/src/cli.zig index 8f27ddb1d1efc..a34cdb23f66a4 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -208,9 +208,8 @@ pub const Arguments = struct { pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; const bunx_commands = [_]ParamType{ - clap.parseParam("--silent Don't print the script command") catch unreachable, clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable, - }; + } ++ auto_only_params; const build_only_params = [_]ParamType{ clap.parseParam("--compile Generate a standalone Bun executable containing your bundled code") catch unreachable, @@ -251,18 +250,6 @@ pub const Arguments = struct { }; pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; - fn printVersionAndExit() noreturn { - @setCold(true); - Output.writer().writeAll(Global.package_json_version ++ "\n") catch {}; - Global.exit(0); - } - - fn printRevisionAndExit() noreturn { - @setCold(true); - Output.writer().writeAll(Global.package_json_version_with_revision ++ "\n") catch {}; - Global.exit(0); - } - pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_path: [:0]const u8, ctx: Command.Context, comptime cmd: Command.Tag) !void { var config_file = switch (bun.sys.openA(config_path, std.os.O.RDONLY, 0)) { .result => |fd| fd.asFile(), @@ -2341,3 +2328,15 @@ pub const Command = struct { }); }; }; + +pub fn printVersionAndExit() noreturn { + @setCold(true); + Output.writer().writeAll(Global.package_json_version ++ "\n") catch {}; + Global.exit(0); +} + +pub fn printRevisionAndExit() noreturn { + @setCold(true); + Output.writer().writeAll(Global.package_json_version_with_revision ++ "\n") catch {}; + Global.exit(0); +} diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 34558a1b6a2d6..f0041dbd98b0a 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -9,7 +9,8 @@ const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; const std = @import("std"); -const Command = @import("../cli.zig").Command; +const cli = @import("../cli.zig"); +const Command = cli.Command; const Run = @import("./run_command.zig").RunCommand; const debug = Output.scoped(.bunx, false); @@ -220,6 +221,8 @@ pub const BunxCommand = struct { var maybe_package_name: ?string = null; var verbose_install = false; var silent_install = false; + var has_version = false; + var has_revision = false; { var found_subcommand_name = false; @@ -230,7 +233,11 @@ pub const BunxCommand = struct { } if (positional.len > 0 and positional[0] == '-') { - if (strings.eqlComptime(positional, "--verbose")) { + if (strings.eqlComptime(positional, "--version") or strings.eqlComptime(positional, "-v")) { + has_version = true; + } else if (strings.eqlComptime(positional, "--revision")) { + has_revision = true; + } else if (strings.eqlComptime(positional, "--verbose")) { verbose_install = true; } else if (strings.eqlComptime(positional, "--silent")) { silent_install = true; @@ -249,7 +256,13 @@ pub const BunxCommand = struct { // check if package_name_for_update_request is empty string or " " if (maybe_package_name == null or maybe_package_name.?.len == 0) { - exitWithUsage(); + if (has_revision) { + cli.printRevisionAndExit(); + } else if (has_version) { + cli.printVersionAndExit(); + } else { + exitWithUsage(); + } } const package_name = maybe_package_name.?; diff --git a/src/symbols.txt b/src/symbols.txt index 1d047fe7e257f..157c0c51573fd 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -152,4 +152,4 @@ __ZN2v87Isolate10GetCurrentEv __ZN2v87Isolate13TryGetCurrentEv __ZN2v87Isolate17GetCurrentContextEv __ZN4node25AddEnvironmentCleanupHookEPN2v87IsolateEPFvPvES3_ -__ZN4node28RemoveEnvironmentCleanupHookEPN2v87IsolateEPFvPvES3_ \ No newline at end of file +__ZN4node28RemoveEnvironmentCleanupHookEPN2v87IsolateEPFvPvES3_ diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index ba381173a8af4..79a261f640642 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -310,3 +310,70 @@ it("should work for github repository with committish", async () => { expect(out.trim()).toContain("hello bun!"); expect(exited).toBe(0); }); + +it.each(["--version", "-v"])("should print the version using %s and exit", async flag => { + const subprocess = spawn({ + cmd: [bunExe(), "x", flag], + cwd: x_dir, + stdout: "pipe", + stdin: "inherit", + stderr: "pipe", + env, + }); + + let [err, out, exited] = await Promise.all([ + new Response(subprocess.stderr).text(), + new Response(subprocess.stdout).text(), + subprocess.exited, + ]); + + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.trim()).toContain(Bun.version); + expect(exited).toBe(0); +}); + +it("should print the revision and exit", async () => { + const subprocess = spawn({ + cmd: [bunExe(), "x", "--revision"], + cwd: x_dir, + stdout: "pipe", + stdin: "inherit", + stderr: "pipe", + env, + }); + + let [err, out, exited] = await Promise.all([ + new Response(subprocess.stderr).text(), + new Response(subprocess.stdout).text(), + subprocess.exited, + ]); + + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.trim()).toContain(Bun.version); + expect(out.trim()).toContain(Bun.revision.slice(0, 7)); + expect(exited).toBe(0); +}); + +it("should pass --version to the package if specified", async () => { + const subprocess = spawn({ + cmd: [bunExe(), "x", "esbuild", "--version"], + cwd: x_dir, + stdout: "pipe", + stdin: "inherit", + stderr: "pipe", + env, + }); + + let [err, out, exited] = await Promise.all([ + new Response(subprocess.stderr).text(), + new Response(subprocess.stdout).text(), + subprocess.exited, + ]); + + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.trim()).not.toContain(Bun.version); + expect(exited).toBe(0); +}); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 8b172f41bd59c..96b5eef5d503c 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1481,3 +1481,46 @@ if (process.platform === "linux") }); }).toThrow("permission denied 0.0.0.0:1003"); }); + +describe("should error with invalid options", async () => { + it("requestCert", () => { + expect(() => { + Bun.serve({ + port: 0, + fetch(req) { + return new Response("hi"); + }, + tls: { + requestCert: "invalid", + }, + }); + }).toThrow("Expected requestCert to be a boolean"); + }); + it("rejectUnauthorized", () => { + expect(() => { + Bun.serve({ + port: 0, + fetch(req) { + return new Response("hi"); + }, + tls: { + rejectUnauthorized: "invalid", + }, + }); + }).toThrow("Expected rejectUnauthorized to be a boolean"); + }); + it("lowMemoryMode", () => { + expect(() => { + Bun.serve({ + port: 0, + fetch(req) { + return new Response("hi"); + }, + tls: { + rejectUnauthorized: true, + lowMemoryMode: "invalid", + }, + }); + }).toThrow("Expected lowMemoryMode to be a boolean"); + }); +}); diff --git a/test/js/bun/websocket/websocket-server.test.ts b/test/js/bun/websocket/websocket-server.test.ts index ddb31ea8268ba..8e76790f4ce4d 100644 --- a/test/js/bun/websocket/websocket-server.test.ts +++ b/test/js/bun/websocket/websocket-server.test.ts @@ -308,6 +308,18 @@ describe("ServerWebSocket", () => { 30_000, ); }); + + test("send/sendText/sendBinary error on invalid arguments", done => ({ + open(ws) { + // @ts-expect-error + expect(() => ws.send("hello", "world")).toThrow("send expects compress to be a boolean"); + // @ts-expect-error + expect(() => ws.sendText("hello", "world")).toThrow("sendText expects compress to be a boolean"); + // @ts-expect-error + expect(() => ws.sendBinary(Buffer.from("hello"), "world")).toThrow("sendBinary expects compress to be a boolean"); + done(); + }, + })); describe("sendBinary()", () => { for (const { label, message, bytes } of buffers) { test(label, done => ({ @@ -374,6 +386,23 @@ describe("ServerWebSocket", () => { })); } }); + + test("publish/publishText/publishBinary error on invalid arguments", done => ({ + async open(ws) { + // @ts-expect-error + expect(() => ws.publish("hello", Buffer.from("hi"), "invalid")).toThrow( + "publish expects compress to be a boolean", + ); + // @ts-expect-error + expect(() => ws.publishText("hello", "hi", "invalid")).toThrow("publishText expects compress to be a boolean"); + // @ts-expect-error + expect(() => ws.publishBinary("hello", Buffer.from("hi"), "invalid")).toThrow( + "publishBinary expects compress to be a boolean", + ); + done(); + }, + })); + describe("publishBinary()", () => { for (const { label, message, bytes } of buffers) { test(label, (done, connect) => ({