From ae18cc0ef350175f3b8f8c71fd52857a8a2ed5c5 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Jan 2025 15:08:03 -0800 Subject: [PATCH 01/56] fix(server) HEAD Requests followup (#16115) --- src/bun.js/api/server.zig | 21 ++++++--- test/js/bun/http/bun-server.test.ts | 68 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 52366fefb2..71be97b4be 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -3213,17 +3213,23 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; }; const globalThis = server.globalThis; + var has_content_length_or_transfer_encoding = false; if (response.getFetchHeaders()) |headers| { // first respect the headers - if (headers.get("transfer-encoding", globalThis)) |transfer_encoding| { - resp.writeHeader("transfer-encoding", transfer_encoding); - } else if (headers.get("content-length", globalThis)) |content_length| { - const len = std.fmt.parseInt(usize, content_length, 10) catch 0; + if (headers.fastGet(.TransferEncoding)) |transfer_encoding| { + const transfer_encoding_str = transfer_encoding.toSlice(server.allocator); + defer transfer_encoding_str.deinit(); + resp.writeHeader("transfer-encoding", transfer_encoding_str.slice()); + has_content_length_or_transfer_encoding = true; + } else if (headers.fastGet(.ContentLength)) |content_length| { + const content_length_str = content_length.toSlice(server.allocator); + defer content_length_str.deinit(); + const len = std.fmt.parseInt(usize, content_length_str.slice(), 10) catch 0; resp.writeHeaderInt("content-length", len); - } else { - resp.writeHeaderInt("content-length", 0); + has_content_length_or_transfer_encoding = true; } - } else { + } + if (!has_content_length_or_transfer_encoding) { // then respect the body response.body.value.toBlobIfPossible(); switch (response.body.value) { @@ -3267,6 +3273,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }, } } + this.renderMetadata(); this.endWithoutBody(this.shouldCloseConnection()); } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index 771debc7f1..1c49bc8bff 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -895,6 +895,74 @@ describe("HEAD requests #15355", () => { } }); + test("should fallback to the body if content-length is missing in the headers", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.url.endsWith("/content-length")) { + return new Response("Hello World", { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + } + + if (req.url.endsWith("/chunked")) { + return new Response( + async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }, + { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }, + ); + } + + return new Response(null, { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + }, + }); + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/null", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("0"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + }); + test("HEAD requests should not have body", async () => { const dir = tempDirWithFiles("fsr", { "hello": "Hello World", From d714943d8755f1dc3ddc5c5fe69698be2fecf67d Mon Sep 17 00:00:00 2001 From: Chawye Hsu Date: Fri, 3 Jan 2025 07:46:27 +0800 Subject: [PATCH 02/56] fix(install): read bunfig `install.cache.dir` (#10699) Signed-off-by: Chawye Hsu --- src/install/install.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/install/install.zig b/src/install/install.zig index 495c63ee0a..cd557fa007 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -3533,7 +3533,7 @@ pub const PackageManager = struct { noinline fn ensureCacheDirectory(this: *PackageManager) std.fs.Dir { loop: while (true) { if (this.options.enable.cache) { - const cache_dir = fetchCacheDirectoryPath(this.env); + const cache_dir = fetchCacheDirectoryPath(this.env, this.options); this.cache_directory_path = this.allocator.dupeZ(u8, cache_dir.path) catch bun.outOfMemory(); return std.fs.cwd().makeOpenPath(cache_dir.path, .{}) catch { @@ -6234,7 +6234,7 @@ pub const PackageManager = struct { } const CacheDir = struct { path: string, is_node_modules: bool }; - pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader) CacheDir { + pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader, options: *Options) CacheDir { if (env.get("BUN_INSTALL_CACHE_DIR")) |dir| { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; } @@ -6244,6 +6244,10 @@ pub const PackageManager = struct { return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } + if (options.cache_directory) |dir| { + return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; + } + if (env.get("XDG_CACHE_HOME")) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; @@ -7362,6 +7366,10 @@ pub const PackageManager = struct { this.max_concurrent_lifecycle_scripts = jobs; } + if (config.cache_directory) |cache_dir| { + this.cache_directory = cache_dir; + } + this.explicit_global_directory = config.global_dir orelse this.explicit_global_directory; } From a85bd429899ca5e09321bf72d58a68a6450593e5 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:53:45 -0800 Subject: [PATCH 03/56] Add short flag for `--filter` (#16058) --- src/cli.zig | 2 +- src/install/install.zig | 2 +- test/cli/run/filter-workspace.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index 54037e2965..792ff2012b 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -235,7 +235,7 @@ pub const Arguments = struct { }; const auto_or_run_params = [_]ParamType{ - clap.parseParam("--filter ... Run a script in all workspace packages matching the pattern") catch unreachable, + clap.parseParam("-F, --filter ... Run a script in all workspace packages matching the pattern") 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, clap.parseParam("--shell Control the shell used for package.json scripts. Supports either 'bun' or 'system'") catch unreachable, }; diff --git a/src/install/install.zig b/src/install/install.zig index cd557fa007..fd61861e5c 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -9573,7 +9573,7 @@ pub const PackageManager = struct { const outdated_params: []const ParamType = &(shared_params ++ [_]ParamType{ // clap.parseParam("--json Output outdated information in JSON format") catch unreachable, - clap.parseParam("--filter ... Display outdated dependencies for each matching workspace") catch unreachable, + clap.parseParam("-F, --filter ... Display outdated dependencies for each matching workspace") catch unreachable, clap.parseParam(" ... Package patterns to filter by") catch unreachable, }); diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 2d5b4f4fe9..8a6b064a77 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -110,7 +110,7 @@ function runInCwdSuccess({ cmd.push("--filter", p); } } else { - cmd.push("--filter", pattern); + cmd.push("-F", pattern); } for (const c of command) { From 012d70f42e54d5419c336a79293539a0996a6074 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 16:03:42 -0800 Subject: [PATCH 04/56] Fix bug with PATH in Bun.spawn (#16067) --- src/bun.js/api/bun/subprocess.zig | 125 +++++++++----- src/bun.js/bindings/c-bindings.cpp | 10 ++ test/js/bun/spawn/spawn-path.test.ts | 26 +++ .../node/child_process/child_process.test.ts | 5 +- test/js/web/streams/streams.test.js | 3 +- test/v8/v8.test.ts | 163 ++++++++++-------- 6 files changed, 215 insertions(+), 117 deletions(-) create mode 100644 test/js/bun/spawn/spawn-path.test.ts diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 7c8774d548..1bdb200b64 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1703,6 +1703,8 @@ pub const Subprocess = struct { return spawnMaybeSync(globalThis, args, secondaryArgsValue, true); } + extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; + // This is split into a separate function to conserve stack space. // On Windows, a single path buffer can take 64 KB. fn getArgv0(globalThis: *JSC.JSGlobalObject, PATH: []const u8, cwd: []const u8, argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct { @@ -1717,14 +1719,30 @@ pub const Subprocess = struct { var actual_argv0: [:0]const u8 = ""; - if (argv0 == null) { - const resolved = Which.which(path_buf, PATH, cwd, arg0.slice()) orelse { - return throwCommandNotFound(globalThis, arg0.slice()); - }; - actual_argv0 = try allocator.dupeZ(u8, resolved); + const argv0_to_use: []const u8 = if (argv0) |_argv0| + bun.sliceTo(_argv0, 0) + else + arg0.slice(); + + // This mimicks libuv's behavior, which mimicks execvpe + // Only resolve from $PATH when the command is not an absolute path + const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/')) + "" + // If no $PATH is provided, we fallback to the one from environ + // This is already the behavior of the PATH passed in here. + else if (PATH.len > 0) + PATH + else if (comptime Environment.isPosix) + // If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does) + bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) + else + ""; + + if (PATH_to_use.len == 0) { + actual_argv0 = try allocator.dupeZ(u8, argv0_to_use); } else { - const resolved = Which.which(path_buf, PATH, cwd, bun.sliceTo(argv0.?, 0)) orelse { - return throwCommandNotFound(globalThis, arg0.slice()); + const resolved = Which.which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse { + return throwCommandNotFound(globalThis, argv0_to_use); }; actual_argv0 = try allocator.dupeZ(u8, resolved); } @@ -1735,6 +1753,41 @@ pub const Subprocess = struct { }; } + fn getArgv(globalThis: *JSC.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { + var cmds_array = args.arrayIterator(globalThis); + // + 1 for argv0 + // + 1 for null terminator + argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); + + if (args.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } + + if (cmds_array.len == 0) { + return globalThis.throwInvalidArguments("cmd must not be empty", .{}); + } + + const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, cmds_array.next().?, allocator); + + argv0.* = argv0_result.argv0.ptr; + argv.appendAssumeCapacity(argv0_result.arg0.ptr); + + while (cmds_array.next()) |value| { + const arg = try value.toBunString2(globalThis); + defer arg.deref(); + + // if the string is empty, ignore it, don't add it to the argv + if (arg.isEmpty()) { + continue; + } + argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); + } + + if (argv.items.len == 0) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } + } + pub fn spawnMaybeSync( globalThis: *JSC.JSGlobalObject, args_: JSValue, @@ -1830,40 +1883,6 @@ pub const Subprocess = struct { } } - { - var cmds_array = cmd_value.arrayIterator(globalThis); - // + 1 for argv0 - // + 1 for null terminator - argv = try @TypeOf(argv).initCapacity(allocator, cmds_array.len + 2); - - if (cmd_value.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); - } - - if (cmds_array.len == 0) { - return globalThis.throwInvalidArguments("cmd must not be empty", .{}); - } - - const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0, cmds_array.next().?, allocator); - argv0 = argv0_result.argv0.ptr; - argv.appendAssumeCapacity(argv0_result.arg0.ptr); - - while (cmds_array.next()) |value| { - const arg = try value.toBunString2(globalThis); - defer arg.deref(); - - // if the string is empty, ignore it, don't add it to the argv - if (arg.isEmpty()) { - continue; - } - argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); - } - - if (argv.items.len == 0) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); - } - } - if (args != .zero and args.isObject()) { // This must run before the stdio parsing happens if (!is_sync) { @@ -1930,11 +1949,15 @@ pub const Subprocess = struct { override_env = true; // If the env object does not include a $PATH, it must disable path lookup for argv[0] - PATH = ""; + var NEW_PATH: []const u8 = ""; var envp_managed = env_array.toManaged(allocator); - try appendEnvpFromJS(globalThis, object, &envp_managed, &PATH); + try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH); env_array = envp_managed.moveToUnmanaged(); + PATH = NEW_PATH; } + + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); + if (try args.get(globalThis, "stdio")) |stdio_val| { if (!stdio_val.isEmptyOrUndefinedOrNull()) { if (stdio_val.jsType().isArray()) { @@ -2007,6 +2030,8 @@ pub const Subprocess = struct { } } } + } else { + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); } } @@ -2124,6 +2149,20 @@ pub const Subprocess = struct { }) { .err => |err| { spawn_options.deinit(); + switch (err.getErrno()) { + .ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => { + const display_path: [:0]const u8 = if (argv0 != null) + std.mem.sliceTo(argv0.?, 0) + else if (argv.items.len > 0 and argv.items[0] != null) + std.mem.sliceTo(argv.items[0].?, 0) + else + ""; + if (display_path.len > 0) + return globalThis.throwValue(err.withPath(display_path).toJSC(globalThis)); + }, + else => {}, + } + return globalThis.throwValue(err.toJSC(globalThis)); }, .result => |result| result, diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 8268b1cab2..cf6a95fcc1 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -870,3 +870,13 @@ extern "C" void Bun__unregisterSignalsForForwarding() } #endif + +#if OS(LINUX) || OS(DARWIN) +#include + +extern "C" const char* BUN_DEFAULT_PATH_FOR_SPAWN = _PATH_DEFPATH; +#elif OS(WINDOWS) +extern "C" const char* BUN_DEFAULT_PATH_FOR_SPAWN = "C:\\Windows\\System32;C:\\Windows;"; +#else +extern "C" const char* BUN_DEFAULT_PATH_FOR_SPAWN = "/usr/bin:/bin"; +#endif diff --git a/test/js/bun/spawn/spawn-path.test.ts b/test/js/bun/spawn/spawn-path.test.ts new file mode 100644 index 0000000000..d47876c33e --- /dev/null +++ b/test/js/bun/spawn/spawn-path.test.ts @@ -0,0 +1,26 @@ +import { test, expect } from "bun:test"; +import { chmodSync } from "fs"; +import { isWindows, tempDirWithFiles, bunEnv } from "harness"; +import path from "path"; + +test.skipIf(isWindows)("spawn uses PATH from env if present", async () => { + const tmpDir = await tempDirWithFiles("spawn-path", { + "test-script": `#!/usr/bin/env bash +echo "hello from script"`, + }); + + chmodSync(path.join(tmpDir, "test-script"), 0o777); + + const proc = Bun.spawn(["test-script"], { + env: { + ...bunEnv, + PATH: tmpDir + ":" + bunEnv.PATH, + }, + }); + + const output = await new Response(proc.stdout).text(); + expect(output.trim()).toBe("hello from script"); + + const status = await proc.exited; + expect(status).toBe(0); +}); diff --git a/test/js/node/child_process/child_process.test.ts b/test/js/node/child_process/child_process.test.ts index a259c6897d..961f9634d3 100644 --- a/test/js/node/child_process/child_process.test.ts +++ b/test/js/node/child_process/child_process.test.ts @@ -195,8 +195,8 @@ describe("spawn()", () => { it("should allow us to set env", async () => { async function getChildEnv(env: any): Promise { const result: string = await new Promise(resolve => { - const child = spawn(bunExe(), ["-e", "process.stdout.write(JSON.stringify(process.env))"], { env }); - child.stdout.on("data", data => { + const child = spawn(bunExe(), ["-e", "process.stderr.write(JSON.stringify(process.env))"], { env }); + child.stderr.on("data", data => { resolve(data.toString()); }); }); @@ -231,6 +231,7 @@ describe("spawn()", () => { { argv0: bun, stdio: ["inherit", "pipe", "inherit"], + env: bunEnv, }, ); delete process.env.NO_COLOR; diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 1caa6eb7ae..b8636ccf9f 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -7,7 +7,7 @@ import { readableStreamToText, } from "bun"; import { describe, expect, it, test } from "bun:test"; -import { tmpdirSync, isWindows, isMacOS } from "harness"; +import { tmpdirSync, isWindows, isMacOS, bunEnv } from "harness"; import { mkfifo } from "mkfifo"; import { createReadStream, realpathSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -445,6 +445,7 @@ it.todoIf(isWindows || isMacOS)("Bun.file() read text from pipe", async () => { stdout: "pipe", stdin: null, env: { + ...bunEnv, FIFO_TEST: large, }, }); diff --git a/test/v8/v8.test.ts b/test/v8/v8.test.ts index 1f4ff131ea..d2a3a468ba 100644 --- a/test/v8/v8.test.ts +++ b/test/v8/v8.test.ts @@ -1,6 +1,6 @@ import { spawn, spawnSync } from "bun"; import { beforeAll, describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, tmpdirSync, isWindows, isMusl, isBroken } from "harness"; +import { bunEnv, bunExe, tmpdirSync, isWindows, isMusl, isBroken, nodeExe } from "harness"; import assert from "node:assert"; import fs from "node:fs/promises"; import { join, basename } from "path"; @@ -38,7 +38,7 @@ const directories = { }; async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promise { - await fs.cp(srcDir, tmpDir, { recursive: true }); + await fs.cp(srcDir, tmpDir, { recursive: true, force: true }); const install = spawn({ cmd: [bunExe(), "install", "--ignore-scripts"], cwd: tmpDir, @@ -47,9 +47,9 @@ async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promis stdout: "inherit", stderr: "inherit", }); - await install.exited; - if (install.exitCode != 0) { - throw new Error("build failed"); + const exitCode = await install.exited; + if (exitCode !== 0) { + throw new Error(`install failed: ${exitCode}`); } } @@ -63,20 +63,24 @@ async function build( cmd: runtime == Runtime.bun ? [bunExe(), "x", "--bun", "node-gyp", "rebuild", buildMode == BuildMode.debug ? "--debug" : "--release"] - : ["npx", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode + : [bunExe(), "x", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode cwd: tmpDir, env: bunEnv, stdin: "inherit", stdout: "pipe", stderr: "pipe", }); - await build.exited; - const out = await new Response(build.stdout).text(); - const err = await new Response(build.stderr).text(); - if (build.exitCode != 0) { + const [exitCode, out, err] = await Promise.all([ + build.exited, + new Response(build.stdout).text(), + new Response(build.stderr).text(), + ]); + if (exitCode !== 0) { console.error(err); - throw new Error("build failed"); + console.log(out); + throw new Error(`build failed: ${exitCode}`); } + return { out, err, @@ -112,89 +116,89 @@ describe.todoIf(isBroken && isMusl)("node:v8", () => { }); describe("module lifecycle", () => { - it("can call a basic native function", () => { - checkSameOutput("test_v8_native_call", []); + it("can call a basic native function", async () => { + await checkSameOutput("test_v8_native_call", []); }); }); describe("primitives", () => { - it("can create and distinguish between null, undefined, true, and false", () => { - checkSameOutput("test_v8_primitives", []); + it("can create and distinguish between null, undefined, true, and false", async () => { + await checkSameOutput("test_v8_primitives", []); }); }); describe("Number", () => { - it("can create small integer", () => { - checkSameOutput("test_v8_number_int", []); + it("can create small integer", async () => { + await checkSameOutput("test_v8_number_int", []); }); // non-i32 v8::Number is not implemented yet - it("can create large integer", () => { - checkSameOutput("test_v8_number_large_int", []); + it("can create large integer", async () => { + await checkSameOutput("test_v8_number_large_int", []); }); - it("can create fraction", () => { - checkSameOutput("test_v8_number_fraction", []); + it("can create fraction", async () => { + await checkSameOutput("test_v8_number_fraction", []); }); }); describe("String", () => { - it("can create and read back strings with only ASCII characters", () => { - checkSameOutput("test_v8_string_ascii", []); + it("can create and read back strings with only ASCII characters", async () => { + await checkSameOutput("test_v8_string_ascii", []); }); // non-ASCII strings are not implemented yet - it("can create and read back strings with UTF-8 characters", () => { - checkSameOutput("test_v8_string_utf8", []); + it("can create and read back strings with UTF-8 characters", async () => { + await checkSameOutput("test_v8_string_utf8", []); }); - it("handles replacement correctly in strings with invalid UTF-8 sequences", () => { - checkSameOutput("test_v8_string_invalid_utf8", []); + it("handles replacement correctly in strings with invalid UTF-8 sequences", async () => { + await checkSameOutput("test_v8_string_invalid_utf8", []); }); - it("can create strings from null-terminated Latin-1 data", () => { - checkSameOutput("test_v8_string_latin1", []); + it("can create strings from null-terminated Latin-1 data", async () => { + await checkSameOutput("test_v8_string_latin1", []); }); describe("WriteUtf8", () => { - it("truncates the string correctly", () => { - checkSameOutput("test_v8_string_write_utf8", []); + it("truncates the string correctly", async () => { + await checkSameOutput("test_v8_string_write_utf8", []); }); }); }); describe("External", () => { - it("can create an external and read back the correct value", () => { - checkSameOutput("test_v8_external", []); + it("can create an external and read back the correct value", async () => { + await checkSameOutput("test_v8_external", []); }); }); describe("Object", () => { - it("can create an object and set properties", () => { - checkSameOutput("test_v8_object", []); + it("can create an object and set properties", async () => { + await checkSameOutput("test_v8_object", []); }); }); describe("Array", () => { // v8::Array::New is broken as it still tries to reinterpret locals as JSValues - it.skip("can create an array from a C array of Locals", () => { - checkSameOutput("test_v8_array_new", []); + it.skip("can create an array from a C array of Locals", async () => { + await checkSameOutput("test_v8_array_new", []); }); }); describe("ObjectTemplate", () => { - it("creates objects with internal fields", () => { - checkSameOutput("test_v8_object_template", []); + it("creates objects with internal fields", async () => { + await checkSameOutput("test_v8_object_template", []); }); }); describe("FunctionTemplate", () => { - it("keeps the data parameter alive", () => { - checkSameOutput("test_v8_function_template", []); + it("keeps the data parameter alive", async () => { + await checkSameOutput("test_v8_function_template", []); }); }); describe("Function", () => { - it("correctly receives all its arguments from JS", () => { - checkSameOutput("print_values_from_js", [5.0, true, null, false, "meow", {}]); - checkSameOutput("print_native_function", []); + it("correctly receives all its arguments from JS", async () => { + await checkSameOutput("print_values_from_js", [5.0, true, null, false, "async meow", {}]); + await checkSameOutput("print_native_function", []); }); - it("correctly receives the this value from JS", () => { - checkSameOutput("call_function_with_weird_this_values", []); + it("correctly receives the this value from JS", async () => { + await checkSameOutput("call_function_with_weird_this_values", []); }); }); @@ -213,44 +217,56 @@ describe.todoIf(isBroken && isMusl)("node:v8", () => { }); describe("Global", () => { - it("can create, modify, and read the value from global handles", () => { - checkSameOutput("test_v8_global", []); + it("can create, modify, and read the value from global handles", async () => { + await checkSameOutput("test_v8_global", []); }); }); describe("HandleScope", () => { - it("can hold a lot of locals", () => { - checkSameOutput("test_many_v8_locals", []); + it("can hold a lot of locals", async () => { + await checkSameOutput("test_many_v8_locals", []); }); - it("keeps GC objects alive", () => { - checkSameOutput("test_handle_scope_gc", []); + it("keeps GC objects alive", async () => { + await checkSameOutput("test_handle_scope_gc", []); }, 10000); }); describe("EscapableHandleScope", () => { - it("keeps handles alive in the outer scope", () => { - checkSameOutput("test_v8_escapable_handle_scope", []); + it("keeps handles alive in the outer scope", async () => { + await checkSameOutput("test_v8_escapable_handle_scope", []); }); }); describe("uv_os_getpid", () => { - it.skipIf(isWindows)("returns the same result as getpid on POSIX", () => { - checkSameOutput("test_uv_os_getpid", []); + it.skipIf(isWindows)("returns the same result as getpid on POSIX", async () => { + await checkSameOutput("test_uv_os_getpid", []); }); }); describe("uv_os_getppid", () => { - it.skipIf(isWindows)("returns the same result as getppid on POSIX", () => { - checkSameOutput("test_uv_os_getppid", []); + it.skipIf(isWindows)("returns the same result as getppid on POSIX", async () => { + await checkSameOutput("test_uv_os_getppid", []); }); }); }); -function checkSameOutput(testName: string, args: any[], thisValue?: any) { - const nodeResult = runOn(Runtime.node, BuildMode.release, testName, args, thisValue).trim(); - let bunReleaseResult = runOn(Runtime.bun, BuildMode.release, testName, args, thisValue); - let bunDebugResult = runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue); - +async function checkSameOutput(testName: string, args: any[], thisValue?: any) { + const [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] = await Promise.allSettled([ + runOn(Runtime.node, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue), + ]); + const errors = [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] + .filter(r => r.status === "rejected") + .map(r => r.reason); + if (errors.length > 0) { + throw new AggregateError(errors); + } + let [nodeResult, bunReleaseResult, bunDebugResult] = [ + nodeResultResolution, + bunReleaseResultResolution, + bunDebugResultResolution, + ].map(r => (r as any).value); // remove all debug logs bunReleaseResult = bunReleaseResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); bunDebugResult = bunDebugResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); @@ -262,7 +278,7 @@ function checkSameOutput(testName: string, args: any[], thisValue?: any) { return nodeResult; } -function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { +async function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { if (runtime == Runtime.node) { assert(buildMode == BuildMode.release); } @@ -272,7 +288,7 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: : buildMode == BuildMode.debug ? directories.bunDebug : directories.bunRelease; - const exe = runtime == Runtime.node ? "node" : bunExe(); + const exe = runtime == Runtime.node ? (nodeExe() ?? "node") : bunExe(); const cmd = [ exe, @@ -286,16 +302,21 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: cmd.push("debug"); } - const exec = spawnSync({ + const proc = spawn({ cmd, cwd: baseDir, env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], }); - const errs = exec.stderr.toString(); + const [exitCode, out, err] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); const crashMsg = `test ${testName} crashed under ${Runtime[runtime]} in ${BuildMode[buildMode]} mode`; - if (errs !== "") { - throw new Error(`${crashMsg}: ${errs}`); + if (exitCode !== 0) { + throw new Error(`${crashMsg}: ${err}\n${out}`.trim()); } - expect(exec.success, crashMsg).toBeTrue(); - return exec.stdout.toString(); + expect(exitCode, crashMsg).toBe(0); + return out.trim(); } From 4dcfd686b4e760b45081ea4a485f877580c685c2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 16:22:58 -0800 Subject: [PATCH 05/56] Fix build --- src/compile_target.zig | 2 +- src/install/install.zig | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/compile_target.zig b/src/compile_target.zig index 5fee414738..00acaf4fad 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -120,7 +120,7 @@ pub fn exePath(this: *const CompileTarget, buf: *bun.PathBuffer, version_str: [: bun.fs.FileSystem.instance.top_level_dir, buf, &.{ - bun.install.PackageManager.fetchCacheDirectoryPath(env).path, + bun.install.PackageManager.fetchCacheDirectoryPath(env, null).path, version_str, }, .auto, diff --git a/src/install/install.zig b/src/install/install.zig index fd61861e5c..7e6649670f 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -3533,7 +3533,7 @@ pub const PackageManager = struct { noinline fn ensureCacheDirectory(this: *PackageManager) std.fs.Dir { loop: while (true) { if (this.options.enable.cache) { - const cache_dir = fetchCacheDirectoryPath(this.env, this.options); + const cache_dir = fetchCacheDirectoryPath(this.env, &this.options); this.cache_directory_path = this.allocator.dupeZ(u8, cache_dir.path) catch bun.outOfMemory(); return std.fs.cwd().makeOpenPath(cache_dir.path, .{}) catch { @@ -6234,7 +6234,7 @@ pub const PackageManager = struct { } const CacheDir = struct { path: string, is_node_modules: bool }; - pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader, options: *Options) CacheDir { + pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader, options: ?*const Options) CacheDir { if (env.get("BUN_INSTALL_CACHE_DIR")) |dir| { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; } @@ -6243,9 +6243,10 @@ pub const PackageManager = struct { var parts = [_]string{ dir, "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } - - if (options.cache_directory) |dir| { - return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; + if (options) |opts| { + if (opts.cache_directory.len > 0) { + return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{opts.cache_directory}), .is_node_modules = false }; + } } if (env.get("XDG_CACHE_HOME")) |dir| { From d9125143b7d467e57f90f310bc65b6418a531600 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:22:39 -0800 Subject: [PATCH 06/56] lockfile: escape names in `bun.lock` (#16120) --- src/install/bun.lock.zig | 31 +++++++++------ .../__snapshots__/bun-lock.test.ts.snap | 25 ++++++++++++ test/cli/install/bun-lock.test.ts | 38 ++++++++++++++++++- 3 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 test/cli/install/__snapshots__/bun-lock.test.ts.snap diff --git a/src/install/bun.lock.zig b/src/install/bun.lock.zig index a865e21978..46db80c961 100644 --- a/src/install/bun.lock.zig +++ b/src/install/bun.lock.zig @@ -556,7 +556,9 @@ pub const Stringifier = struct { try writer.writeByte('"'); // relative_path is empty string for root resolutions - try writer.writeAll(relative_path); + try writer.print("{}", .{ + bun.fmt.formatJSONStringUTF8(relative_path, .{ .quote = false }), + }); if (depth != 0) { try writer.writeByte('/'); @@ -565,8 +567,8 @@ pub const Stringifier = struct { const dep = deps_buf[dep_id]; const dep_name = dep.name.slice(buf); - try writer.print("{s}\": ", .{ - dep_name, + try writer.print("{}\": ", .{ + bun.fmt.formatJSONStringUTF8(dep_name, .{ .quote = false }), }); const pkg_name = pkg_names[pkg_id]; @@ -760,9 +762,9 @@ pub const Stringifier = struct { try writer.writeAll(", "); } - try writer.print("\"{s}\": \"{s}\"", .{ - dep.name.slice(buf), - dep.version.literal.slice(buf), + try writer.print("{}: {}", .{ + bun.fmt.formatJSONStringUTF8(dep.name.slice(buf), .{}), + bun.fmt.formatJSONStringUTF8(dep.version.literal.slice(buf), .{}), }); } @@ -779,10 +781,10 @@ pub const Stringifier = struct { for (optional_peers_buf.items, 0..) |optional_peer, i| { try writer.print( - \\{s}"{s}"{s} + \\{s}{}{s} , .{ if (i != 0) " " else "", - optional_peer.slice(buf), + bun.fmt.formatJSONStringUTF8(optional_peer.slice(buf), .{}), if (i != optional_peers_buf.items.len - 1) "," else "", }); } @@ -879,8 +881,8 @@ pub const Stringifier = struct { }); try writer.writeByte('\n'); try incIndent(writer, indent); - try writer.print("\"name\": \"{s}\"", .{ - pkg_names[pkg_id].slice(buf), + try writer.print("\"name\": {}", .{ + bun.fmt.formatJSONStringUTF8(pkg_names[pkg_id].slice(buf), .{}), }); if (workspace_versions.get(pkg_name_hashes[pkg_id])) |version| { @@ -929,7 +931,10 @@ pub const Stringifier = struct { const name = dep.name.slice(buf); const version = dep.version.literal.slice(buf); - try writer.print("\"{s}\": \"{s}\"", .{ name, version }); + try writer.print("{}: {}", .{ + bun.fmt.formatJSONStringUTF8(name, .{}), + bun.fmt.formatJSONStringUTF8(version, .{}), + }); } if (!first) { @@ -955,7 +960,9 @@ pub const Stringifier = struct { try writer.print( \\"{s}", \\ - , .{optional_peer.slice(buf)}); + , .{ + bun.fmt.formatJSONStringUTF8(optional_peer.slice(buf), .{}), + }); } try decIndent(writer, indent); try writer.writeByte(']'); diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap new file mode 100644 index 0000000000..8a80309993 --- /dev/null +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -0,0 +1,25 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`should escape names 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": {}, + "packages/\\"": { + "name": "\\"", + }, + "packages/pkg1": { + "name": "pkg1", + "dependencies": { + "\\"": "*", + }, + }, + }, + "packages": { + "\\"": ["\\"@workspace:packages/\\"", {}], + + "pkg1": ["pkg1@workspace:packages/pkg1", { "dependencies": { "\\"": "*" } }], + } +} +" +`; diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index d5d6ce6c3c..8c99857070 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun"; +import { spawn, write, file } from "bun"; import { expect, it } from "bun:test"; import { access, copyFile, open, writeFile } from "fs/promises"; import { bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; @@ -49,3 +49,39 @@ it("should write plaintext lockfiles", async () => { `{\n \"lockfileVersion\": 0,\n \"workspaces\": {\n \"\": {\n \"dependencies\": {\n \"dummy-package\": \"file:./bar-0.0.2.tgz\",\n },\n },\n },\n \"packages\": {\n \"dummy-package\": [\"bar@./bar-0.0.2.tgz\", {}],\n }\n}\n`, ); }); + +// won't work on windows, " is not a valid character in a filename +it.skipIf(isWindows)("should escape names", async () => { + const packageDir = tmpdirSync(); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "quote-in-dependency-name", + workspaces: ["packages/*"], + }), + ), + write(join(packageDir, "packages", '"', "package.json"), JSON.stringify({ name: '"' })), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + '"': "*", + }, + }), + ), + ]); + + const { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "bun.lock")).text()).toMatchSnapshot(); +}); From 40724d29ac3a37834651106d9ca53dbf9623d8a1 Mon Sep 17 00:00:00 2001 From: Yiheng Date: Fri, 3 Jan 2025 10:24:03 +0800 Subject: [PATCH 07/56] Update cache.md (#16028) --- docs/install/cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install/cache.md b/docs/install/cache.md index 6543ec87a4..f03f75e432 100644 --- a/docs/install/cache.md +++ b/docs/install/cache.md @@ -1,4 +1,4 @@ -All packages downloaded from the registry are stored in a global cache at `~/.bun/install/cache`. They are stored in subdirectories named like `${name}@${version}`, so multiple versions of a package can be cached. +All packages downloaded from the registry are stored in a global cache at `~/.bun/install/cache`, or the path defined by the environment variable `BUN_INSTALL_CACHE_DIR`. They are stored in subdirectories named like `${name}@${version}`, so multiple versions of a package can be cached. {% details summary="Configuring cache behavior (bunfig.toml)" %} From b59e7c7682e0a783be894eb4617db891f3f68fd7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 18:55:38 -0800 Subject: [PATCH 08/56] Add missing exception checks to JSPropertyIterator (#16121) Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> --- src/bun.js/ConsoleObject.zig | 16 +-- src/bun.js/api/JSBundler.zig | 8 +- src/bun.js/api/JSTranspiler.zig | 11 +- src/bun.js/api/bun/h2_frame_parser.zig | 8 +- src/bun.js/api/bun/subprocess.zig | 7 +- src/bun.js/api/ffi.zig | 8 +- src/bun.js/api/server.zig | 4 +- src/bun.js/bindings/JSPropertyIterator.zig | 71 +++++++----- src/bun.js/bindings/bindings.zig | 2 +- src/bun.js/javascript.zig | 4 +- src/bun.js/node/util/parse_args.zig | 4 +- src/bun.js/test/expect.zig | 6 +- src/bun.js/test/pretty_format.zig | 123 +++++++++++++-------- src/ini.zig | 4 +- src/install/install.zig | 1 + src/js_ast.zig | 6 +- src/shell/interpreter.zig | 18 ++- test/js/bun/spawn/spawn-env.test.ts | 24 ++++ 18 files changed, 195 insertions(+), 130 deletions(-) create mode 100644 test/js/bun/spawn/spawn-env.test.ts diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 9def500ece..8996a196da 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -378,13 +378,13 @@ pub const TablePrinter = struct { } } } else { - var cols_iter = JSC.JSPropertyIterator(.{ + var cols_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(this.globalObject, row_value); defer cols_iter.deinit(); - while (cols_iter.next()) |col_key| { + while (try cols_iter.next()) |col_key| { const value = cols_iter.value; // find or create the column for the property @@ -561,13 +561,13 @@ pub const TablePrinter = struct { }.callback); if (ctx_.err) return error.JSError; } else { - var rows_iter = JSC.JSPropertyIterator(.{ + var rows_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, this.tabular_data); defer rows_iter.deinit(); - while (rows_iter.next()) |row_key| { + while (try rows_iter.next()) |row_key| { try this.updateColumnsForRow(&columns, .{ .str = String.init(row_key) }, rows_iter.value); } } @@ -634,13 +634,13 @@ pub const TablePrinter = struct { }.callback); if (ctx_.err) return error.JSError; } else { - var rows_iter = JSC.JSPropertyIterator(.{ + var rows_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, this.tabular_data); defer rows_iter.deinit(); - while (rows_iter.next()) |row_key| { + while (try rows_iter.next()) |row_key| { try this.printRow(Writer, writer, enable_ansi_colors, &columns, .{ .str = String.init(row_key) }, rows_iter.value); } } @@ -2995,7 +2995,7 @@ pub const Formatter = struct { this.quote_strings = true; defer this.quote_strings = prev_quote_strings; - var props_iter = JSC.JSPropertyIterator(.{ + var props_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -3009,7 +3009,7 @@ pub const Formatter = struct { defer this.indent -|= 1; const count_without_children = props_iter.len - @as(usize, @intFromBool(children_prop != null)); - while (props_iter.next()) |prop| { + while (try props_iter.next()) |prop| { if (prop.eqlComptime("children")) continue; diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 78ebaee29b..a75e260f72 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -453,13 +453,13 @@ pub const JSBundler = struct { return globalThis.throwInvalidArguments("define must be an object", .{}); } - var define_iter = JSC.JSPropertyIterator(.{ + var define_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(globalThis, define); defer define_iter.deinit(); - while (define_iter.next()) |prop| { + while (try define_iter.next()) |prop| { const property_value = define_iter.value; const value_type = property_value.jsType(); @@ -485,7 +485,7 @@ pub const JSBundler = struct { } if (try config.getOwnObject(globalThis, "loader")) |loaders| { - var loader_iter = JSC.JSPropertyIterator(.{ + var loader_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(globalThis, loaders); @@ -496,7 +496,7 @@ pub const JSBundler = struct { var loader_values = try allocator.alloc(Api.Loader, loader_iter.len); errdefer allocator.free(loader_values); - while (loader_iter.next()) |prop| { + while (try loader_iter.next()) |prop| { if (!prop.hasPrefixComptime(".") or prop.length() < 2) { return globalThis.throwInvalidArguments("loader property names must be file extensions, such as '.txt'", .{}); } diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index d53c20996d..7eb446bea2 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -338,7 +338,7 @@ fn transformOptionsFromJSC(globalObject: JSC.C.JSContextRef, temp_allocator: std return globalObject.throwInvalidArguments("define must be an object", .{}); } - var define_iter = JSC.JSPropertyIterator(.{ + var define_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -351,7 +351,7 @@ fn transformOptionsFromJSC(globalObject: JSC.C.JSContextRef, temp_allocator: std var values = map_entries[define_iter.len..]; - while (define_iter.next()) |prop| { + while (try define_iter.next()) |prop| { const property_value = define_iter.value; const value_type = property_value.jsType(); @@ -624,26 +624,25 @@ fn transformOptionsFromJSC(globalObject: JSC.C.JSContextRef, temp_allocator: std return globalObject.throwInvalidArguments("replace must be an object", .{}); } - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(globalThis, replace); + defer iter.deinit(); if (iter.len > 0) { - errdefer iter.deinit(); try replacements.ensureUnusedCapacity(bun.default_allocator, iter.len); // We cannot set the exception before `try` because it could be // a double free with the `errdefer`. defer if (globalThis.hasException()) { - iter.deinit(); for (replacements.keys()) |key| { bun.default_allocator.free(@constCast(key)); } replacements.clearAndFree(bun.default_allocator); }; - while (iter.next()) |key_| { + while (try iter.next()) |key_| { const value = iter.value; if (value == .zero) continue; diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 05d30aa613..41d8ef85d6 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -2899,14 +2899,14 @@ pub const H2FrameParser = struct { var buffer = shared_request_buffer[0 .. shared_request_buffer.len - FrameHeader.byteSize]; var encoded_size: usize = 0; - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, headers_arg); defer iter.deinit(); // TODO: support CONTINUE for more headers if headers are too big - while (iter.next()) |header_name| { + while (try iter.next()) |header_name| { if (header_name.length() == 0) continue; const name_slice = header_name.toUTF8(bun.default_allocator); @@ -3231,7 +3231,7 @@ pub const H2FrameParser = struct { } // we iterate twice, because pseudo headers must be sent first, but can appear anywhere in the headers object - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalObject, headers_arg); @@ -3240,7 +3240,7 @@ pub const H2FrameParser = struct { for (0..2) |ignore_pseudo_headers| { iter.reset(); - while (iter.next()) |header_name| { + while (try iter.next()) |header_name| { if (header_name.length() == 0) continue; const name_slice = header_name.toUTF8(bun.default_allocator); diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 1bdb200b64..f638466a7d 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -122,17 +122,18 @@ pub const ResourceUsage = struct { }; pub fn appendEnvpFromJS(globalThis: *JSC.JSGlobalObject, object: JSC.JSValue, envp: *std.ArrayList(?[*:0]const u8), PATH: *[]const u8) !void { - var object_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, object); + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, object); defer object_iter.deinit(); + try envp.ensureTotalCapacityPrecise(object_iter.len + // +1 incase there's IPC // +1 for null terminator 2); - while (object_iter.next()) |key| { + while (try object_iter.next()) |key| { var value = object_iter.value; if (value == .undefined) continue; - var line = try std.fmt.allocPrintZ(envp.allocator, "{}={}", .{ key, value.getZigString(globalThis) }); + const line = try std.fmt.allocPrintZ(envp.allocator, "{}={}", .{ key, value.getZigString(globalThis) }); if (key.eqlComptime("PATH")) { PATH.* = bun.asByteSlice(line["PATH=".len..]); diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index 9fffa7bb9d..4fad8407d1 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -706,9 +706,9 @@ pub const FFI = struct { if (try object.getTruthy(globalThis, "define")) |define_value| { if (define_value.isObject()) { const Iter = JSC.JSPropertyIterator(.{ .include_value = true, .skip_empty_name = true }); - var iter = Iter.init(globalThis, define_value); + var iter = try Iter.init(globalThis, define_value); defer iter.deinit(); - while (iter.next()) |entry| { + while (try iter.next()) |entry| { const key = entry.toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory(); var owned_value: [:0]const u8 = ""; if (iter.value != .zero and iter.value != .undefined) { @@ -1421,7 +1421,7 @@ pub const FFI = struct { JSC.markBinding(@src()); const allocator = VirtualMachine.get().allocator; - var symbols_iter = JSC.JSPropertyIterator(.{ + var symbols_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -1430,7 +1430,7 @@ pub const FFI = struct { try symbols.ensureTotalCapacity(allocator, symbols_iter.len); - while (symbols_iter.next()) |prop| { + while (try symbols_iter.next()) |prop| { const value = symbols_iter.value; if (value.isEmptyOrUndefinedOrNull()) { diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 71be97b4be..f162020626 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1222,13 +1222,13 @@ pub const ServerConfig = struct { return global.throwInvalidArguments("Bun.serve expects 'static' to be an object shaped like { [pathname: string]: Response }", .{}); } - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(global, static); defer iter.deinit(); - while (iter.next()) |key| { + while (try iter.next()) |key| { const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory(); const value = iter.value; diff --git a/src/bun.js/bindings/JSPropertyIterator.zig b/src/bun.js/bindings/JSPropertyIterator.zig index bd55ea078c..353d89d83a 100644 --- a/src/bun.js/bindings/JSPropertyIterator.zig +++ b/src/bun.js/bindings/JSPropertyIterator.zig @@ -41,13 +41,19 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { this.* = undefined; } - pub fn init(globalObject: *JSC.JSGlobalObject, object: JSC.JSValue) @This() { + pub fn init(globalObject: *JSC.JSGlobalObject, object: JSC.JSValue) bun.JSError!@This() { var iter = @This(){ .object = object.asCell(), .globalObject = globalObject, }; iter.impl = Bun__JSPropertyIterator__create(globalObject, object, &iter.len, options.own_properties_only, options.only_non_index_properties); + if (globalObject.hasException()) { + return error.JSError; + } + if (iter.len > 0) { + bun.debugAssert(iter.impl != null); + } return iter; } @@ -58,39 +64,48 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { } /// The bun.String returned has not incremented it's reference count. - pub fn next(this: *@This()) ?bun.String { - const i: usize = this.iter_i; - if (i >= this.len) { + pub fn next(this: *@This()) !?bun.String { + // Reuse stack space. + while (true) { + const i: usize = this.iter_i; + if (i >= this.len) { + this.i = this.iter_i; + return null; + } + this.i = this.iter_i; - return null; - } - - this.i = this.iter_i; - this.iter_i += 1; - var name = bun.String.dead; - if (comptime options.include_value) { - const FnToUse = if (options.observable) Bun__JSPropertyIterator__getNameAndValue else Bun__JSPropertyIterator__getNameAndValueNonObservable; - const current = FnToUse(this.impl, this.globalObject, this.object, &name, i); - if (current == .zero) { - return this.next(); + this.iter_i += 1; + var name = bun.String.dead; + if (comptime options.include_value) { + const FnToUse = if (options.observable) Bun__JSPropertyIterator__getNameAndValue else Bun__JSPropertyIterator__getNameAndValueNonObservable; + const current = FnToUse(this.impl, this.globalObject, this.object, &name, i); + if (current == .zero) { + if (this.globalObject.hasException()) { + return error.JSError; + } + continue; + } + current.ensureStillAlive(); + this.value = current; + } else { + // Exception check is unnecessary here because it won't throw. + Bun__JSPropertyIterator__getName(this.impl, &name, i); } - current.ensureStillAlive(); - this.value = current; - } else { - Bun__JSPropertyIterator__getName(this.impl, &name, i); - } - if (name.tag == .Dead) { - return this.next(); - } - - if (comptime options.skip_empty_name) { - if (name.isEmpty()) { - return this.next(); + if (name.tag == .Dead) { + continue; } + + if (comptime options.skip_empty_name) { + if (name.isEmpty()) { + continue; + } + } + + return name; } - return name; + unreachable; } /// "code" is not always an own property, and we want to get it without risking exceptions. diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 909e8dc1e2..4fc4b0ddee 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4492,7 +4492,7 @@ pub const JSValue = enum(i64) { .quote_strings = true, }; - JestPrettyFormat.format( + try JestPrettyFormat.format( .Debug, globalObject, @as([*]const JSValue, @ptrCast(&this)), diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 3444695992..6b55cf2f21 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -3952,11 +3952,11 @@ pub const VirtualMachine = struct { .observable = false, .only_non_index_properties = true, }); - var iterator = Iterator.init(this.global, error_instance); + var iterator = try Iterator.init(this.global, error_instance); defer iterator.deinit(); const longest_name = @min(iterator.getLongestPropertyName(), 10); var is_first_property = true; - while (iterator.next() orelse iterator.getCodeProperty()) |field| { + while ((try iterator.next()) orelse iterator.getCodeProperty()) |field| { const value = iterator.value; if (field.eqlComptime("message") or field.eqlComptime("name") or field.eqlComptime("stack")) { continue; diff --git a/src/bun.js/node/util/parse_args.zig b/src/bun.js/node/util/parse_args.zig index 221823fcb1..3c1e61e160 100644 --- a/src/bun.js/node/util/parse_args.zig +++ b/src/bun.js/node/util/parse_args.zig @@ -300,13 +300,13 @@ fn storeOption(globalThis: *JSGlobalObject, option_name: ValueRef, option_value: fn parseOptionDefinitions(globalThis: *JSGlobalObject, options_obj: JSValue, option_definitions: *std.ArrayList(OptionDefinition)) bun.JSError!void { try validateObject(globalThis, options_obj, "options", .{}, .{}); - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, options_obj); defer iter.deinit(); - while (iter.next()) |long_option| { + while (try iter.next()) |long_option| { var option = OptionDefinition{ .long_name = String.init(long_option), }; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index a9ef928354..ae4b6b2478 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2882,7 +2882,7 @@ pub const Expect = struct { }.anythingInIterator); pass = !any_properties_in_iterator; } else { - var props_iter = JSC.JSPropertyIterator(.{ + var props_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, @@ -4525,13 +4525,13 @@ pub const Expect = struct { const matchers_to_register = args[0]; { - var iter = JSC.JSPropertyIterator(.{ + var iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, matchers_to_register); defer iter.deinit(); - while (iter.next()) |*matcher_name| { + while (try iter.next()) |*matcher_name| { const matcher_fn: JSValue = iter.value; if (!matcher_fn.jsType().isFunction()) { diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 2934577d49..ac9b1fdcd7 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -95,7 +95,7 @@ pub const JestPrettyFormat = struct { comptime Writer: type, writer: Writer, options: FormatOptions, - ) void { + ) bun.JSError!void { var fmt: JestPrettyFormat.Formatter = undefined; defer { if (fmt.map_node) |node| { @@ -123,7 +123,7 @@ pub const JestPrettyFormat = struct { if (level == .Error) { unbuffered_writer.writeAll(comptime Output.prettyFmt("", true)) catch unreachable; } - fmt.format( + try fmt.format( tag, @TypeOf(unbuffered_writer), unbuffered_writer, @@ -135,7 +135,7 @@ pub const JestPrettyFormat = struct { unbuffered_writer.writeAll(comptime Output.prettyFmt("", true)) catch unreachable; } } else { - fmt.format( + try fmt.format( tag, @TypeOf(unbuffered_writer), unbuffered_writer, @@ -152,7 +152,7 @@ pub const JestPrettyFormat = struct { } } if (options.enable_colors) { - fmt.format( + try fmt.format( tag, Writer, writer, @@ -161,7 +161,7 @@ pub const JestPrettyFormat = struct { true, ); } else { - fmt.format( + try fmt.format( tag, Writer, writer, @@ -206,7 +206,7 @@ pub const JestPrettyFormat = struct { tag.tag = .StringPossiblyFormatted; } - fmt.format(tag, Writer, writer, this_value, global, true); + try fmt.format(tag, Writer, writer, this_value, global, true); if (fmt.remaining_values.len == 0) { break; } @@ -228,7 +228,7 @@ pub const JestPrettyFormat = struct { tag.tag = .StringPossiblyFormatted; } - fmt.format(tag, Writer, writer, this_value, global, false); + try fmt.format(tag, Writer, writer, this_value, global, false); if (fmt.remaining_values.len == 0) break; @@ -574,13 +574,13 @@ pub const JestPrettyFormat = struct { 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) catch {}, // TODO: - Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch {}, // TODO: - Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch {}, // TODO: - Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch {}, // TODO: + Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, + Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors) catch return, // undefined is overloaded to mean the '%o" field - Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors), + Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors) catch return, else => unreachable, } @@ -680,9 +680,10 @@ pub const JestPrettyFormat = struct { writer: Writer, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); + if (this.formatter.failed) return; const key = JSC.JSObject.getIndex(nextValue, globalObject, 0); const value = JSC.JSObject.getIndex(nextValue, globalObject, 1); - this.formatter.writeIndent(Writer, this.writer) catch unreachable; + this.formatter.writeIndent(Writer, this.writer) catch return; const key_tag = Tag.get(key, globalObject); this.formatter.format( @@ -692,8 +693,8 @@ pub const JestPrettyFormat = struct { key, this.formatter.globalThis, enable_ansi_colors, - ); - this.writer.writeAll(" => ") catch unreachable; + ) catch return; + this.writer.writeAll(" => ") catch return; const value_tag = Tag.get(value, globalObject); this.formatter.format( value_tag, @@ -702,9 +703,9 @@ pub const JestPrettyFormat = struct { value, this.formatter.globalThis, enable_ansi_colors, - ); - this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; - this.writer.writeAll("\n") catch unreachable; + ) catch return; + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch return; + this.writer.writeAll("\n") catch return; } }; } @@ -715,7 +716,8 @@ pub const JestPrettyFormat = struct { writer: Writer, pub fn forEach(_: [*c]JSC.VM, globalObject: *JSGlobalObject, ctx: ?*anyopaque, nextValue: JSValue) callconv(.C) void { var this: *@This() = bun.cast(*@This(), ctx orelse return); - this.formatter.writeIndent(Writer, this.writer) catch {}; + if (this.formatter.failed) return; + this.formatter.writeIndent(Writer, this.writer) catch return; const key_tag = Tag.get(nextValue, globalObject); this.formatter.format( key_tag, @@ -724,10 +726,9 @@ pub const JestPrettyFormat = struct { nextValue, this.formatter.globalThis, enable_ansi_colors, - ); - - this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch unreachable; - this.writer.writeAll("\n") catch unreachable; + ) catch return; + this.formatter.printComma(Writer, this.writer, enable_ansi_colors) catch return; + this.writer.writeAll("\n") catch return; } }; } @@ -790,6 +791,8 @@ pub const JestPrettyFormat = struct { var ctx: *@This() = bun.cast(*@This(), ctx_ptr orelse return); var this = ctx.formatter; const writer_ = ctx.writer; + if (this.failed) return; + var writer = WrappedWriter(Writer){ .ctx = writer_, .failed = false, @@ -801,14 +804,14 @@ pub const JestPrettyFormat = struct { if (ctx.i == 0) { handleFirstProperty(ctx, globalThis, ctx.parent); } else { - this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + this.printComma(Writer, writer_, enable_ansi_colors) catch return; } 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.writeIndent(Writer, writer_) catch return; this.resetLine(); } else { this.estimated_line_length += 1; @@ -880,7 +883,7 @@ pub const JestPrettyFormat = struct { } } - this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors); + this.format(tag, Writer, ctx.writer, value, globalThis, enable_ansi_colors) catch return; if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -899,7 +902,7 @@ pub const JestPrettyFormat = struct { value: JSValue, jsType: JSValue.JSType, comptime enable_ansi_colors: bool, - ) error{}!void { + ) bun.JSError!void { if (this.failed) return; var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length }; @@ -1174,7 +1177,7 @@ pub const JestPrettyFormat = struct { this.writeIndent(Writer, writer_) catch unreachable; this.addForNewLine(1); - this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1197,7 +1200,7 @@ pub const JestPrettyFormat = struct { 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); + try this.format(tag, Writer, writer_, element, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1223,16 +1226,42 @@ pub const JestPrettyFormat = struct { }, .Private => { if (value.as(JSC.WebCore.Response)) |response| { - response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; - return; + response.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Response"); + } + return error.JSError; + }; } else if (value.as(JSC.WebCore.Request)) |request| { - request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + request.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Request"); + } + return error.JSError; + }; return; } else if (value.as(JSC.API.BuildArtifact)) |build| { - build.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; - return; + build.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print BuildArtifact"); + } + return error.JSError; + }; } else if (value.as(JSC.WebCore.Blob)) |blob| { - blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch {}; + blob.writeFormat(Formatter, this, writer_, enable_ansi_colors) catch |err| { + this.failed = true; + // TODO: make this better + if (!this.globalThis.hasException()) { + return this.globalThis.throwError(err, "failed to print Blob"); + } + return error.JSError; + }; return; } else if (value.as(JSC.DOMFormData) != null) { const toJSONFunction = value.get_unsafe(this.globalThis, "toJSON").?; @@ -1244,7 +1273,7 @@ pub const JestPrettyFormat = struct { .Object, Writer, writer_, - toJSONFunction.call(this.globalThis, value, &.{}) catch |err| this.globalThis.takeException(err), + try toJSONFunction.call(this.globalThis, value, &.{}), .Object, enable_ansi_colors, ); @@ -1434,7 +1463,7 @@ pub const JestPrettyFormat = struct { ); const tag = Tag.get(message_value, this.globalThis); - this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); writer.writeAll(", \n"); } } @@ -1450,9 +1479,9 @@ pub const JestPrettyFormat = struct { const tag = Tag.get(data, this.globalThis); if (tag.cell.isStringLike()) { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } else { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } writer.writeAll(", \n"); }, @@ -1465,7 +1494,7 @@ pub const JestPrettyFormat = struct { ); const tag = Tag.get(data, this.globalThis); - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); writer.writeAll("\n"); } }, @@ -1530,7 +1559,7 @@ pub const JestPrettyFormat = struct { 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); + try this.format(Tag.get(key_value, this.globalThis), Writer, writer_, key_value, this.globalThis, enable_ansi_colors); needs_space = true; } @@ -1541,7 +1570,7 @@ pub const JestPrettyFormat = struct { this.quote_strings = true; defer this.quote_strings = prev_quote_strings; - var props_iter = JSC.JSPropertyIterator(.{ + var props_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -1555,7 +1584,7 @@ pub const JestPrettyFormat = struct { defer this.indent -|= 1; const count_without_children = props_iter.len - @as(usize, @intFromBool(children_prop != null)); - while (props_iter.next()) |prop| { + while (try props_iter.next()) |prop| { if (prop.eqlComptime("children")) continue; @@ -1578,7 +1607,7 @@ pub const JestPrettyFormat = struct { } } - this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); + try this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors); if (tag.cell.isStringLike()) { if (comptime enable_ansi_colors) { @@ -1641,7 +1670,7 @@ pub const JestPrettyFormat = struct { 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); + try this.format(Tag.get(children, this.globalThis), Writer, writer_, children, this.globalThis, enable_ansi_colors); } writer.writeAll("\n"); @@ -1664,7 +1693,7 @@ pub const JestPrettyFormat = struct { var j: usize = 0; while (j < length) : (j += 1) { const child = JSC.JSObject.getIndex(children, this.globalThis, @as(u32, @intCast(j))); - this.format(Tag.get(child, this.globalThis), Writer, writer_, child, this.globalThis, enable_ansi_colors); + try 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; @@ -1949,7 +1978,7 @@ pub const JestPrettyFormat = struct { } } - pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) void { + pub fn format(this: *JestPrettyFormat.Formatter, result: Tag.Result, comptime Writer: type, writer: Writer, value: JSValue, globalThis: *JSGlobalObject, comptime enable_ansi_colors: bool) bun.JSError!void { if (comptime is_bindgen) { return; } diff --git a/src/ini.zig b/src/ini.zig index 77fe57fe03..ab519a1ace 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -530,7 +530,7 @@ pub const IniTestingAPIs = struct { const envjs = callframe.argument(1); const env = if (envjs.isEmptyOrUndefinedOrNull()) globalThis.bunVM().transpiler.env else brk: { var envmap = bun.DotEnv.Map.HashTable.init(allocator); - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, envjs); @@ -538,7 +538,7 @@ pub const IniTestingAPIs = struct { try envmap.ensureTotalCapacity(object_iter.len); - while (object_iter.next()) |key| { + while (try object_iter.next()) |key| { const keyslice = try key.toOwnedSlice(allocator); var value = object_iter.value; if (value == .undefined) continue; diff --git a/src/install/install.zig b/src/install/install.zig index 7e6649670f..fb21bdec05 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6243,6 +6243,7 @@ pub const PackageManager = struct { var parts = [_]string{ dir, "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } + if (options) |opts| { if (opts.cache_directory.len > 0) { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{opts.cache_directory}), .is_node_modules = false }; diff --git a/src/js_ast.zig b/src/js_ast.zig index 3216d457a1..fe7456598e 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -8129,7 +8129,7 @@ pub const Macro = struct { threadlocal var args_buf: [3]js.JSObjectRef = undefined; threadlocal var exception_holder: Zig.ZigException.Holder = undefined; - pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError; + pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError || bun.JSError; pub const Run = struct { caller: Expr, @@ -8344,7 +8344,7 @@ pub const Macro = struct { return _entry.value_ptr.*; } - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(this.global, value); @@ -8361,7 +8361,7 @@ pub const Macro = struct { ); _entry.value_ptr.* = out; - while (object_iter.next()) |prop| { + while (try object_iter.next()) |prop| { properties[object_iter.i] = G.Property{ .key = Expr.init(E.String, E.String.init(prop.toOwnedSlice(this.allocator) catch unreachable), this.caller.loc), .value = try this.run(object_iter.value), diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 58a3a915f7..4fbb9a564b 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -721,31 +721,24 @@ pub const ParsedShellScript = struct { } pub fn setEnv(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var env = - if (this.export_env) |*env| - brk: { - env.clearRetainingCapacity(); - break :brk env.*; - } else EnvMap.init(bun.default_allocator); - defer this.export_env = env; - const value1 = callframe.argument(0); if (!value1.isObject()) { return globalThis.throwInvalidArguments("env must be an object", .{}); } - var object_iter = JSC.JSPropertyIterator(.{ + var object_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, value1); defer object_iter.deinit(); + var env: EnvMap = EnvMap.init(bun.default_allocator); env.ensureTotalCapacity(object_iter.len); // If the env object does not include a $PATH, it must disable path lookup for argv[0] // PATH = ""; - while (object_iter.next()) |key| { + while (try object_iter.next()) |key| { const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); var value = object_iter.value; if (value == .undefined) continue; @@ -759,7 +752,10 @@ pub const ParsedShellScript = struct { env.insert(keyref, valueref); } - + if (this.export_env) |*previous| { + previous.deinit(); + } + this.export_env = env; return .undefined; } diff --git a/test/js/bun/spawn/spawn-env.test.ts b/test/js/bun/spawn/spawn-env.test.ts new file mode 100644 index 0000000000..5d2e34cc0e --- /dev/null +++ b/test/js/bun/spawn/spawn-env.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from "bun:test"; +import { spawn } from "bun"; +import { bunExe } from "harness"; + +test("spawn env", async () => { + const env = {}; + Object.defineProperty(env, "LOL", { + get() { + throw new Error("Bad!!"); + }, + configurable: false, + enumerable: true, + }); + + // This was the minimum to reliably cause a crash in Bun < v1.1.42 + for (let i = 0; i < 1024 * 10; i++) { + try { + const result = spawn({ + env, + cmd: [bunExe(), "-e", "console.log(process.env.LOL)"], + }); + } catch (e) {} + } +}); From f834304c27e1ecab2d7169e1939a6b215b4fbc76 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 20:15:13 -0800 Subject: [PATCH 09/56] Support generating V8 Heap Snapshots (#16109) --- cmake/tools/SetupWebKit.cmake | 2 +- packages/bun-types/bun.d.ts | 19 ++++++- src/bun.js/api/BunObject.zig | 6 -- src/bun.js/bindings/BunObject.cpp | 46 ++++++++++++++- src/bun.js/bindings/CommonJSModuleRecord.cpp | 57 ++++++++++++++----- src/codegen/generate-classes.ts | 32 ++++++++++- src/codegen/generate-jssink.ts | 30 ++++++++-- src/js/node/v8.ts | 15 ++++- test/bun.lockb | Bin 435666 -> 437514 bytes test/js/bun/util/v8-heap-snapshot.test.ts | 34 +++++++++++ test/package.json | 1 + 11 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 test/js/bun/util/v8-heap-snapshot.test.ts diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 7ff6ffca0d..10ac6d22a9 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION 30046aef5ec6590c74c6a696e4f01683f962a6a2) + set(WEBKIT_VERSION 05798ff248070ff86118ae40583f759dbd193c6f) endif() if(WEBKIT_LOCAL) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 96e6cbf5ae..c9690785a7 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3580,9 +3580,24 @@ declare module "bun" { function nanoseconds(): number; /** - * Generate a heap snapshot for seeing where the heap is being used + * Show precise statistics about memory usage of your application + * + * Generate a heap snapshot in JavaScriptCore's format that can be viewed with `bun --inspect` or Safari's Web Inspector */ - function generateHeapSnapshot(): HeapSnapshot; + function generateHeapSnapshot(format?: "jsc"): HeapSnapshot; + + /** + * Show precise statistics about memory usage of your application + * + * Generate a V8 Heap Snapshot that can be used with Chrome DevTools & Visual Studio Code + * + * This is a JSON string that can be saved to a file. + * ```ts + * const snapshot = Bun.generateHeapSnapshot("v8"); + * await Bun.write("heap.heapsnapshot", snapshot); + * ``` + */ + function generateHeapSnapshot(format: "v8"): string; /** * The next time JavaScriptCore is idle, clear unused memory and attempt to reduce the heap size. diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a3a494b1a1..de110e5bda 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -18,7 +18,6 @@ pub const BunObject = struct { pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter); pub const deflateSync = toJSCallback(JSZlib.deflateSync); pub const file = toJSCallback(WebCore.Blob.constructBunFile); - pub const generateHeapSnapshot = toJSCallback(Bun.generateHeapSnapshot); pub const gunzipSync = toJSCallback(JSZlib.gunzipSync); pub const gzipSync = toJSCallback(JSZlib.gzipSync); pub const indexOfLine = toJSCallback(Bun.indexOfLine); @@ -145,7 +144,6 @@ pub const BunObject = struct { @export(BunObject.createShellInterpreter, .{ .name = callbackName("createShellInterpreter") }); @export(BunObject.deflateSync, .{ .name = callbackName("deflateSync") }); @export(BunObject.file, .{ .name = callbackName("file") }); - @export(BunObject.generateHeapSnapshot, .{ .name = callbackName("generateHeapSnapshot") }); @export(BunObject.gunzipSync, .{ .name = callbackName("gunzipSync") }); @export(BunObject.gzipSync, .{ .name = callbackName("gzipSync") }); @export(BunObject.indexOfLine, .{ .name = callbackName("indexOfLine") }); @@ -830,10 +828,6 @@ pub fn sleepSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b return .undefined; } -pub fn generateHeapSnapshot(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - return globalObject.generateHeapSnapshot(); -} - pub fn gc(vm: *JSC.VirtualMachine, sync: bool) usize { return vm.garbageCollect(sync); } diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 5e1062a2bb..576e9a9baa 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -1,4 +1,7 @@ #include "root.h" + +#include "JavaScriptCore/HeapProfiler.h" +#include #include "ZigGlobalObject.h" #include "JavaScriptCore/ArgList.h" #include "JSDOMURL.h" @@ -34,6 +37,8 @@ #include "ErrorCode.h" #include "GeneratedBunObject.h" +#include "JavaScriptCore/BunV8HeapSnapshotBuilder.h" + BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__lookup); BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolve); BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveSrv); @@ -528,6 +533,45 @@ JSC_DEFINE_HOST_FUNCTION(functionPathToFileURL, (JSC::JSGlobalObject * lexicalGl RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(jsValue)); } +JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + vm.ensureHeapProfiler(); + auto& heapProfiler = *vm.heapProfiler(); + heapProfiler.clearSnapshots(); + + JSValue arg0 = callFrame->argument(0); + auto throwScope = DECLARE_THROW_SCOPE(vm); + bool useV8 = false; + if (!arg0.isUndefined()) { + if (arg0.isString()) { + auto str = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + if (str == "v8"_s) { + useV8 = true; + } else if (str == "jsc"_s) { + // do nothing + } else { + throwTypeError(globalObject, throwScope, "Expected 'v8' or 'jsc' or undefined"_s); + return {}; + } + } + } + + if (useV8) { + JSC::BunV8HeapSnapshotBuilder builder(heapProfiler); + return JSC::JSValue::encode(jsString(vm, builder.json())); + } + + JSC::HeapSnapshotBuilder builder(heapProfiler); + builder.buildSnapshot(); + auto json = builder.json(); + // Returning an object was a bad idea but it's a breaking change + // so we'll just keep it for now. + JSC::JSValue jsonValue = JSONParseWithException(globalObject, json); + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(jsonValue)); +} + JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = globalObject->vm(); @@ -604,7 +648,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj file BunObject_callback_file DontDelete|Function 1 fileURLToPath functionFileURLToPath DontDelete|Function 1 gc Generated::BunObject::jsGc DontDelete|Function 1 - generateHeapSnapshot BunObject_callback_generateHeapSnapshot DontDelete|Function 1 + generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 1 gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1 gzipSync BunObject_callback_gzipSync DontDelete|Function 1 hash BunObject_getter_wrap_hash DontDelete|PropertyCallback diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 9573cfa46a..5fb4cdb9d2 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -1016,11 +1016,13 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) JSCommonJSModule* thisObject = jsCast(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_id); - visitor.append(thisObject->m_filename); - visitor.append(thisObject->m_dirname); - visitor.append(thisObject->m_paths); - visitor.append(thisObject->m_overridenParent); + + // Use appendHidden so it doesn't show up in the heap snapshot twice. + visitor.appendHidden(thisObject->m_id); + visitor.appendHidden(thisObject->m_filename); + visitor.appendHidden(thisObject->m_dirname); + visitor.appendHidden(thisObject->m_paths); + visitor.appendHidden(thisObject->m_overridenParent); } DEFINE_VISIT_CHILDREN(JSCommonJSModule); @@ -1029,18 +1031,43 @@ void JSCommonJSModule::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { auto* thisObject = jsCast(cell); - if (auto* id = thisObject->m_id.get()) { - if (!id->isRope()) { - auto label = id->tryGetValue(false); - analyzer.setLabelForCell(cell, makeString("CommonJS Module: "_s, StringView(label))); - } else { - analyzer.setLabelForCell(cell, "CommonJS Module"_s); - } - } else { - analyzer.setLabelForCell(cell, "CommonJS Module"_s); - } + analyzer.setLabelForCell(cell, "Module (CommonJS)"_s); Base::analyzeHeap(cell, analyzer); + auto& vm = cell->vm(); + auto& builtinNames = Bun::builtinNames(vm); + if (auto* id = thisObject->m_id.get()) { + analyzer.analyzePropertyNameEdge(cell, id, vm.propertyNames->id.impl()); + } + + if (thisObject->m_filename) { + JSValue filename = thisObject->m_filename.get(); + if (filename.isCell()) { + analyzer.analyzePropertyNameEdge(cell, filename.asCell(), builtinNames.filenamePublicName().impl()); + } + } + + if (thisObject->m_dirname) { + JSValue dirname = thisObject->m_dirname.get(); + if (dirname.isCell()) { + analyzer.analyzePropertyNameEdge(cell, dirname.asCell(), builtinNames.dirnamePublicName().impl()); + } + } + + if (thisObject->m_paths) { + JSValue paths = thisObject->m_paths.get(); + if (paths.isCell()) { + analyzer.analyzePropertyNameEdge(cell, paths.asCell(), builtinNames.pathsPublicName().impl()); + } + } + + if (thisObject->m_overridenParent) { + JSValue overridenParent = thisObject->m_overridenParent.get(); + if (overridenParent.isCell()) { + const Identifier overridenParentIdentifier = Identifier::fromString(vm, "parent"_s); + analyzer.analyzePropertyNameEdge(cell, overridenParent.asCell(), overridenParentIdentifier.impl()); + } + } } const JSC::ClassInfo JSCommonJSModule::s_info = { "Module"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCommonJSModule) }; diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index b333caf88b..c3333635a4 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1176,7 +1176,23 @@ JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject return rows.map(a => a.trim()).join("\n"); } +function allCachedValues(obj: ClassDefinition) { + let values = (obj.values ?? []).slice().map(name => [name, `m_${name}`]); + for (const name in obj.proto) { + let cacheName = obj.proto[name].cache; + if (cacheName === true) { + cacheName = "m_" + name; + } else if (cacheName) { + cacheName = `m_${cacheName}`; + } + if (cacheName) { + values.push([name, cacheName]); + } + } + + return values; +} var extraIncludes = []; function generateClassHeader(typeName, obj: ClassDefinition) { var { klass, proto, JSType = "ObjectType", values = [], callbacks = {}, zigOnly = false } = obj; @@ -1364,7 +1380,8 @@ function generateClassImpl(typeName, obj: ClassDefinition) { .join("\n"); for (const name in callbacks) { - DEFINE_VISIT_CHILDREN_LIST += "\n" + ` visitor.append(thisObject->m_callback_${name});`; + // Use appendHidden so it doesn't show up in the heap snapshot twice. + DEFINE_VISIT_CHILDREN_LIST += "\n" + ` visitor.appendHidden(thisObject->m_callback_${name});`; } const values = (obj.values || []) @@ -1578,6 +1595,19 @@ void ${name}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) } Base::analyzeHeap(cell, analyzer); + ${allCachedValues(obj).length > 0 ? `auto& vm = thisObject->vm();` : ""} + + ${allCachedValues(obj) + .map( + ([name, cacheName]) => ` +if (JSValue ${cacheName}Value = thisObject->${cacheName}.get()) { + if (${cacheName}Value.isCell()) { + const Identifier& id = Identifier::fromString(vm, "${name}"_s); + analyzer.analyzePropertyNameEdge(cell, ${cacheName}Value.asCell(), id.impl()); + } +}`, + ) + .join("\n ")} } ${ diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 4271cd1212..afd9b36bdc 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -736,24 +736,43 @@ extern "C" void ${name}__setDestroyCallback(EncodedJSValue encodedValue, uintptr void ${className}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { + Base::analyzeHeap(cell, analyzer); auto* thisObject = jsCast<${className}*>(cell); if (void* wrapped = thisObject->wrapped()) { analyzer.setWrappedObjectForCell(cell, wrapped); // if (thisObject->scriptExecutionContext()) // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); } - Base::analyzeHeap(cell, analyzer); + } void ${controller}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { + Base::analyzeHeap(cell, analyzer); auto* thisObject = jsCast<${controller}*>(cell); if (void* wrapped = thisObject->wrapped()) { analyzer.setWrappedObjectForCell(cell, wrapped); // if (thisObject->scriptExecutionContext()) // analyzer.setLabelForCell(cell, makeString("url ", thisObject->scriptExecutionContext()->url().string())); } - Base::analyzeHeap(cell, analyzer); + + auto& vm = cell->vm(); + + if (thisObject->m_onPull) { + JSValue onPull = thisObject->m_onPull.get(); + if (onPull.isCell()) { + const Identifier& id = Identifier::fromString(vm, "onPull"_s); + analyzer.analyzePropertyNameEdge(cell, onPull.asCell(), id.impl()); + } + } + + if (thisObject->m_onClose) { + JSValue onClose = thisObject->m_onClose.get(); + if (onClose.isCell()) { + const Identifier& id = Identifier::fromString(vm, "onClose"_s); + analyzer.analyzePropertyNameEdge(cell, onClose.asCell(), id.impl()); + } + } } @@ -763,8 +782,11 @@ void ${controller}::visitChildrenImpl(JSCell* cell, Visitor& visitor) ${controller}* thisObject = jsCast<${controller}*>(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_onPull); - visitor.append(thisObject->m_onClose); + + // Avoid duplicating in the heap snapshot + visitor.appendHidden(thisObject->m_onPull); + visitor.appendHidden(thisObject->m_onClose); + void* ptr = thisObject->m_sinkPtr; if (ptr) visitor.addOpaqueRoot(ptr); diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index ada998d747..e9f158dc04 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -28,8 +28,21 @@ class GCProfiler { function cachedDataVersionTag() { notimpl("cachedDataVersionTag"); } +var HeapSnapshotReadable_; function getHeapSnapshot() { - notimpl("getHeapSnapshot"); + if (!HeapSnapshotReadable_) { + const Readable = require("node:stream").Readable; + class HeapSnapshotReadable extends Readable { + constructor() { + super(); + this.push(Bun.generateHeapSnapshot("v8")); + this.push(null); + } + } + HeapSnapshotReadable_ = HeapSnapshotReadable; + } + + return new HeapSnapshotReadable_(); } let totalmem_ = -1; diff --git a/test/bun.lockb b/test/bun.lockb index 5d98fa37535db41d2de17bc87f16664cb7ab71cd..7810462ead91fa59e07e7f148e01ddf1cff96bc5 100755 GIT binary patch delta 76084 zcmeEvd0bTG-}X5JBXdMtGet#3Go?b)tQkcZcR|H{14SJXVUSG*7fKyWO-;?^WVfhf zWQ$f-wunnwR+eN|wu(kYS!rc^DV6nJ*SYUOe!s`3x8L)5-uExh$KkrK>;B&R_g>DK z#kY^PxbtL-wVk7~U)lLYtE(UGdok|(&?oJGWSqPG?9eA}|GM7hzdndtdiddOw?0-! z<28BR_%VBGyhV%rWqhdfQ)Q}a>9U9fy`uHc2>7cWPMU`pJ$#`c4uTl`?+b;@;s?CwK8N9 z0bT-Z0sH}pn*cuqHU%z+G4p!`f361BuVf#21atvaD_6R;KZ-N1Ih9N4qfp48m@98@_8oE0fZ%lD<_qf8XiSevO8 zozrE_Zim1YeHS8oBn+H2y$5|24h*?U7W_S$#fN>2hLn>0x~`k ziOC0|S?uRiD23xz1)bO(I`eA`YzQpyEvLYzX*Zhdf|^C0kCfRxgRx|LW_oj`N9WGa9t3A~y}sOOnQ5s7ZO!9B%{*(a zkOYNiB%>`buCBXha$sc^sTwxdX^XZoNtJ ztlZgojK8M4YCPguz&qwHdo%C$nC05^1>T%OUtXb>n&I)HaP2L#Yn^71hh5ToH?RRx?S?h$nh%s+ z?5)$q|9Om5b38e@-szf_=9@O#n>Afaj+0&5A4pH`tgyL4o5FLkvdb%gSHbSE!d(hW z6s}M>TcJl{-=ah%^aRq6Ix4(Mp|0?|UUJMT6&_W%*Gvj&)93mDva3>ab7oA-ou{RG zQrQ#QR5X@dJ6<8XDla!T%U3Wx8s4LIz>3ZIcg?*a9rbU_s*q;>tK(%!X$i6mvT{>r z&hTcX4F_jeG#M(r&jI9c>;uv{dEj7O| zuOQRcN!hjb%GwlUH{X+oWknkToqatkTNbw%*|YIpUv&Dd-aM@koYl)z#pZct<}&?N zO7|6D9nGGn&4y~F1F{pRb?1hmX;Z+FzFScn1Pnyj(LOrmS9x;AOq?bAs{+V%KG42w zv#?$p|4_ILQ?1IEy%km)WNRd1p*A2hKU5?O_4o>NQfGP#8kv#x+eB95KufDKG=)~< z;rHwVW3kNbS<_R$nSZ5XKQ7FN7irRua)>Z#x?`*m=3#EU?V$h1Q2)38rM zJo!g=$WrI#=1-^dWqWh5@%ajdtN7E)WW4fso7UZ|uJ88Oxl{JX)mY3~+1zQlX{^S_ z(3$N6Ko%c3CeF^8?oIV-+O*leX}RczzgEg>)kAvP`{tx&6(EPUt7W>t?jJbpHiXXc z4F;k)wxUz!x(3bq{<=!`CYXI1w($q1#^k$Y-2>}>p(iT~J*oLO$lL=5{7U4? zN^ZYL;(TB$@QsS^6mkelfV7*-@xt{k(y^AOHp*JQ4FnG@x~*6aMqaV`Rm0Zd|9)i8 z@%pT>xnM>%YUYpLB6FRyS=M$c@M`2*yh-LdutbjNdLX+rD>o|*o~b>8c=GAq88cX$ zfW6O?jenZv%kbpqLI3GK**6~n+4aYO?9(qxt$h7O1->lrG#EaMh~|js2y6@73~T|k z1KRn5n`(ZoC<*~CX{~|u z(bU|mT&!c-`0dgsw*k2(`~qaW&x6eySMCnk|EU?dGriNXil=#|0eSgeU$#fndhe1$n3wA-NXOA*FVck~eW#~T!6paR4bH><0na-#sL@M+VW}oC24Yi40h8a2_DF6qVtK-DcgZI@^Nxn?0`@=pVaJ zW>i0_Tjtp7tPry0Ahk=cO&j4AV5AoE;0$Gv30e=v9eel3~9mppg`)f zrtBU>fsMcysRE`b90KI1yMVNB4`jg_kmY=XbXRe|dKCd0>{c21fNWuIc6z!OhXMTI z(NRl*&LNt7USgmJ@^#r$T`owx7RdP%c=mHq>4U0eMFWq3yf`bGueYe@AMu+ka1f9Q zdm}>@)cAL~lRLqwp9ZIX-5-)q%lD-6v|9>J{YxN+u*XFipPiPUo`$ijKsx#Y@2grZ67kaX@I5P|Q zpclA4RO%;ybeqLMc9th|4$ZW{b`*G;(jrXSzp3)`rsw45BcmC<=(KqS`8H2qGcux? ze?Ow>zQsVEru@Jbz;s}9AZlkhMOY)+qw%;l<24BMfQ~?R zReC$wk^@M<`L-R%g0=ve@r4N8nm(T^3_J;Yw53_M&9(kH9b~c$h5fIU2W$)64CF{J0CFUgf%Je_AU&f!kRxqZ@sA>3 zuDH{>upu}A48Kv<$gQx8!d5`8JE1_1#BWGI+zw<1e+uLXz6qq=0U$FT1!N2FMM2!N zGH;S28z}fL=!_rJ9r^eXh`Cv|uoaLkxrhj2C6F!L59FlFRW;9aNf#Uq27h_S+G~Hk`16VWi4RRa%gEXxdqV1+?64)?1nt@or-@PQcx0XL<%Xjk~~lf`ynKk5hlgEKYD6t?Ou7 zUn?yv^h`5+uv>q~Od9MqP9r83F`-PWN17GTl0sy;!KMc#C7DS>-1@s_G5+3SRt$05 zfMSGJlKkj#FQCfg=QfXZ-w^Ogmg@}QI<9up~(#CyUg$rZoQ$IgungF zV*EW}R^ac=X81_AzS>M0={CwSbJ?M~SrzZJg)}iwj*Qd$n&G3|da0R&zwON8QEvNe z>^-PPe2o6Q89v&rw>FdTce+`Ozt5W$ququBn+7w&z>Rhq1HpQO1(~G@PW=J1c#K;= zZB~qN8zZo^bVp3ESvA;cUjpVbONPW4&mlAdx@PJ1umVmpD@VuZ8D{u6xBj@9gug$V z#pB$@1niUjVd>zg8(Y9!R(=>;<6SV8YqLiBDzkXJ8>3Z$znjeP32x(CEZH%L*Ui#+ zrygY%PjKrq%nJN{$_!6(8^5>Iv|E^7lqTygvlvnyc7=hEg3PL)T@FQ%KEY}H0mhoh zI`_u5(AO+Uh%s(Qh_$e@hW7WsdQrDGY7IZMEQTN?-JJci%&eH`)|;E*licS3{P?E8_gvAg_ozeje3#NqiyUhV_*&QfE9tUhh^VCV-`<#8+tq0 zOXwHOhaO-z%KBlfykM*gd>e6x!1`LL!E8b8&688&j2_te*as+Xg46JWvCD(32~uVj zPqiwFzrD?HkK6J1H8|)YUd*~B*qXLF2EyakN0=2Jx3QsvtR5!qXs7*su)bzxb}z@Z zszstS0y7&-`cKtpr{e`M`qrg&pl-)N4|CK9gK^Y?&B#QwzlI^L@j4jOqIIyW+o{G& z!Q#QB@6fUc%o=-ZY2M+0_R^tF;~*Gw#9SzGIY4ArvgVz;YT5uq z>1O0;r|vV8vfTDZq4hB%X2#g75$b1_jEgaPq3P@yyETlrg7pAvL_g7=HjA^}#xKy= zKf38T$H`{bcA9U(s!E0Id1(&vm(cBbj2i~CtHqY_`qc5bm7mzhFWo~VBF2p zC9ywX{^WzP^3uJZHk0z0V{x9_=ma@l7LS>d2gVl5wcrsjrot4#Kz^d)uxP-4+dHLY z&4e&gpwQa#O2@rmtXoaI{X;0SZQ*bnC);L>KuDJ0fpgpe#yM=QqsA#P<^YB@F&zCy zOVkW(I2fW47m3y_1EVDteAM$W7@Z0Xd4{4n>@llFdZHOV+ikxGn$@?)afI0U$OpDT zSVrMoY%f;Qo@V$Qw=oAAOTwI)?Zld0JjZSS5n5mRj?ov3j9izha-8}iGik2dJ_}VD zZl=tQvA=*&q7@2r2SUj;p@(ZiKO;26vU6jCC0L>5HKF4*p%yq~B$y@hdj(qo7V%h3 z=$D#M9NH^0S%%OcE4w#pLQVSxv`ICgZ3qpp?9SALqOl7|Ek6*_UopcMU_$}tFrkB| zI*sm_2AE4=k#n5JG%)2rI8XaG2+-+Pls%F7P z6X$`!7dW>tdtL*R^Plrh8(pjUh6{|tg}I#LG%~?%07EB^a~k)6!Rv~`%&H|$qXLY> z6=r%yIQ6Dx(w*F7i}AOoS#hV^SUy%Z*P82et(9)Qg;~7PZA=+gGt=RErDpglxA7@7 zti|v%PoC4>WIP7QOu=3;8X-AwJZr22V@u`QQm*3UY7#P`#-Xvb_W`@b8pgQ@u|=9$ zI>xE*Fq7_b8>gVjc3}m!g(R6L?~2o%X80OxEJ=YPJab$Q5W2W@n+h=LHgG&gc(NR* zngKKtp{V$Hr?DE0HAh`DTn-Sd9UxUBowm9Y%@*t8j42bPy|s!NTftmbS*WM+0T^qI zePEhPpJcXJA7>1~{9=x13C@2jz@owGn4V0h{Y5bJ*vc5AOG;qFtHSP50LG~US6_sY z2a|Q;`sGVCoo!RY#z`k)#LrwJCB z6|Uf^ylaRU9AgwBL}$U=#!b+xX81ig7J6jynl)cz!043dAuQ>$%nDfSg2q&6Fr4um z7-xniJYiUGrUeRuy)jZTbmd)6`y#MmW(nrpn+S1849oFvOO?$Jwzj6dVBCJ>G3hTb zPAjmL*tVw2IIMK&nhjvwdNC=m5>zT?#o0Tg!O}Wb&qRppE2iO4r>!{6JUKWHcMTPr z+(y3{a&eRk&Q>t?C^`ct^mAZCtp%b-y1e6*^J@==@O?sEQ8Amx>6r&7H@GI z>%gVAR4sHG1^kxhPWA|7mJI7Wq4%Ve{e-DcCeU#EPD=?T?5{)rn^a-TJOaSRCi!5>) z@2DtL06SDrwj5zu9Vb{%SfZa1w*X8Q;Q8I<0Ex3Q!Mf!59jvA{;;m%e2+5I-bE$sW zP&n9dw!zkc?tZuNnX(EpBNsaLPGYVti?`L4yL$yAkZk$i_XuHSqgSI_2Ik93!2dACw}7e6$8FdrX81#H zdvhOpz^t4SV;_sqE!Gma5+U{jdIgO=3`SqPv~RX9kP9ttezDYNfN_?{+mS6`oI&u( zg-*w5F!o_hx9HcHN!#7}6tftAO|xRV+i=ana{wzneSHn8QqXyB zU_ZkQbj$-|2g`b`tu`DGIM7In!-6(X7A+@xZ!q>UdOp)>ECiF4j9i0_7K}?Js*H)f zqEPw{o~1nCH1>kA0XU7^3g9%KM4+V7O7BOH6heuY$4vu;$+3aumsIkT+&cA29lkb<{U*1LGLM8wWZaZ-TMz zH3b??76odFwDuujea)1SF~(|y=u_4K!#D)i0}NaB2&eHI7&mTA2@K`+w+GgA4tg3G z{Y5Tl&x7&&WBIAQ$zoL6EWs8(oFV9NlxL z<0t4bRtZ&)I~|>u$zG}HCqA@+)xFSFw_|Rf15>kfh|95Du1ILoJx)Evtk~=35qqE8 zcu&Q^p>UoFU4i>66cl1c?sM`u35x<~tQ3~LC!LOmz}UP?Reawp-tRV=+$mjAZe?S^ z(v2B&`h)efkGoUk4%Wo|R^c z=i=-~VSz>It{5Y9Rlv2moQ(%#&N%pjnP7AhFgV`ztEC&l)poe>xNp;eIQyf}Mwtm>66LhK3nH4f%Kg2@H3(;Dfx*p5+d7MQFBRtVen zHRj3Z6AJUBRIg0aDMc%x`M4#wmT&Tw1(b!Ll0aYoEKDPsIE z?dO8AWwHm?mKzQTw?NQE)flW1V6wY;a%i+(=5BSaF$8QdEqSkEYyji5ZD4iLYhb;> ztOvtJYf~19XECFlwq%pfQ|$LZ!~qI7BVQoohB0n>aP!dZZh3yK@e0GM6!;YnEQ(?8 zMh_hVqjzA!z~@3X)Qk+y$pgT8nw9%vjO7TiQE2lEE(c{03?Ck-fUt|Oc9-Ec@*X*M z=zWayo*KsUr6%N=1?&3bE(Zt)xsF+Nr_*>IjI&lAr9(H$I5^8tr+qY7v>CB1#@L7u z%e6AMeY4TrbR^EWrC5sCc8)lW7v5&1Yq3uM8nS3nH zsC%yzkr~=C5R85%H_bUI_#f9ky&HX6r+FRBXUq+iFtSUQ5*_o zI51B< z?*bbau(XFfX+4yB7SH?;qPt_t!u{5Q(L>>4IG4Q+M&}O_kvCuo+$CFz>C9(-VC-g0 zjB!>JI|yEjE;)=SE@OB~g#L|rN}jbaA&_hZ7z;o{X$i)a6JzCdIvPAJeZOX&I^0lL z=}X(%1}K~ZGPiOtR$iVN{sQBmAr5)>cqUL1?<;46-H15cBVs{&5Uj6d$y$5~#-&r9 zpQ4|Y=NW6owl4r1XD!a}C>|boBe=dvSZ%|>x?)or`V_(>^D!2jx!SX%LZYT zM>&n9V62@?zF);zo2l`WVsc!re@=FbRTsw;Fc;F*jIwb*6pjsg9Ha4eZEK^>fxt~K ztNbk32&+tXxBXyX-{rP69E`mwJKt0c&4D9-UK7WSGwOL+1MI?x^MKJ8P&<^c4eTZ` zdG38*Sz2p}QU3+$Usy%q*IqFCF3uEFoVKT5Fk76(76p;py}U7tIV4>WPKSaPDTYl4 z_sa*s*l2mvcuvLPfgl(sk=Nl!{IYcTOGDs+5{Gy>n2#uivjA4HGm6Q_B-b9UJpq%! zF8R=AFis>`j&?af`qpF_^NK9Mx+gOV!MY+2j)>FLzDo>CP={9oPtiOdI~@zb#=z3@ zZ1L7Dy5WGrHXByw)H_l$=P|ES!B{3Leh9N%#o=vS_|@QYX^HUyTLQ*`k`F%K24nLv zQ0Uc$uLbr8K4TsO#yD##80*2&^Em7b`Ka1@3_x;4I#P} zPCoF6I&a8nj9#DUa=aNRl+H2?j193SrEw1!tA~!u#1lj?PBRoW&S`7$mYMu(oU!&T zITCUNPlK`lV1fsU*BpnVn<*tR##Dsp0ys5?V9Nkwd*F8UbeDeIJb6CO=>4{A2pW%O z-KrQGg064%j*LTRpLd0TF!iNX?QRuq^|$Sfcg*DKIHUf%0UxZo*X01|k2JX1!q_bV zlhcuV`^)c|$-l)p{({(t+17mKqxXCAX(~>em~TtLmEesUS1r zPq#fE$>jc3_ar_oT*Gb!%c)^!YBT$|Q;0JwFZObLsA(B6!VZn&;(D-Kz~qs5Y$d)| z1%{;zbvgpp4@~y@H6Lkwg8-ez7S9D^Tkz==r&s{2!;KC(Zu0- zF&Kv*(_^zxKvC>+9~i4Ccbw1)?Na z$Eg~@I5TBYjD4LIs+@`k&WuK)`qolX@AJTvV}JDmV_aQPItmj2j8j-v>uE4Vp2)}cHQb0H zBMl+Cbwlf3?-j7gVA5N!`9{uF^eR@ad@y!`wK^D2fJsN>o%xSoeG!L;8sEG0b0WE( z9_R3#Q&T-KC+)kaSQYpVAw0H2Rc`(kO%f3XDvJ>N#aamL+rYY-m78Mh?;?adT~xOI zchW*0<9dV1&X|wTEoMnkjQuHuEL+<*--+Y~=$5wM2O6D$5FRn1%*PPIlY8WN0UENuKgSc^{)vnM+gtSP`9@cl7rOZ7gbC941{EFJ%*69`xPN+7x!ym zxPuXrt$*%Uk=#^|v;FX^*aWn9Iv*JDISBPaJ+Ybm;&Onn%cU!x29u)*r?>aIV12?O zC&unaC`P0-!w^w}eFW#cy>2x;!Ys*&u}!EJC!3*5ilOvCd}HgW$=hI@_n0Wp;$-)m z%-z~M?UTUd8--gClGBNE{(Uf3&#I9l>UVXo5O5*G4~5q7P&}9U7L3;LJJk55KiGVJ zEG!$L?v@o7zemAnB@bf1fb|DMt*~YGy(sgOE;SzvdoJGhdJ>^H%c=_N%eP?CO_*Qj zKV^P6GvaNaOfa`uxd$IcLx}B#n=EtMzXQV@!~5GE{*qe}mg#*?`y4RqtqJ2WLIb5e z9+QP?y4;3>25Y*Q*jkS@7D3{YhoeJ=D;NatZ!e4qYM|?)WgEng&~^3RD^?5pA~2cp zA%xsA<88PZw&~U)fa+rBciBX8TX;k+)L|l}EtbOr2o9A&Bd8`g_ENC)Qt%vtgQcxA zSl8lZ&@Y1>@DT8l*uY*BKmJm1s}-z_)O+c6(W0I1*6WC5KyV#h8>s4YDHs$|qb6Jm zu0aqB9P9qhrC>8hjXJR=XlzB0LnrTW-%|{0%wVT2)DWAl#Vi?Yh?78L6TUe>*RYmL z+ed~-?ugiqb;Ty2@mXD6?gG}Mnzo^$MJK2uLv=NUs{X*!ATYX`Tu)AeaU+u3;5GHo z78zFpw*l*7rr<4%VuUbj<6~^^)DtJ8kmoh{9t3l-9#`3?l37l<1tGdM+5+c)3yhO3 z)H)U!^=rz4TPJ{V;jbgA`eRoDyAce2gxls_U_7SC18Fr_Dj4>kbeE%nZXFt|J3iYJ z4aBA!;3`@}bfHM;g3-k{Dkh7{8_@B~5u6}{pIE_&XvE_iA>%}4XN)Rk*br_W)Ru zm9BJb54cHsO&-&m>((VR_6GD%YiRrqy(aDqJ=ow z4Sn@1)V`v!n;v6xwG_!Wai-x*7`-gXVZ-+_#vx73>DN913OWGx#`_3zwxeXc72X!# zkmzkzK91uzLzd3t?|oncz~EuHu^NmoZqOs-VQL{5_bHsz-~+FL4YJ~@uzh#HS35>n zaXg{V21~RUR-!Q^yo7q;haAFpG*j6M1jiHg!i>3Fh2cw_C zcDB>@UZiN@LW`TU!%$m~Llz-43Gp=zHY%a8{;2V@PJ6rda5$0D6wRNFpsa-F3zq|g z8x8L|@pP^>v&UQm|Fzy=U5pTuXd;qc4t6tG9pSl2Mn5X z>krGC1F&7Y7K(N6H5MT`D7+`y3g$8+@TtQO5Mp8I2E37Xbw^#^6LD!U#(;79VoGgx z=^aIKtiI6pQ%7+UX!Pq8XeJ+8tpekAW~~>tS2~H4ap-G3O0;n63+;oWbghqwaO<}j zD-mR#Xw5>W?bRre+#4O;{d!%SCQ^E1qTPdFvJ8HY-~<_rzd_fg%HVbcC(B@+XpD*s zPD5~p3?4yH<&i6VOf^jNXlg4)VCUJ5gy18C=(P9vK(TlINU^yCuUYvw)y=2PZ zD+tbz!5eO=39d&lL#n?am}LcRGo0dNJaSy@5-k$YLm#+wZMdu>>VIvInu!RO4aQql zOn_>d;;r?!9ufV><)mDJX6@?EWj)LJxg-@f{W92?44_;0%TH#*v zF?;|SYzUZjM`YU_D_RUgCm3;Z4&$x7h1l=F@Yz%}?O}viPYfVdn={G^&$KosyPBhyAw>SyK_Lp0<7=c{-_m-*gFrdig0HN;GsTFSYPJzj8;sV;bpKJi;F&>~S1>-7#5kb0g zFu4`;&9sY(Vd2HIub%zoq((YC6f6Ma0R+ddBB$|44U2@Ie+|YtfCB;+^QZxm;qe3V zoCC({bN-*uoIT+Be!b3T|X!Sgge>M1k9Lb2H87$jrxm}0Xl7zF2x z(J{8y28rZx=n8whjK(ee1gE_}7~Z$R1E;kJsUtaj;5D#hQ8^rI^fd``6_tkt5LI~` zUmpVNDL1lD5ac-+EuQSObs8)-O+aB@e9wrPS{`lR26m%-29Ju&8-u{%W;l;qw|w~fpCQxZ7@-j-;Kh7! zP_Z!Cx*Y*QK(paB2`^r?u|9YvUd{2k9WP$QIwB%PZ{ufvcT&T3Ib^o0@M8IE@Zv>u z;3e>4d@)|Zc$MH~$LoH)coC^Tgcr+s7%yJLI(Y5G_hT)CT_DWpX}ow{4jKO}Uc@rI zc-2N$aG#}0WP$tfB0i^dBJG~Xi~0+EoWUBttc1frUjGDx#Dd9sGe6DC@nU|jk>Db- zp0AVOB9gyBf{RH0774EZ1exw_E0u+QtKfH(KxBJ9z>5t%i5IWGLstBhWhIdXR4QH@ zncpW$zl2bO#Of(}Gx69I-C^~{=gNr4%)h{kiSf+Ix@sfizru@%=SkL88(A4V{;}e} zmC+iJ_TS;fcsy{iE}&>XRc{o|$e)#18x4^%Rc~fR|EZ#DV~|K(%yvxGJ6Y`rWcIB0jM1`yfM71)rhYsG(n9gyO2*mD)$trLiAIN5&q!Uy4L>c@YGQnwOcRAEW_AfEI4 zh%DBrIFZ2~HZ}JVuoCnH(lCy(3OQc=6u%rYJ|6LPMDz@}0m~f;N^ctjWXf^;^WPw& zCaCzoLz*Px58Yt8vcC+<`E{8HB$El5b_V{if?gmioT)Or9MbFam7P!7T@E=gx7tuX z{jdmzw7p#=Aktuk;zSmFr{aHytmtak(R7VUw^rdginxdzxD9?KYydLhM#XC*3oceV zk#<{v%-}wy6KPkf_~npM+wh0!9tP4AIy{B|BOX`6(?DkQ3`K=D5xY|F5=nWFk}rp$ zqA~-!$u}9gqvlUTYk*%?sff(wFt9Q31EmwGp8&FUA1a;5;75x853rrpT~)}yKt^8y zS1##?fV6xdlZNW&38x(NSg0$xPgjZ^$`$oL6}r^!V8VLp?T{pFA=)HJ^` zmvG77<|?~c%8p1En4@@YWPbCY(}l*e0u&Yj*#j$80wOE0 zO7Ysrgr(OwpMRu?cG~cbZumD_d;h>sfzzQWHs(rc0`^D_mGPc6k{F1 z_gVB9D4vkv=1*A@$qy(_BtNJ)k^FgbaSq8Xe|$kj6B&I-;fqQqvJo#Ud_~2-s^Wl`5aZ8NH;&BcrBFkp_Tx%%NqX2IxBoq6<8Ztu~SMv#reRCLVe&_6+xuoSBevv z;WrAuRr=o{kNnlJV?Mtr``XC-{-~!S{s0={KrTjvUKI)-XS#ay^?!)84^#G+Lwb8N zm2Y!pN94S0t@Jj*SfQ9vTV+6GMcXNUIi!7i6;I@0u#4hE#^0#0E09q)D}Dz~V zg~@>qnh1dfr6|MN$OMz2vuCF(yBW%k$oO=ndzDV4eJ+qbk*{#Jiob-)Wht-%bCtp6 zkf%sL;#pvk!bQsdKS7>yOqKrbK!zJsMzxVG*sOFSD_ElV<&g1PRXmZa#A87Az!OUU zcZ{aMgu8%j*;5LiRtajOE-r4+-D0Q@4{o@O?^ibe6XXOsjPxw~RfXj$KO*&`iW6D4 z*A*TM#wXJ#yrB$gBNM&_ovnTc$OZqbvLiCxR|>yYI+6M}K&Jnex=1O&`)l(H&{%3e zDx04ehl@!47p4EI@Vttzjm-Ok(uqv>8<5kpDJCR&Gfqr}v~R9B&@Y!22<){^DuT#H zUJvAGbW{9hATziH$c$Y;UPSVqivJzb#En19zpovWgc^?V?U@F_kA zh=1B`_`@nK0{%Bh%fh^m{o73~$39CU{Uq5NY_3!iN=ZSMjxxb~{x3BPyQA{2o*KPNfs6KcVy| zt*sQolgfbD2K-IMYa@H$9q3$vJ^|7_zXWoV_zB4P^FYqcFpk4NK>t5A{C{LO`EP5= zmW87w^o3TcMYXX3bouqke?@eHb|ce?WAm^Zvtyi~94Qx&BhXW2cR6Hnu_~TOd$;2M z1UcOjls(XzW3>fD|BJyt=GJjQWjyvDqby(h=LzH!%@h>Co|p=xOZtJ_t(U2SiR5b( zzZ^1NFd;8d3po@U6(^Dx1KHJ^l}_YH-lyLT%8 z<*V-~naF(KQ=G_h-v`piK2iGrBFgoczl=y<`(73BPmmS;QQ7|!r2Wszp2$ghp-w;9 z!fFWY$v>6BKS7=V8)8kwdo5el5xf;B`?(E}9oJT2q{4PU{L`WozYfTb?G9u*r_x(ul8;iH$c`Kr zq9*+~6+xs2P6o0iQx#4FGQkWW3rGj@BC^7nN}ma2dY{7ERQy69^D6@KA~M}#>nO_r z1QxIi$O4utgOxyDe}_!BMx|Q|WI^jyIwJEk6(^G44W#GzixFT3CCY$EgL@SxvZkeq z6IsAE#fh|kKyd`Ej~EOBQW{19S8Zf|Bk@8yKR;S-UH=I(gTMKlftBAs`#goB&oX0)Fk=4&l~XO{hxfk0A`jQ?XP^!0MCqgroZwzgUfy100-Lt@#hQ@{?|Td zP*zc@PrBb<{PXDi_g)*VNP-mtC0ZURb9~_}JIG+ICdLwQo1->94+Ce)RPy zee(V5_y2tBjpA5;TXX-me(xU|vHoG-Glx1aeeUFMhrjRJI<#Q(#N1xHh8f51zp9(r z-(D56_qp{WKK^F#nHNU>(d&zSv&Rq2eEaUxt0$z^d-t_k)jL(YdfB(lvlpyA+RvYJ zdU#D^rs`&ussEYU$lu9nqR&$juM*_^{sc@$P%uKId(G*RX?M zE%~~`13RY1=ZZ0J^(>7F9eV5hgI6^?-|O)QH$PQwR*G7!S&;w*&=6cPtP_)-)Pg0MXS!bJ*a zMM6A;34%VS^!jCz1w3$QTOY0EHifF$6;MLT%=%#gwYTtjD@gsG=xx5O~Ek^LdqBjVPeM^2zw}mjfK!a zB#niTF&@GJ3XOy@4np$@5VFTXXd?DfI6@&}JcOpgJ08NkBnZbSG!x+yAaqKGuwVj& za8XX-1cm4%2rWfn5`^UwAyiU`5K+kxT$3QIPKMA%R8TlWA$B5!NU>rfgpDZ>&QWMD zdQ7s7*RK)l2pz;(fVeOj(TORD?kI{=pm!2K5TZoFWWaTzlyJSMCfp!KP60%V9fZz8 zp9<(Ak^mxO8ZtUC6&ZCEh6j2#;UU~4_7b{_M$-T{3oqdoafsj);i&+Zm__Iz$_YJ1 zFYgdw7uFjS1p1SEgo_kbhy*``35y`?^g~!Fswp^bhmcYPVYS#% z1Yr+_utgBoh@?djG8RKPKw+IQZimo(354w1A(&z>g(DOq7DLz|yo({sTMFSA1tG$h zKN`7l@NA{9V;R1p%AtT!V@BC6@-ktARM5uOBkymG+zTDdo_fo#a;?WC`8-^ z;aTCm3&Om$5ROqO6X9zhbXo^t!5RpAMLC5N6r$Hc*e?p#LRh{YLM4R*B5EB3mkD9@ zItb5;3JPZ^#IA>MNUT^7VdLEp&QW+t^e`dBZ-7u@LO3kWQn)}N@oosOisHK=Y`+J> zMGEC2VFQE-0>aJ>5RQs!3XY8sQtp9pOzgM^!X64?0>YajNkGUbhH!wwae@D%9{sTi zLiR=o@8BCT5ROoYD2DK!@D@Xuw;94Q3Ll8@O%OV5fv{i`gbGnk;RJ=~%@9tB!p#tt zmq4hbP${CeU~T$HEFyd?DhQv5t|fpfv4Zfas3LqOdfW>*E!Gh}7iS4yh(22ZUy5RY z*j|ckFK$J)XGOw&5GLFYVds4iz82LK9NQqIltMTsc9cTcLm}*b2;YgM`ypgJ0O0_I zAB3?DLh}b9WN(A;lh{k)2!)6TAp9b{4?vjr5QJkC&WrE|A#{2e!h#1OREu&7Cn!We z1mSm4_z;BU+aXj^xG16?hTz%(VfDih{t^`w&QOTm&SOK6xN|#?4Ua%MMa5Sd(q<=2=Tiilsp5WgE&j!0)@n9A#@bQ&qCN<2H_%wD3P!m!h}5# zcJ797y{M+(*b5=03_`TnQ3hcTg|Iykx`?Db5Hj{bI6$GRF!n-dz8^yNUI;gdy%dg6 zh}Z|=X5rljVcv5Pj!|%m@cj@v9e}W4KZG8loWcnT(a%AM5rxk|Sbh*fC52cKbpV3v zc?hcyKyZr+3TG(99)!?GtT+f^;|mbZQRpXnJP#rM5QLKFAq)^_DO{kC_yUAMqWA>} z+h2rmkwStQ*gWtA>~B~iDJi#5cW_AdkMmDk@OOTjKdHPP#7tU zmmxHN1w!`A5Jro=6pm1cI1FK|@E(RR?^OuLD2x~3uR!Q@1j2$>AS8)$3MVK;zY1ZZ zD0~&d@^T236jDUg5eTl=Agn$DVT!1raE3x`IRuYbQ4V3_Q3&TKq>3J|L5P1HLdk0o z(!^N`7bqkig^(_ak3!gf48lbUUXk!RR+darN|-6C30Y#~F+jH1LC6vM8-QGqM934n z2(yIoCLmvU2tKiwP#_w;1(+?oggN36VXg>24wxrq5eh{);Z_m(HsCf{7Q#gem3&k$U{+Uki_Kih`#gh1g8gJ z!Vkju9q^;@5PlMS2|tTQe*k_FUc#^95aGNCzX-S>W)Z4IIY6BF3l)g|6BYPf6#i)& zzhQY0+Dm>>MEwN=mkwd|Ul9Hh6%@`u*bp0p!$9zcJA-f*uz{Zg4+<7NbqMi6NKv8( zSw{gqXgq#{zK;zMB8mwP@dLpS2|<9mqLdISstNVP$Y4O2*g>c-bUUDdNCJqA5M+D6 zj%*tVqYi}T4hY$GAT$wsDIB2?5dxv9@P8+?LX0T93c~WMAyiU`6;VwgxSBy&-4udbR8TlWA@*treZ-2Z zA#7|8;T(m2qDM0b@!=3knn4&K&QiEQA+b4xL87=hgzYULT%?d762c)&XbE9wID{dh znu4Phgp?K#62*=d5cW_AYYAbvNNNcoBLcz!3L}Nl3PSVN5VBiA7%lcvI6@&J0>W6~ zjesz(4TNJ9#*6UQ5IVJmu%I=BBvB4QPZp7F024(aVUjpbNGXeI8}ykT*%@zG;CESy zhU1YvK4wwG_b=wAP0RD7&eVqBU84GBo>4)5-5fD6SUlN2$YBq`FG?C>QMaI$MnPJ> zFD)Ox;80f8J}5ot>N42ldz?k7S>Dc>KKa2%Tbr`U9fR)C?KfY8g2ZuOklz?}t)^WC zyBTGRqJpOAVI!}XWlsb)6n%OJeQXu?*7ZRSJSbX}8Pvt%^==4qG;(ygR8Jkh#N0%9 zItG0y?u!bVMRPrgI_L>K%nN(`a#qobo_N<-{LnwBiYyyQ zmG|q=uI}E;`pp}w57KxiZhqK~w7Jt;-I@1l<;_&o7HbbAuw22FYgKeaEFL z%udhp&M3_EeF?3eRdu*Yc7KdUtxjn>AgF+?pDxzM>cM3j2L#pAMJqkK%tjhC~!|t&R!16+c&tpUx=S01Rhl zx5*Z`l7rsW_2Fe+!P%MQ4ERS*Mm(MgOb5-_q;On3w0FtM}_wc2|DT zEqdLhy>)BQ@9Hhy@CDsz55>=WH(^F0y44DiF+1o!y9a7$P0`!Sw#*G$cz`wiMIbn> zZ2rx`)mPWuK9X+;;p^fa;U5~@Yrvm>$%|UK`ZzJ)uH_Zr91VVBnpfbrrTF$ff2)Dl zQK2UWH;H{+CFe)9n^_U~61dXpfb*wIc)g)Ce$+eCQt*=yVkEP8OGWY5=b}~Aai#HN zSvM)|ZKd%yo^DavJE9a>_}^7gtXyvu^`6rB6H|SZ_C7S$3hu_=+_tU{q2Zs_5U+kx z(msZOH%>JEv@PbMb$y~VeqL^%(yFB9xBl&yQCc{(BX!hQqXR#n)dJ!xY$h)Lh!$JIap0Hsd3~+4 zRtUeWH2%mI?IQ3>R@ymetW0aXl9cwH()jxceurTwbxu!3t>Deb({IzW3K{zVtNpweE8@Y57<@%Oz@b-&gTgg?#0>vxs7 z6T;`9F;D*97uy(x7k^fV*F~jWhj5kB{!|+F3hReR+2?;L?FNJoC_4?v{OGicg4H1C zO6&}Ap)#~74I72lKxsir!v>=@Qd+Rmx!ERZx&a!;bueDV%8ozI#^4aVwkpl3 z(hi08u+n-cEs^bSqr?~`@~iwUl@_Zq91iU&rNt?21hg<{To>I+gx?PsULjhu35&X#S7&JLfTNT)mF@4M#L-Ihz<{mq2pprdX$WsnE7w@1r9!() zX@MWbn-0yRw7>_~)1dKJv3T)A?5xZTyy6kQj{P6_#k_Qc`DtdZR`~Ti%P<4sz|X-= zgvJ@+#p_9gxq?krc9|*+(55JDCc;w@<^o1!!C82vBFxJJw0;jd8-!lZYZ{P4l7kng z5wGb=%SD)8&nxije0c~*D~-R@$3kY|6``~YrR788H0R}28vM$yEk={L(q$@Rm5aRJL!S|P&nn^@@oJSE-= zu`djH%~INJ2)~Q&;Hs9dwD|}(P+h??f0UXa4_S z+D4_VL>T|;lcg0yW5=z+i@(N-h0d>SQQ~Td(;@QO3XOT+h1Xao(@ND+wAG zt1@UzvKFuLD(xPnt%LR!UcB}yZ9T#V@ZzGj&(hHUCdfg&c=1aG%;#>rwo}0MoYFQR zeBM&zVS;w|;Kk*YtKUIoClF?ZsXec>jR>>Cyk3CD#uei=g5%F+@K@MyL($S z!71(w5AMYZNzvjKptwtLcPSF!y`R1JbZAf0bKdv*{(RxOS!=KLEca*2+%yF@VgC)J zgWfjfHe){s(n0T-Zn4;Ar-hnGWgK?2-_Yj73L%U1+1Vz~@g;5uA@OCX26dn+L_%Gt4wdE9Toy|i_!cTcB`62wAp*XE3Qz##GmTjwEBHY+ z$PPImC*+4*kQ?%V9C!19FJuNAqyrz2W3PJ@|78Gg$OLIY&b}ETJ$#`NQ((G4a`?z~ zN=#RfuVPv-6ERa^rh#OT8Z>x^`zNqse~;;g`5E&A{0fI5>Nx+!!BN-&yI><6fLJ&N zTVNX;ft_#=_JSO~4?#R^hkdXK2!#EfP z6JR`y1lb!6fnhKfWLq=>Cc<#|5e7j&7%bCQe=GwaEx1EE@PPD?0WyLYctbJ}b%v-f zL^a{UwJTUazK-<~!C{`!)Pkcn zVBHA6z$VxXu@DD~U@?q?@h|}!xR_*KY}PVM2R6vi_RcQiSIyk4$YuBw19@t z2pR*r2i3c^<{K5tMHm!?Vo)4PKuIVJ`5-?mV15;)14;&6ryx26(HV%2U=4^~UIMcs~^;io#+V76o#?m-Db_734VY19F6yBfI-LL%ITJ8&27!F^do9$X0Xx9zOVzkY_v*ZmV!&zi((dpLQojw7hK-K9x8Xg?E12BQG>umG3!D-kk2Kr zh9xiq5}fSmXvVHAvpZ{Zs#2b~}s@3vZHa6{p31IIHbN&r zHo|4092|uMFc0R#Y?uk_AR3xMb7%>zpf%)&T;LCRp&%53!cYKmLJshOEMSMI%>0)L z+#ormfRvC5tRM<#Q8>%51Z{+0vG0Msun?xh444XeU=)mjKF|cdfl5#XI zk&z>6SLgygfS=~&Gjp`R{9ImDkRM#D2GyY^l!9<54P~GlaJxo7nl{IjV`c#;2(rr? z19HZdBd8oNWwS1u^BFJrfYJfSe*5f}9*1gPa!YKwXHEW1<52lI$^t$3&O}lVJ)>hZ!Ir zApaFpJ}h1gvp57p2!uiy1cLk^QVA#tr63&E5=K5eJ_{7c=~6ypA1t3+FOQ`SJfpLf zhYD~E*HBo^^#WJ~uL!#wa~ZsZ!%!c*K<;QgA+5%wF>0H4`N_c>00d zZ^;YeD5cya7!NaG3`~MFAo_aI&vzn`7El#}$v{@f3zzfBQeopeu17K zAMg{+WB?Qg`6W`xbTHHfQ8G4#MzDjLv|`rAtOM0S^Z@eh>ZVW~M5R`Vnvyu8c2ngd zR13smCRIEU+Cn1`x5EgpgjKK_Ho*7L4%$FTCB_|6Yp{mplg~{mBP1f+Oso)vy1gA|78? zzY7_0g=8RA90);BTB1eP94|he6O6I>YzS0|rAh`~Xq#BXki~YLg6m8u^Lvv->YvXZsQ=@^mA0JM4p`5N&$O_{1CW)mC3$jqCd#z!zJUr zNk>dNW@|k+-EnY~#F1P1wL{TD4Tq613VtGjF(4&y)WFyNFL57<|1iiVJ(R!o({x1? z1DyilB-_gTkQ-#{DO*erNC|H6o;amVr&$I3Xq#+1F1EJU&(?FQ-KKf(_nL%t)(L?_dABWM7!*{=hY zKo-Kh;05lG9NgeD@k1K?EW| zX2#|C$*6k`ub>l%`x}taBzDR0C(K0n05Ys)Hg$n7*d?xtAYEh#rtCUh@e?!3wIoLn z9FyT-1(|VEf@CTcW{R(_)0*ySFjI@W+C7*(lT0NZAfo9&f{Lhcj;Q6@6T57^MK%LS zp3`GWRASGKnG1444v?|r4Kf{O!t?`~y0U-`d?7Q~;rx!Sui^i&+jTLIv21n{4QIW7fv31vQ}tRJW+i(OTAW^|9B3hVT!n}N)&O)UtUJ>#`ImipSK{PO;mn{or zAiEq6N@J0OKp`jvqNo*xZE^4fQQ{T>QQpe2rXXa5^dPDqF+D(bw~}ynkZ=xvhr49v z>x_t;w;WR(f?n7q0=bqz*-X16A_s=7-~(r=9XqC+uYAFknvhMT=o_j=dz34Luk5*N3xW&fT@M+PPFzh~SLUnXgPiBJNIV+AM=QW7!0 z4)_h%m7p?w3)0bKu!_A3EF$yOFy~@c#f*enAljQc&=#75+({4}WIbpgp7o(2G=j$9 zxXsWCdkbg@61X+U$yN48qH)WGDd((8nAss4>{EwFX@0z*_+XS~%iS5@%(!;M%!Jti zQ*?J;*xO^zh}lWyAFVa#Ri^AeX*SocYUyY#XNKW;4uhdE1fr9v1EaM{hEzb7F*wOU z{|Pe&=EFP??bRIB@+U20pv)gK`A@)UJT%2V4s!}jhKb;u%2=J$hM%-dji%w8ikL-# zD|@@yn6pgedKLDSumYCDGFS>rU@0pA-lSA()c;bg=ZWDQoCS$_H|7r54w8Z>;N0jvySSG1<~Pip zU-_TGei{zIKGS_TVfMND^A_BPyKo2YnRW>yvJXMz#Qqo>L{VMuu>1jU;SIcoSMU;Ez;k#8a?kO&_^mO}|A^5T~QQ*J~^d{JLVD0d{%fjgvyG$4vy zk&)XPg)rrS5)Ik8mfIWNAc_;wWy!r)Pmntvz96?j7jTDkWkGLqSrTuX)>nRO&CaqcAjKZRwlD1rR&$Yw?) zk>$aj&%DlyDJSy6AUA&G29T(Z!XOyxKoA6isFfVHHix5!u+%%Az2cmrW21LgxIuKF1i@h+W)Lv!mqVtr5 zh1%Ff$yN=dHfv(mfLcHys_F+3QG%yNxGts)rG}XG!J7c}Fh%9u2xK_P-G%S4H-V;* zFpdBC8{I^WoT%lATR%}7?v{yDU37zFt#1{Y+RE4B8t5PFFGu^_DsYBYjV~xg&CrIh zu(&GCm-o_bN~;z+j?AYaRCxOnZ3d3&l*{7UEGW>QfBGVs8lT5)8rQp8YF9;kiuo7u zFG7A*x*}Fz-lX3hfU!ZHR9dj(YJ~EeN$iKz?i5x}^(??@RpU}vGaKIC8llsxbN!gbw!cGw9ZGb`O6|`a+K^$78Fyfi zOX?JA*DTG)HIurCplddjb~YI-qVmqx3i94&o!MHw&`Bwb=$oFu)O~J6*M5W!@(=S5 zVr?^h5=Sqea^ZMSFWo0Za>k47S2S-esHn>tb-jqyTPx10rqa(L^1Z5{7{9CC3p7u+ zAd$YTTHeyUvtqkXjGU07hk94vS^WC0`@|UHugknqQ|D;Gwqz7kntFNLCzC5){Ll!2 zU<860Vd{p&n?rrXu!SJUhAetn;x7kJ_na^YIjK%ET}cJZrM?>>;Dvzusl=C?-cMhT zKq#H2D2HZzq$S>th;0!V-t~nZw7BGRgc?Lp+dKp^BhY)(l+=xUGFfziz(D^{insv* z@{m0Sx#6jpTab(IJ$`Z5S}8wEw?r(A#a`8#kK6&ZL*%X?Ct1l^sA09UZ5xbp%Dqr8 z5C|PjXGA8tPntplC#~Dk-r_2iLRnng>A+H7-bH+Se)y*I7(FX`eN|I)=8=S%(oXd| zSD{JgDCs1O!nn>>CCQ2-zF5_n0@yBarjx2_79RHF3wynFdd|(B7b7K{r@BiC-Dol! zas$WCA74AsZ<|xjtg8Pp`F}~!NXe{vM>hY*3U-A0YZ))7Mhh4dGgQk(TBa;TT-GrN zWmcHbv};GL)UDFgbx=|N5Sm@@%f7B#)#U|RHA~tIs=z`mh_~&VE~M-^>4wr1XLv-H z3vRlWfjzj0f2e;@5tpHA@1@);vBEIhnYug?@YW!=!v0B$9mGmLS3GlA}uPn zPMzbYdY2Gkev>Yl+|#Jp_vzbq{v5&wjHI83_y>l#r1Vn$i!>iwQW8as=zaO@!+=FJ z)hAW%I0I6Tb);Iih@#I_w=irO$(@Wj-$RpAULCQ1wV68eOAZydn4qOp)y2%R!74B{ z6Iy9?V+D>?)Un0HQBAon;p&F6iE&xga2mFeO!rbXhaQBi>AU{gV~cA6nb`R=W(_rK ziI&B-CyO!vwA_<@f5c4RHzXfI{ZROLby8xBSC24kXR{jfdGM6h{ddn>z>q(Li;RvN z%5SOWYky;2#`)~Oa^l42+eE}rk-D<@sk%#PvaYJ!Ce7QMk~!Y0?mqMPbc07nDi6MN z^2<^!yX_4#6umyW%)1^1wz(O&96n*j+BH!9OuN`dAt2p& zXYG!AEuT+MLV$e83~Dyq6!lT$<|8MSw{*t*$JT5u${AUfYG17aR#L+9YLFOSDt;yT z^H*Q~G<;R$DlJ3GaI>m(FJBeCiZfV*I=D*viMQr!t)?_BRA&sE<8|>pKPGI8kNniv z;!1w>39grl5jnkGd~E{*jI1{t`srfAk&td=NqPb!#prsrQKo=jv0>HEb{n3}8POD# zeGL)KQ`c{5nUWQ8S*ct%a2>0<;~ct|aMGxEpI5zEJ%7E9&TuD9pV&1QiVYu_ZJ*QU zy4pcd`!fWj5BpY4+jdy4!LP}aY(A(?meH4Mv>=-&RhANo79Re6YsHs1<@skX&!Wow zOzL?NkS1H#W<|3lEk3Sv3IyUKZI#$8)vvE|wEoJclp0S^+qVd$Lm+)Fzl?R~Cf;`j zZJ>7l%-G#*Chk`Hr?yk9`O-=L^<~gyph~xvcKfqN9;=q?=z8C;rR(V%8`;XNUaR?9 zl6$HHYqdyMvkr}_QJ{aT3Ao7EPc7!t3It`oSa5LYV&NSVf8x&zm=Ve3{eWPLPf17iOfZf7(VSQ z)HKI0wfyuLbb+brm-Qr(bQjR`9c+t50Vq|Lthw*2GwIfZ>oMrF-hNeM12vLM4ckD! zKA}!Z577;M_m)dpH)>O@5j41m%6>-kQXX}zDb&`D)PvDMc3$R0CL5Noqaj;!2WmLlMJCj#r%oOp5 zMwC`L7P!%U>5 zYL0`gF9AIXxX0ybM4kthvd(}%sepn;e&(s4MRbEH+R8gIWja64w@%Uh>V`yqQGFES ziP}Iw$(^ONDiTZXd`jzE`slQoDn6LM_WOg*kjx2YA{Eu3SOPazDpqUnS}^WQthU9~ z77}5!^P;L5RyCg*lfcq|i;R+RW!s5-CH3T2@>aui?J*9gQPv1)AX_UFTZEyp@00HLI{Q?w)ElaYs!-K)Q(Mys{oX z&lj(>xDLlfGQ12^cC6k{mfa34zp%A4=r(-l{?Wr@T^7Ifx{|{g<_tcv8(z1e`;&QZ z-KWq}=8n8ZWuuqu7f>FDIt9s_8Zw(I~8%v`rhz*R5D)y)`D-$9!v! zE@L>T)!VhA_9Pa!Aao#+D&Y_(os25e4o3GHRc#-$-QXPz-mPlZ4o-(g$Lg4LPf5Bz z{J%7<-ahRk^3*Uox}W7{vszD|_OQ6ho{}?y%l}n!s_JiA$-@6vk*QyP)9P8wda-RG zA2J-ypI@@K!5Vj88A$rP%Ew;9chLqJRfAp3U8U4mxq7LZ%0bvm{U%qP)M~EO%UxOr zb6{ppI`^taKC74dazW#Kd{E1*+;dt3)QjEPGc=Sp_7K}7^>Gg=%~shDu=+R}ZmFtO zm=r&$L3`=<>+2eGZ2cQgLw>I0Ki$YKD~-!GwE+RX7ZxwZX%(28P}kKL3H69jvOo>5 zk)m6vVY3=bC^_(>l2cK+tzK4HaE-2EbE{{Jo?zLf>)CtLBJd zWXO?sBhzt~{VW8y70yNC|6C9wXKM5Tt@>XBvvrh`#^pMw6?6T2@wchThcw@eb$GU1 zbQkZ_=Gg32b^$tp2({yomfaMObS>!hev9D>GX)fS7OEb3Y8 zpQ0wOx_8)IlBw0-RNxUBGFF|8C#k;%Gm~MDaqI}Grr+w=~^3OY4fY~&!=tk zeo1=fi6yIgCUN^CAgjdcv}1>ky3rs&7tobGrOlxIeeaeDPHCryI(FyPRQNGkw52JK zf7bf^0mWRIEQwp+WA#uyk1-wU=Q!#Awlws;8!GK_y_0gdGtYSK?GxJ@y`ofxH#vtl zEmXu926AK69BdIukKS2?m8@r9J~XaK@Y+9&u(Xp)Tc&Y2$7X*puEUWfli0P2~WG?J@2edLv6u4nh_WeOBkOICcO8*kou(<^j(jm!AR>6PjF|HD}rZDDjL_3?Mg z-%oj+phFH*l~1q+98gNGs;PLcY>he@JutpxrIKpE(i-|u(PzLT-Lb*#sbWr%MO`T%F`Pa{41TKQX|m|2x}T=oH1BRyWWQqvAA1j|6|Gm7o^D5c zq$1PTYp`rt_Dt7xAAM8XSM4~hRm=FWhp{f->b3ckTl1#FS+P}?Gg@{&KD_&k=36-F z25-{S&~~}^|2aqUMmv6DMOP!vXeG^%DDo~70eiqVXUORR75uvfov$KFgI} zE2ge5Qe8jGHa%@0V`JlSI-g(HTn#di;ZS3}xuBxXQ^&uPtZerRH)xQy$@CJxA%J#H zP6GQ>%X36($dUZjH(Xs->u~&sRZJf~gu1Vuy+ZCipA1DV?f1q8=ELL9-QKR=Eprx1 zL}@`(8YE9~s)}L8Zl?8K%{q^Q$I?&TKaaK`y|P^(uB`oxiDKfchz>7ps~F6KWkix@ zUR47D`G(#Fs;0b}C*n!W9)ZPO>Zy|=_opI;VbDhTT_lDcs`y2RkFOeeQ48U-d)x4_ zmG5sfeB${kmT&4FH`EdOiR!R=CIaUXkhyQshA*i;_CLqR7xg)k+&xsyudoFwb%_PY zv@f|t%yrboOC;7urM-+XNR_)x4*IAja#cr7lq*BrVboDqakM=gXe`Nja@<_9Ygss5 z%&~(Qq|%cTt6ae=#9;)sRbvoImyI2@@Yip;k6vboQ)ZVzYASLz3-u~PrN$2hVnR=@ zLw%~}oM}qeE{iVYqr|sa1zi2t9{dm6Yt{EEi)G4TYUWiAwqw7CI@Z|DDMx303D(Xy!% zJGIOi{~LsB1_fedl#R*K5*VGFfQ(hWn49VLn z_YG>pZH&>gYX5X@ZPtcWQqexL4Dk2&1F*$)4zel9{t`@4XykeO>Ge$Q6$MPVf!OO6Mo#?jC*s&O~8%wb=~ z8r|yq2gfH(n7ZM-lg5neNQI!yrQR|>k(0KT)Dx{$G8unQs30Fgk3?} zgH+os8yI(D`yJ6`=!(@~m&f=>e!gAWZS4g=x8(T9oda6u?Ksu!rWR?@#;Y|qIsST# zH{xjC-ahEw5A)X{DW^d)u4`Rex1+-^CA4H)hfkVS6Te-LJEhm2GEIdfZUs z_Q`+MBm1gDx3tXmC6kQYzwT;XKPKfwsb{&jMLj>9q(0x$d@LU)DWBU+Z#`Aj+w_R6 zDu&&tMVqXmZ)?$P=3MV+zoY9tc?ZwR>Jf%5)l_5cth+AeWFe0Pqtf+)c2<6O+1_nP zW~AEKb?)U#f5dRg(Y2D)S+i-X?p>`N-!?mC8S2`7Ug&RpU&~~r7H7^-?p#||%us6|V1DIa z-7-(jc%=E*e^$m|8$5sP(tz7tes!+Tf;a^HhaUCf^wZ+iV@RM#GAZuS94 z$rSyl^sbn^gSVgtcc_$&ESidS%dFWdIx zkq<+6#Rr#Z@L98sm9}>Chcoa0(oR+gxno8hwwR-`KBiW_pQDOBrorE-&X2WVOX0a{ z0nTA3m}#YRe*1XS;_n_VXLl{jn(Pt_^OTSWKDk@=%~7*wuX#jCbn;heo_b1H`#(uA zM#$Qfc`C;ft(_%nj2iQV!sL%pJD-qR*%)JV?s-?}-MT|h7Ky4TSPxMTA3sv75c~1! z$?Y+{wLjODJ~1lIQ&O9ZgdK^4i$0%da`(;>r^L@Osw@(g_!#B?9LWc!FCi{QbhC|>da4*1?q~V+7k(HQvEQtT+YF#XZ3YT zj9Q>Ph$Czc5^|&PS;MQ--K%WQ=V)tYD2saXb#S*Cv}wOKC~v!4E?Ce6CGCEXgv z-R-ajD~t(vt99WL?eOvq(m6Od(ey3cj7i{R!&N6LzmB=1`{=X8;uY#M&0~49LixO) z0Nq!r$QPQA=N+DfmyuEM?BZT0J8%5X;`-xC_2UbzguTrwWx0al(epK|;Dst=UhXmR)f2vr}R#KHLh5cI$JhO_$@ZEKZ*btCamEP5Xx#w%GFl zHQYT$opefguU0jY2rIVQh~sX#+$r8vsJPE5Q6C=$W^|e4wc7O>eYlO&r~7I(Ut%AJ zgml+*H%|r@N{ml*N~~C|PQBEE>?7A3HMQtb|AKwKdu7af`XpU;gUa@b^#9?WzbMK% zQ)wHGu$gN3|5D-I)j(<)eUjXeaNnr95!Rg8)q+=S{LF3fMV0b3TPb7rZp%az>B->6 z_OWrTx)wyK%Cx0VEWfLsZ0{}kHu52JVlwx|&T~#?H+>;A;!$bdaGW+}%w4;Yp6y~! zl zRc-!2qIjG!74I8;=6K`RC(xb^HY1COQ%C=x(d!{0UHMM+rz=m*zu|&}>|)q4wTheZ zj>L*@Rn^}y2Fh<$9o{h;8@E8MVLTACH7@p@7UpWvw#7YtuUTAewRRXQkge9Gn)e?r z%VUltCgJ5O^+(p*#t2AimeLJ#l&I#T->W7y24VXhs_aKPP#+}Z3_E@6j>uSh%IZX6 z?oEc8KC|*=J-y9ulfM}`JA_F))C9s>7Vc0hK5EgH^E*|6MD_&XYIP!srW|JUytbcK zm&j5)>J0&lQcWcK?Jkv&$hdB}OHHbYwu#Qj-rN5Z#lEvk4g5r$&vvO9pAh+Bw>lyw z{+~@V=(D!Tkw(m3BaQ8m%?7$`tS<*QdYq)8ZBzCyRCLnWU%6M+6p3Gn!-JyV%~|x? z;_bgLCJt#VYR@d7n)`)?v;IC~{j0mI|Jtkr^NC8z?1C}-)TuAzecb-(*{nYH8~Y8x z6hBo+etq#Gxuaw@{^R}A>so_CA003f^v#f=OWmC5&Nwsu5g*wX@5-FpZCmpe38oKQ zmps$AS%WRw!RgPfKDNvUjTA2v}v?=&-`=?tQJ={Tx5~rCbq@uYYXR6dR<_u7mo^5@}#Hc$3@nv zLwv~J8f;HG?tO>VB#YH2v@)s7rsJzeuZlA4Ocgk zvGv z!i6UCa5`>f`_*Z+7@sTLPM0ywwTS z1wnh#QTI8a7JL;|I8mh{53%{RCOIyReMD4pu%d2qol;LlFzA#jm7FLyoia+=spie! zbDen{s!QrmDwq;A_al*kxT;5te4Qz)du_cudNuy%5%fH=DUnUUl$vcfa`Hpln;obr z9rgG8lvGv($YjG7#T2U8vrx#Q+1jEW-;89H^8rJS}?7*NCr#1bi zB;|4GX8QQ9_xyR^#?Ne{q%Voj>O9kQVcT-=F>(KVmIC2G>5QE_{Esc#;Q-Lm~`S0knyl6*MmvH z!8TEFaj#tZw4oiaq`syiQ(J>j;zXrp4(fB=7<%0|FFCU;@}{g(Qf5Z`;p^(S2>y!( zU3Exf9cnp~pdP280nDTE)dZC-Embvkn33i5zWbHWiDKuZB8(N|UV^%oj-LH8L3Kki ztW7_oLIzfQ+-*kk_p<#lwV$67jNA=us;&F6_kcJ(cluT%%?-7kuoh!2^-_=0TC><} zH;lsfEKz56iL7gOI3vuV`=LzQ+XZ>15GnR;ZUT?0l1caa0QxtP_tp?bM98-7Yq z%fvK8I2K@)C!IC3CCzPBKAocu{gdmPJTpW8wb+r41GIh2Ok-L+5$ajD(UsT9<-rIk zcaTe+d#VLT?69Wyj4C`)@odTaou)^LQjo`&=#TC3kv{Z8r|KPlU0L*{)2H`6wN{Sj zQTJ3SPl`IuluY+&#L<@{eP{+p95e5!9!OZ0-c#K&W5(W7MbeY%K|;ucdA4ce(Z8%7 zoy8gA5RDT_4u9|EMes}Ns(|rJhwq$ht1qJa7bjpM>=NLOI}^G zJ!sY3>67!mI!ai3F(f42?1MLZd`#7s=b0Q~tKOfH0fFXb6j!=FI(gQk*Dp?i9{1IP zj1*zWeN`RFuqkFXe$+;-Oz}rM{g#eVaZB)#mY>o#d+&3n^2~C^vE{y6n}t-4n%QV+ z4^G>`t-wL2#P$2?1hHG*+*i>)m@W_01h22ID?CvCC|WFL^VyTGE=PX9ifO*@S0R&b z-v7B-|0Q*^q<=4mdhSWzHZ$+=RHePFneD|M8C|vG;Z1oq&is_>JDW!H0d6Xn($Yq^@{bBSROM67IEbt2cAk$qgM@ zEttDj<6|S%ABKE0J^$2c64s3MfNJW^G}h{|v5eKd7gMk0#ubLr2oYXGd92oW)2vBL z{H?@)HtXM-^UoTYmdP6OAJvqY=^yH@iqC&KI-*w_zw)#vJv#oOvA*sqj$BE!xVN+a z=TaXSX56WDl=HuLA;*khHm-fm3u9EfG?}zv!On#4R6i@4JeINZh1$;CVLyq42MyG) z*TmZAE4X>+5)5BnA$p;nXQpe9dZ}{RtdZt({(sAMkmFkKZvS{;7Vu9=htQwyj#2kN zSEZ2_!4kZ!cGV-Z)!UL(Wt$c8X9$^{9S!*(U15DzYlnY|NuMj4{h>PeQ5|{Sskwec z67WvN`dK4w0q=~%LZ1aOi{>wwcZUjOk1C1^{h`8)sz^4g8&4J%&1MZYpG35OGQ-UK zaX`C)eIByUM=7(78dcr*k0ATRV%7u%3nVq!Twf8R84$j0rBjXCMbB}yS93R z>6?%YAJtO{yXB+Nq+{Dm+pzu8VY!Jan_PB7hfSZ~3eCAxvYt;BGc0?Ciyu{~?3C{I zN7Vz4l9UoICWs-4;CIQNinCm&kFpt{H0R_#YZ)Pyeb&V7|H3?|UjpbZ2FY1-(^5>H_Re9j% zzddOqihg+j>%To^!xK5I{l=3vGxAtNIOKixNR8@{$9m$gPJe%1hF`b>=3H8=MgzbO}638x)mFJuS=# znuSCTB<8hB8C~}AhRV8xej3=8O6BmkwzK_?gd9YARFCyP-nq#=J$C(p|2%53KYh!1 z*U1_j>T)raDjUE69>%L}a=Kp}JM6N}=bl6=+G!?|DXG;2Bpk1deRbrsT_BLmPq%`~ zrY>Bc_&Y@e|Y>r_e_?vKN=5!%c#;7 zjPh7Q;XuReT*%8{o*b29&ks#A6(v|^ZttD!q6KD|e6-;)28i2diFch-1odE#(Lxbwcb+$|WlXmg348%AVy z`V{g~$0cl4B;-y*Ky0B13a7*YFI7_{?jn&5iL+~ydA3Rzl+!8U=Bo5xbgT%{|q@1u_|kD zEpD4TYVfvA4KBtubOo=A%lJ+CTa5oNCr2<($W%kzE{0OjvdOMKm!?7YBbXk+pBHv+ z5MQsfyk97Nje%rD6=c7Yg&(#=AvM`g)Z`Yld5|cURb?%~Y?a>6D0HJ0j}BH`c=QJ| zHKvF?eyR%+7WZswfk@8HW)#um)i94tdAd6ka&)v6+0@e#422y?$W*-QY{OLt#;zYp zTvEeyw0L}^)D72s_cD8z+M)v&A39gw?5b2ra%?_|sydgn7PW=*GQLcqby~ZhD>%2A z=s{!yj=26%sc+a31mxJ2`r_;94P#UHbf(lkhcQuX`|)9|3za^VF@w?)@!3?GQq*1& zkt1Q`!6z(fiKN@cQS(Nv`KVo`tReP`>>MSV^G9y0u;Pi{V3(=kXw0#Q@P*g zRu#jok^j7N)b&^u^Qbl9Wc&NvM&%B4YdR-<+)~s9j;1V-s1k_7k|wY6r;9nB=~=2; zmS*#8JlbP9o>#5MGc4(jdMU55(w2-JQTW#L9xSA&CfS_w6$+#8TH7P$Um!a*7=2Nq|4jY!w_=(2*E5@Wdcg_tplm5C46`=njB&Owj&uCRHsG!+i#BJuw zF+NmCS%xeL)GSNHX2QConu|}EwSX}nr}z54&GLbRxxXrh>M(9k<-|u;h$kf{?9J=Z z`i0XcynuR4Z1%=T$b8(Zzv^@Rc;*~Vi9rQawsKU?93*5euNSehdWDX^S9MBk#Yg7h zy9YK+jLW>MtJCLF0o6^yzC}Xj@>AmyFKx_Q1U0K8XBi8+x@5=aOlI5b^6o} zP}|EdRKB6hw8lQDrJ*t)Jz*FarVX`AYl`c;zzat@&gYD3m0YyIK5S5>t zQW%6c@sZVacDsJ3XQkV<(pkkH0@OId+EW)X2H}@icgKu=Q{jm&p^qwi5w%NV4?x0$ zIF=u-UUZ(z3^Y!T^vjq&k+a8?DBI%gAZP3~iYWIAx;~m`f-#{=!7uK9;n+^!du36Hl{imIc8wI`Yq11{GHjD6-Vw^U?8L?TVG z8I_3K&R4WE5JlkH0S^*$Em-S}+~4$>`RMeO8?OU<>puD|l(NNCb;8=~AR$|^5k*(} zeAqDMH)q&R#njMBbU3pJ7P51K5SGNE%A*RVpnJLDEKtQd+Y(Yr9jL;l+s+_V{`yb*A`j@zjINI6!@mWi0UXO;S`+Iq1Si(`B>Xr)6QclgUMhRM0P^ZM~ zQbD;@$NZs!DpB1U?W#Y+?XMnGxBBslMaml7lM1Y;O4qOk*#i0UpWNP>G9h!hiW~Gt z9?e$ltH#z~ro;B7f1|#uNl}wN-5F04_}I7e zIU*VT{cg0{+o*jGxdX(UCc04LAp#>2^|mJKilck8TwErY4DU^G-Ai2a*|M5o=8bM5 zZc&TcHl9(4NM1~4gPxVtp;~18c_o!7p6@Fu@7nYl{V239rgmnw;Ecc*)>nT zYsS7#E?ajsA1_W`fqcg!u;c_-d~D#Jf6s!$luP ze31>ZY|BXYv+7>Y>YK}6-Dv+uX|nmI9-2D{0du=~p$T_B`D@rzt*K|NScm)!H1u3! z2RAG;HRsY^U$~ad2@1(bFH74 z&)3~vv)#BZfvQ}6YniC3;|%BF#X^s|{^c?oWxf+b zw1-^R&CW7q(ToRN%i?mc_K|~~E~otAE!X+E&h^&Qt997zr+!zAE#2hHTV~tm{^Z%V zxBffTE=S9k-uf}32_IPE`>q}m;h2qydu!HOmVs+Ig5T;hw4+Cx%9rh2%fMUOxyjge z5#{V{P5*w!)+G-8@m^?muG8Z`pzFDc3GIVYzvmliGNyign)}ey7b!YM<1g{2YhEi; zzL}Lyjc#hx>^=F=x=Y;P2F$i;ixuwBwq=jr-JnDFxXq2Mk)un0{};zT0fhtp=2BRW z^nw0;+V^jUx=J1}DN?pizaDLS2k>~ds@30W&nnMp6Htu8G7lCN=?;DR^e8OxF&qKnl}`44dOh)aLd nx^_>hd~2j+d`)SMI=sX!Aax7#y8jwCHEN=kTjg8ow(9=?U~n`q delta 75657 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 { + const snapshot = Bun.generateHeapSnapshot("v8"); + // Sanity check: run the validations from this library + const parsed = await v8HeapSnapshot.parseSnapshot(JSON.parse(snapshot)); + + // Loop over all edges and nodes as another sanity check. + for (const edge of parsed.edges) { + if (!edge.to) { + throw new Error("Edge has no 'to' property"); + } + } + for (const node of parsed.nodes) { + if (!node) { + throw new Error("Node is undefined"); + } + } + + expect(parsed.nodes.length).toBeGreaterThan(0); + expect(parsed.edges.length).toBeGreaterThan(0); +}); + +test("v8.getHeapSnapshot()", async () => { + const snapshot = v8.getHeapSnapshot(); + let chunks = []; + for await (const chunk of snapshot) { + expect(chunk.byteLength).toBeGreaterThan(0); + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); +}); diff --git a/test/package.json b/test/package.json index efe5c43799..5387f4f5cf 100644 --- a/test/package.json +++ b/test/package.json @@ -67,6 +67,7 @@ "svelte": "5.4.0", "typescript": "5.0.2", "undici": "5.20.0", + "v8-heapsnapshot": "1.3.1", "verdaccio": "6.0.0", "vitest": "0.32.2", "webpack": "5.88.0", From faec20080d2fe966852bc77261a819c45f142b7a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 20:27:30 -0800 Subject: [PATCH 10/56] Update nodejs-apis.md --- docs/runtime/nodejs-apis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index f51b4c4553..6b8ac367e0 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -53,7 +53,7 @@ Some methods are not optimized yet. ### [`node:events`](https://nodejs.org/api/events.html) -🟡 `events.addAbortListener` & `events.getMaxListeners` do not support (web api) `EventTarget` +🟢 Fully implemented. `EventEmitterAsyncResource` uses `AsyncResource` underneath. ### [`node:fs`](https://nodejs.org/api/fs.html) @@ -161,7 +161,7 @@ Some methods are not optimized yet. ### [`node:vm`](https://nodejs.org/api/vm.html) -🟡 Core functionality works, but experimental VM ES modules are not implemented, including `vm.Module`, `vm.SourceTextModule`, `vm.SyntheticModule`,`importModuleDynamically`, and `vm.measureMemory`. Options like `timeout`, `breakOnSigint`, `cachedData` are not implemented yet. There is a bug with `this` value for contextified options not having the correct prototype. +🟡 Core functionality works, but experimental VM ES modules are not implemented, including `vm.Module`, `vm.SourceTextModule`, `vm.SyntheticModule`,`importModuleDynamically`, and `vm.measureMemory`. Options like `timeout`, `breakOnSigint`, `cachedData` are not implemented yet. ### [`node:wasi`](https://nodejs.org/api/wasi.html) From dda49d17f9a120cf33b8654ff1510b9815bee2e3 Mon Sep 17 00:00:00 2001 From: Michael H Date: Fri, 3 Jan 2025 15:29:05 +1100 Subject: [PATCH 11/56] docs: fix #16116 (#16122) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d483bf0684..de4ea94c58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts. -If you are using Windows, please refer to [this guide](/docs/project/building-windows.md) +If you are using Windows, please refer to [this guide](/docs/project/building-windows) ## Install Dependencies From ab8fe1a6c31776a5ac6fc557296f694d76b28daf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 21:17:26 -0800 Subject: [PATCH 12/56] Bump --- LATEST | 2 +- bench/bun.lockb | Bin 73871 -> 73871 bytes bench/package.json | 2 +- bench/snippets/node-zlib-brotli.mjs | 37 ++++++++++++++++++++++++++++ package.json | 2 +- 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 bench/snippets/node-zlib-brotli.mjs diff --git a/LATEST b/LATEST index 2c6bb72b8c..f1e15715db 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.1.41 \ No newline at end of file +1.1.42 \ No newline at end of file diff --git a/bench/bun.lockb b/bench/bun.lockb index e77a3b406cbb3bbefb85da86f9112d458613b082..7ccc5f77c570b405dc3a11bff97bc332afd6290b 100755 GIT binary patch delta 203 zcmeA_$kKn1WrCi<(#hNX19i6sD1@%0FM3XP;t||OG_rK7W zIknLajM+ZQ3EGMqVh+ij|GYfB-7_F!#dL?C1qqwqCcKVUox;Gt&3_cQ(sxWKOYJy<2JMBu{p&2JN4$E)@-FfjB1aX%1G0OE;2JPC*=1Mw6f zo(ja%Hvc{Fno&{($W#SlH6T_8VhteH1Y)htjE4o-m<$az3!fC#VKkf`D8s0*+4HNL FHUPsoOqKuu diff --git a/bench/package.json b/bench/package.json index a80d7566dc..d71efc00aa 100644 --- a/bench/package.json +++ b/bench/package.json @@ -13,7 +13,7 @@ "execa": "^8.0.1", "fast-glob": "3.3.1", "fdir": "^6.1.0", - "mitata": "^1.0.10", + "mitata": "^1.0.25", "react": "^18.3.1", "react-dom": "^18.3.1", "string-width": "7.1.0", diff --git a/bench/snippets/node-zlib-brotli.mjs b/bench/snippets/node-zlib-brotli.mjs new file mode 100644 index 0000000000..01208d3ec9 --- /dev/null +++ b/bench/snippets/node-zlib-brotli.mjs @@ -0,0 +1,37 @@ +import { bench, run } from "../runner.mjs"; +import { brotliCompress, brotliDecompress, createBrotliCompress, createBrotliDecompress } from "node:zlib"; +import { promisify } from "node:util"; +import { pipeline } from "node:stream/promises"; +import { Readable } from "node:stream"; +import { readFileSync } from "node:fs"; + +const brotliCompressAsync = promisify(brotliCompress); +const brotliDecompressAsync = promisify(brotliDecompress); + +const testData = + process.argv.length > 2 + ? readFileSync(process.argv[2]) + : Buffer.alloc(1024 * 1024 * 16, "abcdefghijklmnopqrstuvwxyz"); +let compressed; + +bench("brotli compress", async () => { + compressed = await brotliCompressAsync(testData); +}); + +bench("brotli decompress", async () => { + await brotliDecompressAsync(compressed); +}); + +bench("brotli compress stream", async () => { + const source = Readable.from([testData]); + const compress = createBrotliCompress(); + await pipeline(source, compress); +}); + +bench("brotli decompress stream", async () => { + const source = Readable.from([compressed]); + const decompress = createBrotliDecompress(); + await pipeline(source, decompress); +}); + +await run(); diff --git a/package.json b/package.json index 56ad737a1d..b0cfe51737 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "bun", - "version": "1.1.42", + "version": "1.1.43", "workspaces": [ "./packages/bun-types" ], From 79430091a15d5a7ac179387cc4650a4e9a57b101 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 2 Jan 2025 21:24:16 -0800 Subject: [PATCH 13/56] Add v8.writeHeapSnapshot (#16123) --- docs/runtime/nodejs-apis.md | 2 +- src/js/node/v8.ts | 42 +++++++++++++++++++++-- test/js/bun/util/v8-heap-snapshot.test.ts | 23 +++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 6b8ac367e0..da92af28e4 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -157,7 +157,7 @@ Some methods are not optimized yet. ### [`node:v8`](https://nodejs.org/api/v8.html) -🔴 `serialize` and `deserialize` use JavaScriptCore's wire format instead of V8's. Otherwise, not implemented. For profiling, use [`bun:jsc`](https://bun.sh/docs/project/benchmarking#bunjsc) instead. +🟡 `writeHeapSnapshot` and `getHeapSnapshot` are implemented. `serialize` and `deserialize` use JavaScriptCore's wire format instead of V8's. Other methods are not implemented. For profiling, use [`bun:jsc`](https://bun.sh/docs/project/benchmarking#bunjsc) instead. ### [`node:vm`](https://nodejs.org/api/vm.html) diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index e9f158dc04..5303d1b4e0 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:v8" + // This is a stub! None of this is actually implemented yet. const { hideFromStack, throwNotImplemented } = require("internal/shared"); const jsc: typeof import("bun:jsc") = require("bun:jsc"); @@ -105,8 +106,45 @@ function stopCoverage() { function serialize(arg1) { return jsc.serialize(arg1, { binaryType: "nodebuffer" }); } -function writeHeapSnapshot() { - notimpl("writeHeapSnapshot"); + +function getDefaultHeapSnapshotPath() { + const date = new Date(); + + const worker_threads = require("node:worker_threads"); + const thread_id = worker_threads.threadId; + + const yyyy = date.getFullYear(); + const mm = date.getMonth().toString().padStart(2, "0"); + const dd = date.getDate().toString().padStart(2, "0"); + const hh = date.getHours().toString().padStart(2, "0"); + const MM = date.getMinutes().toString().padStart(2, "0"); + const ss = date.getSeconds().toString().padStart(2, "0"); + + // 'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot' + return `Heap-${yyyy}${mm}${dd}-${hh}${MM}${ss}-${process.pid}-${thread_id}.heapsnapshot`; +} + +let fs; + +function writeHeapSnapshot(path, options) { + if (path !== undefined) { + if (typeof path !== "string") { + throw $ERR_INVALID_ARG_TYPE("path", "string", path); + } + + if (!path) { + throw $ERR_INVALID_ARG_VALUE("path must be a non-empty string"); + } + } else { + path = getDefaultHeapSnapshotPath(); + } + + if (!fs) { + fs = require("node:fs"); + } + fs.writeFileSync(path, Bun.generateHeapSnapshot("v8"), "utf-8"); + + return path; } function setHeapSnapshotNearHeapLimit() { notimpl("setHeapSnapshotNearHeapLimit"); diff --git a/test/js/bun/util/v8-heap-snapshot.test.ts b/test/js/bun/util/v8-heap-snapshot.test.ts index 805fdcc79f..5125b26667 100644 --- a/test/js/bun/util/v8-heap-snapshot.test.ts +++ b/test/js/bun/util/v8-heap-snapshot.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test"; +import { tempDirWithFiles } from "harness"; +import { join } from "node:path"; import * as v8 from "v8"; import * as v8HeapSnapshot from "v8-heapsnapshot"; @@ -32,3 +34,24 @@ test("v8.getHeapSnapshot()", async () => { } expect(chunks.length).toBeGreaterThan(0); }); + +test("v8.writeHeapSnapshot()", async () => { + const path = v8.writeHeapSnapshot(); + expect(path).toBeDefined(); + expect(path).toContain("Heap-"); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); + +test("v8.writeHeapSnapshot() with path", async () => { + const dir = tempDirWithFiles("v8-heap-snapshot", { + "test.heapsnapshot": "", + }); + + const path = join(dir, "test.heapsnapshot"); + v8.writeHeapSnapshot(path); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); From f0cb1b723e218439c77917246a2ca7e40bd2adfa Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 3 Jan 2025 04:32:27 -0800 Subject: [PATCH 14/56] Remove spinlock in libpas on Linux (#16130) --- cmake/tools/SetupWebKit.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 10ac6d22a9..19a59b77c5 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION 05798ff248070ff86118ae40583f759dbd193c6f) + set(WEBKIT_VERSION c30d63fe69913f17dce5fcbe17d06e670e0eaeff) endif() if(WEBKIT_LOCAL) From c130df6c589fdf28f9f3c7f23ed9901140bc9349 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 3 Jan 2025 08:21:00 -0800 Subject: [PATCH 15/56] start verdaccio in multiple test files (#16118) --- src/install/install.zig | 24 +- .../bun-install-registry.test.ts.snap | 0 .../__snapshots__/bun-workspaces.test.ts.snap | 3650 ++++++++--------- test/cli/install/bun-add.test.ts | 3 +- .../bun-install-lifecycle-scripts.test.ts | 2910 +++++++++++++ .../bun-install-registry.test.ts | 3598 +--------------- test/cli/install/bun-install-retry.test.ts | 3 +- test/cli/install/bun-install.test.ts | 2 +- test/cli/install/bun-link.test.ts | 18 +- test/cli/install/bun-pm.test.ts | 3 +- test/cli/install/bun-run.test.ts | 12 +- test/cli/install/bun-update.test.ts | 3 +- test/cli/install/bun-workspaces.test.ts | 676 ++- test/cli/install/bunx.test.ts | 3 +- test/cli/install/dummy.registry.ts | 6 - .../missing-directory-bin-1.1.1.tgz | Bin test/harness.ts | 86 +- test/regression/issue/08093.test.ts | 3 +- 18 files changed, 5549 insertions(+), 5451 deletions(-) rename test/cli/install/{registry => }/__snapshots__/bun-install-registry.test.ts.snap (100%) create mode 100644 test/cli/install/bun-install-lifecycle-scripts.test.ts rename test/cli/install/{registry => }/bun-install-registry.test.ts (69%) rename test/cli/install/{registry => }/missing-directory-bin-1.1.1.tgz (100%) diff --git a/src/install/install.zig b/src/install/install.zig index fb21bdec05..c7a92109a2 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6239,17 +6239,17 @@ pub const PackageManager = struct { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false }; } - if (env.get("BUN_INSTALL")) |dir| { - var parts = [_]string{ dir, "install/", "cache/" }; - return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; - } - if (options) |opts| { if (opts.cache_directory.len > 0) { return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{opts.cache_directory}), .is_node_modules = false }; } } + if (env.get("BUN_INSTALL")) |dir| { + var parts = [_]string{ dir, "install/", "cache/" }; + return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; + } + if (env.get("XDG_CACHE_HOME")) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; @@ -7280,6 +7280,10 @@ pub const PackageManager = struct { this.did_override_default_scope = this.scope.url_hash != Npm.Registry.default_url_hash; } if (bun_install_) |config| { + if (config.cache_directory) |cache_directory| { + this.cache_directory = cache_directory; + } + if (config.scoped) |scoped| { for (scoped.scopes.keys(), scoped.scopes.values()) |name, *registry_| { var registry = registry_.*; @@ -7475,6 +7479,10 @@ pub const PackageManager = struct { this.scope.url = URL.parse(cli.registry); } + if (cli.cache_dir) |cache_dir| { + this.cache_directory = cache_dir; + } + if (cli.exact) { this.enable.exact_versions = true; } @@ -9596,7 +9604,7 @@ pub const PackageManager = struct { }); pub const CommandLineArguments = struct { - cache_dir: string = "", + cache_dir: ?string = null, lockfile: string = "", token: string = "", global: bool = false, @@ -9980,6 +9988,10 @@ pub const PackageManager = struct { cli.no_summary = args.flag("--no-summary"); cli.ca = args.options("--ca"); + if (args.option("--cache-dir")) |cache_dir| { + cli.cache_dir = cache_dir; + } + if (args.option("--cafile")) |ca_file_name| { cli.ca_file_name = ca_file_name; } diff --git a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap similarity index 100% rename from test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap rename to test/cli/install/__snapshots__/bun-install-registry.test.ts.snap diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index 423a2b49ae..b419206fba 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -1,2177 +1,2177 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`dependency on workspace without version in package.json: version: * 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *.*.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*.*.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*.*.*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: =* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "=*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "=*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: kjwoehcojrgjoj 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "kjwoehcojrgjoj", - "name": "lodash", - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "kjwoehcojrgjoj", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *.1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*.1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*.1.*", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *-pre 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*-pre", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 1, - }, - ], +"{ "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", "package_index": { - "bar": 2, + "no-deps": 1, "foo": 0, - "lodash": 1, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true }, - "depth": 0, - "id": 0, - "path": "node_modules", + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*-pre", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 1, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "1", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", - "tag": "npm", - "value": "1.3.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1", + "npm": { + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", - "tag": "npm", - "value": "1.3.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.*", + "npm": { + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1.1.* 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "1.1.*", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "<1.2.0 >=1.1.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", - "tag": "npm", - "value": "1.1.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.1.*", + "npm": { + "name": "no-deps", + "version": "<1.2.0 >=1.1.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; -exports[`dependency on workspace without version in package.json: version: 1.1.1 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "1.1.1", - "name": "lodash", - "npm": { - "name": "lodash", - "version": "==1.1.1", - }, - "package_id": 3, - }, - ], +exports[`dependency on workspace without version in package.json: version: 1.1.0 1`] = ` +"{ "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", - "tag": "npm", - "value": "1.1.1", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.1.0", + "npm": { + "name": "no-deps", + "version": "==1.1.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *-pre+build 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*-pre+build", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*-pre+build", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: *+build 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "id": 2, - "literal": "*+build", - "name": "lodash", - "npm": { - "name": "lodash", - "version": ">=0.0.0", - }, - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "*+build", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: latest 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "latest", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on workspace without version in package.json: version: 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} + "11592711315645265694": "1.0.0" + } +}" `; exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` -{ - "dependencies": [ - { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", - "name": "bar", - "package_id": 2, - "workspace": "packages/bar", - }, - { - "behavior": { - "workspace": true, - }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", - }, - { - "behavior": { - "prod": true, - "workspace": true, - }, - "dist_tag": { - "name": "lodash", - "tag": "lodash", - }, - "id": 2, - "literal": "latest", - "name": "lodash", - "package_id": 3, - }, - ], +"{ "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ + "no-deps": [ 1, - 3, + 3 ], + "foo": 0, + "bar": 2 }, - "packages": [ - { - "bin": null, - "dependencies": [ - 0, - 1, - ], - "id": 0, - "integrity": null, - "man_dir": "", - "name": "foo", - "name_hash": "14841791273925386894", - "origin": "local", - "resolution": { - "resolved": "", - "tag": "root", - "value": "", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/mono", - "tag": "workspace", - "value": "workspace:packages/mono", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [ - 2, - ], - "id": 2, - "integrity": null, - "man_dir": "", - "name": "bar", - "name_hash": "11592711315645265694", - "origin": "npm", - "resolution": { - "resolved": "workspace:packages/bar", - "tag": "workspace", - "value": "workspace:packages/bar", - }, - "scripts": {}, - }, - { - "bin": null, - "dependencies": [], - "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", - "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "packages/bar", + "workspace": "packages/bar", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 0 }, + { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", + "dist_tag": { + "name": "no-deps", + "tag": "no-deps" + }, + "package_id": 3, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 3, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "npm", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" + }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { "11592711315645265694": "1.0.0", - "15298228331728003776": "4.17.21", - }, -} + "5128161233225832376": "4.17.21" + } +}" `; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 89c4a9e312..2cc47f7bef 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, appendFile, copyFile, mkdir, readlink, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join, relative, resolve } from "path"; import { check_npm_auth_type, @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts new file mode 100644 index 0000000000..44bcf1bb5c --- /dev/null +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -0,0 +1,2910 @@ +import { + VerdaccioRegistry, + isLinux, + bunEnv as env, + bunExe, + assertManifestsPopulated, + readdirSorted, + isWindows, + stderrForInstall, + runBunInstall, +} from "harness"; +import { beforeAll, afterAll, beforeEach, test, expect, describe, setDefaultTimeout } from "bun:test"; +import { writeFile, exists, rm, mkdir } from "fs/promises"; +import { join, sep } from "path"; +import { spawn, file, write } from "bun"; + +var verdaccio = new VerdaccioRegistry(); +var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + setDefaultTimeout(1000 * 60 * 5); + await verdaccio.start(); +}); + +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); + env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); + env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); +}); + +// waiter thread is only a thing on Linux. +for (const forceWaiterThread of isLinux ? [false, true] : [false]) { + describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { + test("root package with all lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + const writeScript = async (name: string) => { + const contents = ` + import { writeFileSync, existsSync, rmSync } from "fs"; + import { join } from "path"; + + const file = join(import.meta.dir, "${name}.txt"); + + if (existsSync(file)) { + rmSync(file); + writeFileSync(file, "${name} exists!"); + } else { + writeFileSync(file, "${name}!"); + } + `; + await writeFile(join(packageDir, `${name}.js`), contents); + }; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + }), + ); + + await writeScript("preinstall"); + await writeScript("install"); + await writeScript("postinstall"); + await writeScript("preprepare"); + await writeScript("prepare"); + await writeScript("postprepare"); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + // add a dependency with all lifecycle scripts + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + + expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "install.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + await rm(join(packageDir, "preinstall.txt")); + await rm(join(packageDir, "install.txt")); + await rm(join(packageDir, "postinstall.txt")); + await rm(join(packageDir, "preprepare.txt")); + await rm(join(packageDir, "prepare.txt")); + await rm(join(packageDir, "postprepare.txt")); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + // all at once + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + }); + + test("workspace lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); + }); + + test("dependency lifecycle scripts run before root lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: script, + postinstall: script, + preinstall: script, + prepare: script, + postprepare: script, + preprepare: script, + }, + }), + ); + + // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for + // for this to happen before running its lifecycle scripts. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + "", + "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "install.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + // add to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 1 install across 2 packages (no changes)"), + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + }); + + test("adding a package without scripts to trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; + // prettier-ignore + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { "what-bin": "1.0.0" }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + // add it to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + }); + + test("lifecycle scripts run if node_modules is deleted", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-postinstall"], + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + // @ts-ignore + "1 package installed", + ]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("INIT_CWD is set to the correct directory", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "bun install.js", + }, + dependencies: { + "lifecycle-init-cwd": "1.0.0", + "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", + }, + trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], + }), + ); + + await writeFile( + join(packageDir, "install.js"), + ` + const fs = require("fs"); + const path = require("path"); + + fs.writeFileSync( + path.join(__dirname, "test.txt"), + process.env.INIT_CWD || "does not exist" + ); + `, + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ another-init-cwd@1.0.0", + "+ lifecycle-init-cwd@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); + }); + + test("failing lifecycle script should print output", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("hello"); + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const out = await new Response(stdout).text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + }); + + test("failing root lifecycle script should print output correctly", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "fooooooooo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("error: Oops!"); + expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); + }); + + test("exit 0 in lifecycle scripts works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("--ignore-scripts should skip lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ignore-scripts"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("hello"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-failing-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + trustedDependencies: ["binding-gyp-scripts"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); + + packageJSON.trustedDependencies = ["binding-gyp-scripts"]; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts work in package root", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + + await rm(join(packageDir, "build.node")); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + for (const script of ["install", "preinstall"]) { + test(`does not add auto node-gyp script when ${script} script exists`, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + [script]: "exit 0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + await writeFile(join(packageDir, "binding.gyp"), ""); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeFalse(); + }); + } + + test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", + "", + "1 package installed", + "", + "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + trustedDependencies: ["lifecycle-install-test"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); + }); + + test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', + }, + }), + ); + + // Package `uses-what-bin-slow` has an install script that will sleep for 1 second + // before writing `what-bin.txt` to disk. The root package has an install script that + // checks if this file exists. If the root package install script does not wait for + // the other to finish, it will fail. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uses-what-bin-slow@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + async function createPackagesWithScripts( + packagesCount: number, + scripts: Record, + ): Promise { + const dependencies: Record = {}; + const dependenciesList: string[] = []; + + for (let i = 0; i < packagesCount; i++) { + const packageName: string = "stress-test-package-" + i; + const packageVersion = "1.0." + i; + + dependencies[packageName] = "file:./" + packageName; + dependenciesList[i] = packageName; + + const packagePath = join(packageDir, packageName); + await mkdir(packagePath); + await writeFile( + join(packagePath, "package.json"), + JSON.stringify({ + name: packageName, + version: packageVersion, + scripts, + }), + ); + } + + await writeFile( + packageJson, + JSON.stringify({ + name: "stress-test", + version: "1.0.0", + dependencies, + trustedDependencies: dependenciesList, + }), + ); + + return dependenciesList; + } + + test("reach max concurrent scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const scripts = { + "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, + }; + + const dependenciesList = await createPackagesWithScripts(4, scripts); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--concurrent-scripts=2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`), + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("stress test", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dependenciesList = await createPackagesWithScripts(500, { + "postinstall": `${bunExe()} --version`, + }); + + // the script is quick, default number for max concurrent scripts + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), + "", + "500 packages installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should install and use correct binary version", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + // this should install `what-bin` in two places: + // + // - node_modules/.bin/what-bin@1.5.0 + // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.0.0", + "what-bin": "1.5.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "+ what-bin@1.5.0", + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.5.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.0.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.5.0", + "what-bin": "1.0.0", + }, + scripts: { + install: "what-bin", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.0.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.5.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + out = await new Response(stdout).text(); + err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("node-gyp should always be available for lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "node-gyp --version", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + + // if node-gyp isn't available, it would return a non-zero exit code + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + // if this test fails, `electron` might be removed from the default list + test("default trusted dependencies should work", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(out).not.toContain("Blocked"); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + // fake electron package because it's in the default trustedDependencies list + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + // electron lifecycle scripts should run, uses-what-bin scripts should not run + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + // now uses-what-bin scripts should run and electron scripts should not run. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("does not run any scripts if trustedDependencies is an empty list", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("will run default trustedDependencies after install that didn't include them", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + trustedDependencies: ["blah"], + }), + ); + + // first install does not run electron scripts + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + }), + ); + + // The electron scripts should run now because it's in default trusted dependencies. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + + describe("--trust", async () => { + test("unhoisted untrusted scripts, none at root node_modules", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // prevents real `uses-what-bin` from hoisting to root + "uses-what-bin": "npm:a-dep@1.0.3", + }, + workspaces: ["pkg1"], + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ), + ]); + + await runBunInstall(testEnv, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ]); + + expect(results).toEqual([true, false]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + expect( + await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ).toBeTrue(); + }); + const trustTests = [ + { + label: "only name", + packageJson: { + name: "foo", + }, + }, + { + label: "empty dependencies", + packageJson: { + name: "foo", + dependencies: {}, + }, + }, + { + label: "populated dependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }, + }, + + { + label: "empty trustedDependencies", + packageJson: { + name: "foo", + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies, empty trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }, + }, + + { + label: "empty dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: {}, + trustedDependencies: [], + }, + }, + ]; + for (const { label, packageJson } of trustTests) { + test(label, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed uses-what-bin@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(join(packageDir, "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // another install should not error with json SyntaxError + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + }); + } + describe("packages without lifecycle scripts", async () => { + test("initial install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }); + }); + test("already installed", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // oops, I wanted to run the lifecycle scripts for no-deps, I'll install + // again with --trust. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + // oh, I didn't realize no-deps doesn't have + // any lifecycle scripts. It shouldn't automatically add to + // trustedDependencies. + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + }); + }); + }); + + describe("updating trustedDependencies", async () => { + test("existing trustedDependencies, unchanged trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // no changes, lockfile shouldn't be saved + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("existing trustedDependencies, removing trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + // this script should not run because uses-what-bin is no longer in trustedDependencies + await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + }); + + test("non-existent trustedDependencies, then adding it", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["electron"], + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); + + // lockfile should save evenn though there are no changes to trustedDependencies due to + // the default list + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + }); + + test("node -p should work in postinstall scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + }); + + test("ensureTempNodeGypScript works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: "node-gyp --version", + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("bun pm trust and untrusted on missing package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.5.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package + await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("bun pm untrusted"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("Found 0 untrusted dependencies with scripts"); + expect(await exited).toBe(0); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(1); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("bun pm trust"); + expect(err).toContain("0 scripts ran"); + expect(err).toContain("uses-what-bin"); + }); + + describe("add trusted, delete, then add again", async () => { + // when we change bun install to delete dependencies from node_modules + // for both cases, we need to update this test + for (const withRm of [true, false]) { + test(withRm ? "withRm" : "withoutRm", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 script ran across 1 package"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // now remove and install again + if (withRm) { + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "rm", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 package removed"); + expect(out).toContain("uses-what-bin"); + expect(await exited).toBe(0); + } + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + let expected = withRm + ? ["", "Checked 1 install across 2 packages (no changes)"] + : ["", expect.stringContaining("1 package removed")]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); + + // add again, bun pm untrusted should report it as untrusted + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expected = withRm + ? [ + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ] + : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); + expect(await exited).toBe(0); + }); + } + }); + + describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { + test("install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, + }, + }), + ); + + const proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + stdin: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); + }); + + // https://github.com/oven-sh/bun/issues/11252 + test.todoIf(isWindows)("bun pm trust", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + [dep]: "1.0.0", + }, + }), + ); + + var { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); + + const proc = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); + }); + }); + }); + + describe("stdout/stderr is inherited from root scripts during install", async () => { + test("without packages", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "No packages! Deleted empty lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("with a package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "Resolving dependencies", + expect.stringContaining("Resolved, downloaded and extracted "), + "Saved lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + }); +} diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts similarity index 69% rename from test/cli/install/registry/bun-install-registry.test.ts rename to test/cli/install/bun-install-registry.test.ts index 846635dfd4..87b5ae2730 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1,7 +1,7 @@ import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { afterAll, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout, test } from "bun:test"; -import { ChildProcess, fork } from "child_process"; +import { ChildProcess } from "child_process"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, mkdir, readlink, rm, writeFile } from "fs/promises"; import { @@ -25,9 +25,10 @@ import { tls, isFlaky, isMacOS, + readdirSorted, + VerdaccioRegistry, } from "harness"; import { join, resolve, sep } from "path"; -import { readdirSorted } from "../dummy.registry"; const { parseLockfile } = install_test_helpers; const { iniInternals } = require("bun:internal-for-testing"); const { loadNpmrc } = iniInternals; @@ -38,8 +39,8 @@ expect.extend({ toMatchNodeModulesAt, }); -var verdaccioServer: ChildProcess; -var port: number = randomPort(); +var verdaccio: VerdaccioRegistry; +var port: number; var packageDir: string; /** packageJson = join(packageDir, "package.json"); */ var packageJson: string; @@ -47,69 +48,28 @@ var packageJson: string; let users: Record = {}; beforeAll(async () => { - console.log("STARTING VERDACCIO"); setDefaultTimeout(1000 * 60 * 5); - verdaccioServer = fork( - require.resolve("verdaccio/bin/verdaccio"), - ["-c", join(import.meta.dir, "verdaccio.yaml"), "-l", `${port}`], - { - silent: true, - // Prefer using a release build of Bun since it's faster - execPath: Bun.which("bun") || bunExe(), - }, - ); - - verdaccioServer.stderr?.on("data", data => { - console.error(`Error: ${data}`); - }); - - verdaccioServer.on("error", error => { - console.error(`Failed to start child process: ${error}`); - }); - - verdaccioServer.on("exit", (code, signal) => { - if (code !== 0) { - console.error(`Child process exited with code ${code} and signal ${signal}`); - } else { - console.log("Child process exited successfully"); - } - }); - - await new Promise(done => { - verdaccioServer.on("message", (msg: { verdaccio_started: boolean }) => { - if (msg.verdaccio_started) { - console.log("Verdaccio started"); - done(); - } - }); - }); + verdaccio = new VerdaccioRegistry(); + port = verdaccio.port; + await verdaccio.start(); }); afterAll(async () => { await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - if (verdaccioServer) verdaccioServer.kill(); + verdaccio.stop(); }); beforeEach(async () => { - packageDir = tmpdirSync(); - packageJson = join(packageDir, "package.json"); + ({ packageDir, packageJson } = await verdaccio.createTestDir()); await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); users = {}; env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - await writeFile( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = "${join(packageDir, ".bun-cache")}" -registry = "http://localhost:${port}/" -`, - ); }); function registryUrl() { - return `http://localhost:${port}/`; + return verdaccio.registryUrl(); } /** @@ -549,7 +509,7 @@ describe("certificate authority", () => { const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { return async function (req: Request) { if (req.url.includes("no-deps")) { - return new Response(Bun.file(join(import.meta.dir, "packages", "no-deps", "no-deps-1.0.0.tgz"))); + return new Response(Bun.file(join(import.meta.dir, "registry", "packages", "no-deps", "no-deps-1.0.0.tgz"))); } return new Response("OK", { status: 200 }); }; @@ -1036,7 +996,7 @@ describe("publish", async () => { cache = false registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-1"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1068,7 +1028,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-2"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1106,7 +1066,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-3"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-3"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1149,7 +1109,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-4"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-4"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1175,7 +1135,7 @@ describe("publish", async () => { test("can publish a package then install it", async () => { const bunfig = await authBunfig("basic"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-1"), { recursive: true, force: true }), write( packageJson, JSON.stringify({ @@ -1207,7 +1167,7 @@ describe("publish", async () => { }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "bunfig.toml"), bunfig), ]); @@ -1223,7 +1183,7 @@ describe("publish", async () => { expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue(); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }), rm(join(packageDir, "node_modules"), { recursive: true, force: true }), ]); @@ -1249,7 +1209,7 @@ describe("publish", async () => { console.log({ packageDir, publishDir }); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-bins"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-bins"), { recursive: true, force: true }), write( join(publishDir, "package.json"), JSON.stringify({ @@ -1313,7 +1273,7 @@ describe("publish", async () => { const publishDir = tmpdirSync(); const bunfig = await authBunfig("manydeps"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-deps"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-deps"), { recursive: true, force: true }), write( join(publishDir, "package.json"), JSON.stringify( @@ -1373,7 +1333,7 @@ describe("publish", async () => { }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-3"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-3"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1398,7 +1358,7 @@ describe("publish", async () => { test("does not publish", async () => { const bunfig = await authBunfig("dryrun"); await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "dry-run-1"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1415,12 +1375,12 @@ describe("publish", async () => { const { out, err, exitCode } = await publish(env, packageDir, "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "dry-run-1"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "dry-run-1"))).toBeFalse(); }); test("does not publish from tarball path", async () => { const bunfig = await authBunfig("dryruntarball"); await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "dry-run-2"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1439,7 +1399,7 @@ describe("publish", async () => { const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "dry-run-2"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "dry-run-2"))).toBeFalse(); }); }); @@ -1473,7 +1433,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test(`should run in order${arg ? " (--dry-run)" : ""}`, async () => { const bunfig = await authBunfig("lifecycle" + (arg ? "dry" : "")); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-4"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-4"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -1505,7 +1465,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("--ignore-scripts", async () => { const bunfig = await authBunfig("ignorescripts"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-5"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-5"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -1530,7 +1490,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("attempting to publish a private package should fail", async () => { const bunfig = await authBunfig("privatepackage"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-6"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-6"), { recursive: true, force: true }), write( packageJson, JSON.stringify({ @@ -1549,7 +1509,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; let { out, err, exitCode } = await publish(env, packageDir); expect(exitCode).toBe(1); expect(err).toContain("error: attempted to publish a private package"); - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); // try tarball await pack(packageDir, env); @@ -1563,7 +1523,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("--access", async () => { const bunfig = await authBunfig("accessflag"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-7"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-7"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1582,7 +1542,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; ({ out, err, exitCode } = await publish(env, packageDir, "--access", "public")); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-7"))).toBeTrue(); + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-7"))).toBeTrue(); }); for (const access of ["restricted", "public"]) { @@ -1601,7 +1561,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; }; await Promise.all([ - rm(join(import.meta.dir, "packages", "@secret", "publish-pkg-8"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "@secret", "publish-pkg-8"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1629,7 +1589,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-9"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-9"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -5271,616 +5231,6 @@ describe("hoisting", async () => { }); }); -describe("workspaces", async () => { - test("adding packages in a subdirectory of a workspace", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "root", - workspaces: ["foo"], - }), - ); - - await mkdir(join(packageDir, "folder1")); - await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); - await writeFile( - join(packageDir, "foo", "package.json"), - JSON.stringify({ - name: "foo", - }), - ); - - // add package to root workspace from `folder1` - let { stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - }); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "root", - workspaces: ["foo"], - dependencies: { - "no-deps": "^2.0.0", - }, - }); - - // add package to foo from `folder2` - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "what-bin"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed what-bin@1.5.0 with binaries:", - " - what-bin", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "what-bin": "^1.5.0", - }, - }); - - // now delete node_modules and bun.lockb and install - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ no-deps@2.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); - }); - test("adding packages in workspaces", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "bar": "workspace:*", - }, - }), - ); - - await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); - await mkdir(join(packageDir, "packages", "boba")); - await mkdir(join(packageDir, "packages", "pkg5")); - - await writeFile(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); - await writeFile( - join(packageDir, "packages", "boba", "package.json"), - JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), - ); - await writeFile( - join(packageDir, "packages", "pkg5", "package.json"), - JSON.stringify({ - name: "pkg5", - version: "1.2.3", - dependencies: { - "bar": "workspace:*", - }, - }), - ); - - let { stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - }); - - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:packages/bar", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); - - // add a package to the root workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - bar: "workspace:*", - "no-deps": "^2.0.0", - }, - }); - - // add a package in a workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "two-range-deps"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed two-range-deps@1.0.0", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", - ]); - - // add a dependency to a workspace with the same name as another workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "bar@0.0.7"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); - - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed bar@0.0.7", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - "bar": "0.0.7", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", - ]); - expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.7", - description: "not a workspace", - }); - }); - test("it should detect duplicate workspace dependencies", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); - - var { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); - }); - - const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; - - for (const rootVersion of versions) { - for (const packageVersion of versions) { - test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - pkg2: rootVersion, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - dependencies: { - pkg2: packageVersion, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ name: "pkg2", version: "1.0.0" }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - } - - for (const version of versions) { - test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "workspace-1": version, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "workspace-1", "package.json"), - JSON.stringify({ - name: "workspace-1", - version: "1.0.0", - }), - ); - // install first from the root, the workspace package - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ workspace-1@workspace:packages/workspace-1`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); - - // install from workspace package then from root - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", - }); - }); - } -}); - describe("transitive file dependencies", () => { async function checkHoistedFiles() { const aliasedFileDepFilesPackageJson = join( @@ -7758,2882 +7108,6 @@ test("missing package on reinstall, some with binaries", async () => { ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); }); -// waiter thread is only a thing on Linux. -for (const forceWaiterThread of isLinux ? [false, true] : [false]) { - describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { - test("root package with all lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - const writeScript = async (name: string) => { - const contents = ` - import { writeFileSync, existsSync, rmSync } from "fs"; - import { join } from "path"; - - const file = join(import.meta.dir, "${name}.txt"); - - if (existsSync(file)) { - rmSync(file); - writeFileSync(file, "${name} exists!"); - } else { - writeFileSync(file, "${name}!"); - } - `; - await writeFile(join(packageDir, `${name}.js`), contents); - }; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - }), - ); - - await writeScript("preinstall"); - await writeScript("install"); - await writeScript("postinstall"); - await writeScript("preprepare"); - await writeScript("prepare"); - await writeScript("postprepare"); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - // add a dependency with all lifecycle scripts - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - - expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "install.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - await rm(join(packageDir, "preinstall.txt")); - await rm(join(packageDir, "install.txt")); - await rm(join(packageDir, "postinstall.txt")); - await rm(join(packageDir, "preprepare.txt")); - await rm(join(packageDir, "prepare.txt")); - await rm(join(packageDir, "postprepare.txt")); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - // all at once - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - }); - - test("workspace lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); - }); - - test("dependency lifecycle scripts run before root lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: script, - postinstall: script, - preinstall: script, - prepare: script, - postprepare: script, - preprepare: script, - }, - }), - ); - - // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for - // for this to happen before running its lifecycle scripts. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - "", - "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "install.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - // add to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 1 install across 2 packages (no changes)"), - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - }); - - test("adding a package without scripts to trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; - // prettier-ignore - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { "what-bin": "1.0.0" }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - // add it to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - }); - - test("lifecycle scripts run if node_modules is deleted", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-postinstall"], - }), - ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - // @ts-ignore - "1 package installed", - ]); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("INIT_CWD is set to the correct directory", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "bun install.js", - }, - dependencies: { - "lifecycle-init-cwd": "1.0.0", - "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", - }, - trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], - }), - ); - - await writeFile( - join(packageDir, "install.js"), - ` - const fs = require("fs"); - const path = require("path"); - - fs.writeFileSync( - path.join(__dirname, "test.txt"), - process.env.INIT_CWD || "does not exist" - ); - `, - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ another-init-cwd@1.0.0", - "+ lifecycle-init-cwd@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); - }); - - test("failing lifecycle script should print output", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("hello"); - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const out = await new Response(stdout).text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - }); - - test("failing root lifecycle script should print output correctly", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "fooooooooo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("error: Oops!"); - expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); - }); - - test("exit 0 in lifecycle scripts works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("--ignore-scripts should skip lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--ignore-scripts"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("hello"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-failing-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - trustedDependencies: ["binding-gyp-scripts"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); - - packageJSON.trustedDependencies = ["binding-gyp-scripts"]; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts work in package root", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - - await rm(join(packageDir, "build.node")); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - for (const script of ["install", "preinstall"]) { - test(`does not add auto node-gyp script when ${script} script exists`, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - [script]: "exit 0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - await writeFile(join(packageDir, "binding.gyp"), ""); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeFalse(); - }); - } - - test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", - "", - "1 package installed", - "", - "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - trustedDependencies: ["lifecycle-install-test"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); - }); - - test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', - }, - }), - ); - - // Package `uses-what-bin-slow` has an install script that will sleep for 1 second - // before writing `what-bin.txt` to disk. The root package has an install script that - // checks if this file exists. If the root package install script does not wait for - // the other to finish, it will fail. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uses-what-bin-slow@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - async function createPackagesWithScripts( - packagesCount: number, - scripts: Record, - ): Promise { - const dependencies: Record = {}; - const dependenciesList = []; - - for (let i = 0; i < packagesCount; i++) { - const packageName: string = "stress-test-package-" + i; - const packageVersion = "1.0." + i; - - dependencies[packageName] = "file:./" + packageName; - dependenciesList[i] = packageName; - - const packagePath = join(packageDir, packageName); - await mkdir(packagePath); - await writeFile( - join(packagePath, "package.json"), - JSON.stringify({ - name: packageName, - version: packageVersion, - scripts, - }), - ); - } - - await writeFile( - packageJson, - JSON.stringify({ - name: "stress-test", - version: "1.0.0", - dependencies, - trustedDependencies: dependenciesList, - }), - ); - - return dependenciesList; - } - - test("reach max concurrent scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const scripts = { - "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, - }; - - const dependenciesList = await createPackagesWithScripts(4, scripts); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--concurrent-scripts=2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`), - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("stress test", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dependenciesList = await createPackagesWithScripts(500, { - "postinstall": `${bunExe()} --version`, - }); - - // the script is quick, default number for max concurrent scripts - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), - "", - "500 packages installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install and use correct binary version", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - // this should install `what-bin` in two places: - // - // - node_modules/.bin/what-bin@1.5.0 - // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.0.0", - "what-bin": "1.5.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "+ what-bin@1.5.0", - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.5.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.0.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.5.0", - "what-bin": "1.0.0", - }, - scripts: { - install: "what-bin", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.0.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.5.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - out = await new Response(stdout).text(); - err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("node-gyp should always be available for lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "node-gyp --version", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - - // if node-gyp isn't available, it would return a non-zero exit code - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - // if this test fails, `electron` might be removed from the default list - test("default trusted dependencies should work", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(out).not.toContain("Blocked"); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - // fake electron package because it's in the default trustedDependencies list - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - // electron lifecycle scripts should run, uses-what-bin scripts should not run - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - // now uses-what-bin scripts should run and electron scripts should not run. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("does not run any scripts if trustedDependencies is an empty list", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("will run default trustedDependencies after install that didn't include them", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - trustedDependencies: ["blah"], - }), - ); - - // first install does not run electron scripts - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - }), - ); - - // The electron scripts should run now because it's in default trusted dependencies. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - - describe("--trust", async () => { - test("unhoisted untrusted scripts, none at root node_modules", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // prevents real `uses-what-bin` from hoisting to root - "uses-what-bin": "npm:a-dep@1.0.3", - }, - workspaces: ["pkg1"], - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ), - ]); - - await runBunInstall(testEnv, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ]); - - expect(results).toEqual([true, false]); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - - expect(await exited).toBe(0); - - expect( - await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ).toBeTrue(); - }); - const trustTests = [ - { - label: "only name", - packageJson: { - name: "foo", - }, - }, - { - label: "empty dependencies", - packageJson: { - name: "foo", - dependencies: {}, - }, - }, - { - label: "populated dependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }, - }, - - { - label: "empty trustedDependencies", - packageJson: { - name: "foo", - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies, empty trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }, - }, - - { - label: "empty dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: {}, - trustedDependencies: [], - }, - }, - ]; - for (const { label, packageJson } of trustTests) { - test(label, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed uses-what-bin@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(join(packageDir, "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // another install should not error with json SyntaxError - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - }); - } - describe("packages without lifecycle scripts", async () => { - test("initial install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }); - }); - test("already installed", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - - // oops, I wanted to run the lifecycle scripts for no-deps, I'll install - // again with --trust. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - // oh, I didn't realize no-deps doesn't have - // any lifecycle scripts. It shouldn't automatically add to - // trustedDependencies. - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - }); - }); - }); - - describe("updating trustedDependencies", async () => { - test("existing trustedDependencies, unchanged trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // no changes, lockfile shouldn't be saved - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("existing trustedDependencies, removing trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - // this script should not run because uses-what-bin is no longer in trustedDependencies - await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - }); - - test("non-existent trustedDependencies, then adding it", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["electron"], - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); - - // lockfile should save evenn though there are no changes to trustedDependencies due to - // the default list - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - }); - - test("node -p should work in postinstall scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - }); - - test("ensureTempNodeGypScript works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: "node-gyp --version", - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "ignore", - env, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("bun pm trust and untrusted on missing package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.5.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package - await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("bun pm untrusted"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("Found 0 untrusted dependencies with scripts"); - expect(await exited).toBe(0); - - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(1); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("bun pm trust"); - expect(err).toContain("0 scripts ran"); - expect(err).toContain("uses-what-bin"); - }); - - describe("add trusted, delete, then add again", async () => { - // when we change bun install to delete dependencies from node_modules - // for both cases, we need to update this test - for (const withRm of [true, false]) { - test(withRm ? "withRm" : "withoutRm", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 script ran across 1 package"); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // now remove and install again - if (withRm) { - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "rm", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 package removed"); - expect(out).toContain("uses-what-bin"); - expect(await exited).toBe(0); - } - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - let expected = withRm - ? ["", "Checked 1 install across 2 packages (no changes)"] - : ["", expect.stringContaining("1 package removed")]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); - - // add again, bun pm untrusted should report it as untrusted - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expected = withRm - ? [ - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ] - : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); - expect(await exited).toBe(0); - }); - } - }); - - describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { - test("install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, - }, - }), - ); - - const proc = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - stdin: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); - }); - - // https://github.com/oven-sh/bun/issues/11252 - test.todoIf(isWindows)("bun pm trust", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - [dep]: "1.0.0", - }, - }), - ); - - var { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); - - const proc = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); - - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); - }); - }); - }); - - describe("stdout/stderr is inherited from root scripts during install", async () => { - test("without packages", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "No packages! Deleted empty lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("with a package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "Resolving dependencies", - expect.stringContaining("Resolved, downloaded and extracted "), - "Saved lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - }); -} - describe("pm trust", async () => { test("--default", async () => { await writeFile( @@ -13086,7 +9560,7 @@ it("$npm_command is accurate during publish", async () => { }), ); await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_command")); - await rm(join(import.meta.dir, "packages", "publish-pkg-10"), { recursive: true, force: true }); + await rm(join(verdaccio.packagesPath, "publish-pkg-10"), { recursive: true, force: true }); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo $npm_command\n`); expect(out.split("\n")).toEqual([ @@ -13125,7 +9599,7 @@ it("$npm_lifecycle_event is accurate during publish", async () => { `, ); await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_lifecycle_event")); - await rm(join(import.meta.dir, "packages", "publish-pkg-11"), { recursive: true, force: true }); + await rm(join(verdaccio.packagesPath, "publish-pkg-11"), { recursive: true, force: true }); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); expect(out.split("\n")).toEqual([ diff --git a/test/cli/install/bun-install-retry.test.ts b/test/cli/install/bun-install-retry.test.ts index 842691bb0b..cbba8e2b37 100644 --- a/test/cli/install/bun-install-retry.test.ts +++ b/test/cli/install/bun-install-retry.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 70addfbf37..fb4a1d7c40 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -23,6 +23,7 @@ import { runBunInstall, isWindows, textLockfile, + readdirSorted, } from "harness"; import { join, sep, resolve } from "path"; import { @@ -32,7 +33,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 20d2be6439..68f5160faa 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -1,16 +1,18 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, mkdir, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toBeValidBin, toHaveBins, stderrForInstall } from "harness"; -import { basename, join } from "path"; import { - dummyAfterAll, - dummyAfterEach, - dummyBeforeAll, - dummyBeforeEach, - package_dir, + bunExe, + bunEnv as env, + runBunInstall, + tmpdirSync, + toBeValidBin, + toHaveBins, + stderrForInstall, readdirSorted, -} from "./dummy.registry"; +} from "harness"; +import { basename, join } from "path"; +import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry"; beforeAll(dummyBeforeAll); afterAll(dummyAfterAll); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index d1a6042f96..8a49dcaf16 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { exists, mkdir, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { bunEnv, bunExe, bunEnv as env, tmpdirSync, readdirSorted } from "harness"; import { cpSync } from "node:fs"; import { join } from "path"; import { @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 99293d6013..3c6416479c 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -1,9 +1,17 @@ import { file, spawn, spawnSync } from "bun"; import { beforeEach, describe, expect, it } from "bun:test"; import { exists, mkdir, rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, isWindows, tempDirWithFiles, tmpdirSync, stderrForInstall } from "harness"; +import { + bunEnv, + bunExe, + bunEnv as env, + isWindows, + tempDirWithFiles, + tmpdirSync, + stderrForInstall, + readdirSorted, +} from "harness"; import { join } from "path"; -import { readdirSorted } from "./dummy.registry"; let run_dir: string; diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 2ecbbb9daa..e81f65184a 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, readFile, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, toBeValidBin, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 7cf4d49d37..28fed1a1a1 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1,31 +1,41 @@ -import { file, write } from "bun"; +import { file, write, spawn } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; -import { beforeEach, describe, expect, test } from "bun:test"; +import { beforeEach, describe, expect, test, beforeAll, afterAll } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; -import { cp } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toMatchNodeModulesAt } from "harness"; +import { cp, mkdir, rm, exists } from "fs/promises"; +import { + bunExe, + bunEnv as env, + runBunInstall, + toMatchNodeModulesAt, + assertManifestsPopulated, + VerdaccioRegistry, + readdirSorted, +} from "harness"; import { join } from "path"; const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); -var testCounter: number = 0; - // not necessary, but verdaccio will be added to this file in the near future -var port: number = 4873; -var packageDir: string; -beforeEach(() => { - packageDir = tmpdirSync(); +var verdaccio: VerdaccioRegistry; +var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + verdaccio = new VerdaccioRegistry(); + await verdaccio.start(); +}); + +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - writeFileSync( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = false -`, - ); }); test("dependency on workspace without version in package.json", async () => { @@ -41,7 +51,7 @@ test("dependency on workspace without version in package.json", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", }), ), ]); @@ -60,7 +70,7 @@ test("dependency on workspace without version in package.json", async () => { "1", "1.*", "1.1.*", - "1.1.1", + "1.1.0", "*-pre+build", "*+build", "latest", // dist-tag exists, should choose package from npm @@ -74,7 +84,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -82,7 +92,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -101,7 +113,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -109,7 +121,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -134,7 +148,7 @@ test("dependency on same name as workspace and dist-tag", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", version: "4.17.21", }), ), @@ -145,7 +159,7 @@ test("dependency on same name as workspace and dist-tag", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: "latest", + "no-deps": "latest", }, }), ), @@ -153,7 +167,9 @@ test("dependency on same name as workspace and dist-tag", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchSnapshot("with version"); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot("with version"); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), @@ -658,3 +674,611 @@ test("$npm_package_config_ works in root in subpackage", async () => { expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo $npm_package_config_qux\n`); expect(await new Response(p.stdout).text()).toBe(`tab\n`); }); + +test("adding packages in a subdirectory of a workspace", async () => { + await write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["foo"], + }), + ); + + await mkdir(join(packageDir, "folder1")); + await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); + await write( + join(packageDir, "foo", "package.json"), + JSON.stringify({ + name: "foo", + }), + ); + + // add package to root workspace from `folder1` + let { stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + }); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "root", + workspaces: ["foo"], + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // add package to foo from `folder2` + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "what-bin"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed what-bin@1.5.0 with binaries:", + " - what-bin", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "what-bin": "^1.5.0", + }, + }); + + // now delete node_modules and bun.lockb and install + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); +}); +test("adding packages in workspaces", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); + await mkdir(join(packageDir, "packages", "boba")); + await mkdir(join(packageDir, "packages", "pkg5")); + + await write(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); + await write( + join(packageDir, "packages", "boba", "package.json"), + JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), + ); + await write( + join(packageDir, "packages", "pkg5", "package.json"), + JSON.stringify({ + name: "pkg5", + version: "1.2.3", + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + let { stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + }); + + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:packages/bar", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); + + // add a package to the root workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + bar: "workspace:*", + "no-deps": "^2.0.0", + }, + }); + + // add a package in a workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "two-range-deps"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed two-range-deps@1.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + + // add a dependency to a workspace with the same name as another workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "bar@0.0.7"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed bar@0.0.7", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + "bar": "0.0.7", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.7", + description: "not a workspace", + }); +}); +test("it should detect duplicate workspace dependencies", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); + + var { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); +}); + +const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; + +for (const rootVersion of versions) { + for (const packageVersion of versions) { + test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + pkg2: rootVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + pkg2: packageVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ name: "pkg2", version: "1.0.0" }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + } +} + +for (const version of versions) { + test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "workspace-1": version, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); + await write( + join(packageDir, "packages", "workspace-1", "package.json"), + JSON.stringify({ + name: "workspace-1", + version: "1.0.0", + }), + ); + // install first from the root, the workspace package + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ workspace-1@workspace:packages/workspace-1`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + // install from workspace package then from root + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + }); +} diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 87a26b0c7b..81efa8318a 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -1,11 +1,10 @@ import { spawn } from "bun"; import { beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isWindows, tmpdirSync, readdirSorted } from "harness"; import { readdirSync } from "node:fs"; import { tmpdir } from "os"; import { join, resolve } from "path"; -import { readdirSorted } from "./dummy.registry"; let x_dir: string; let current_tmpdir: string; diff --git a/test/cli/install/dummy.registry.ts b/test/cli/install/dummy.registry.ts index 060b50a0fe..f83f719542 100644 --- a/test/cli/install/dummy.registry.ts +++ b/test/cli/install/dummy.registry.ts @@ -87,12 +87,6 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe return _handler; } -export async function readdirSorted(path: PathLike): Promise { - const results = await readdir(path); - results.sort(); - return results; -} - export function setHandler(newHandler: Handler) { handler = newHandler; } diff --git a/test/cli/install/registry/missing-directory-bin-1.1.1.tgz b/test/cli/install/missing-directory-bin-1.1.1.tgz similarity index 100% rename from test/cli/install/registry/missing-directory-bin-1.1.1.tgz rename to test/cli/install/missing-directory-bin-1.1.1.tgz diff --git a/test/harness.ts b/test/harness.ts index bbdc48006f..d83a0372cc 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,8 +1,9 @@ -import { gc as bunGC, sleepSync, spawnSync, unsafe, which } from "bun"; +import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { heapStats } from "bun:jsc"; +import { fork, ChildProcess } from "child_process"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { readFile, readlink, writeFile } from "fs/promises"; -import fs, { closeSync, openSync } from "node:fs"; +import { readFile, readlink, writeFile, readdir, rm } from "fs/promises"; +import fs, { closeSync, openSync, rmSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; import detectLibc from "detect-libc"; @@ -1434,3 +1435,82 @@ export function textLockfile(version: number, pkgs: any): string { ...pkgs, }); } + +export class VerdaccioRegistry { + port: number; + process: ChildProcess | undefined; + configPath: string; + packagesPath: string; + + constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) { + this.port = randomPort(); + this.configPath = opts?.configPath ?? join(import.meta.dir, "cli", "install", "registry", "verdaccio.yaml"); + this.packagesPath = opts?.packagesPath ?? join(import.meta.dir, "cli", "install", "registry", "packages"); + } + + async start(silent: boolean = true) { + await rm(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", `${this.port}`], { + silent, + // Prefer using a release build of Bun since it's faster + execPath: Bun.which("bun") || bunExe(), + }); + + this.process.stderr?.on("data", data => { + console.error(`[verdaccio] stderr: ${data}`); + }); + + const started = Promise.withResolvers(); + + this.process.on("error", error => { + console.error(`Failed to start verdaccio: ${error}`); + started.reject(error); + }); + + this.process.on("exit", (code, signal) => { + if (code !== 0) { + console.error(`Verdaccio exited with code ${code} and signal ${signal}`); + } else { + console.log("Verdaccio exited successfully"); + } + }); + + this.process.on("message", (message: { verdaccio_started: boolean }) => { + if (message.verdaccio_started) { + started.resolve(); + } + }); + + await started.promise; + } + + registryUrl() { + return `http://localhost:${this.port}/`; + } + + stop() { + rmSync(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process?.kill(); + } + + async createTestDir() { + const packageDir = tmpdirSync(); + const packageJson = join(packageDir, "package.json"); + await write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = "${join(packageDir, ".bun-cache")}" + registry = "${this.registryUrl()}" + `, + ); + + return { packageDir, packageJson }; + } +} + +export async function readdirSorted(path: string): Promise { + const results = await readdir(path); + results.sort(); + return results; +} diff --git a/test/regression/issue/08093.test.ts b/test/regression/issue/08093.test.ts index 280d0ec4df..4d32dab6b7 100644 --- a/test/regression/issue/08093.test.ts +++ b/test/regression/issue/08093.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, From 912a2cbc120445706ce5cde6a7f32a66627448ac Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 3 Jan 2025 13:57:46 -0800 Subject: [PATCH 16/56] Expose some no-ops (#16125) Co-authored-by: Jarred-Sumner --- bench/snippets/native-overhead.mjs | 8 +---- src/bun.js/bindings/NoOpForTesting.cpp | 47 +++++++++++++++++++++++++ src/bun.js/bindings/NoOpForTesting.h | 3 ++ src/bun.js/bindings/ZigGlobalObject.cpp | 24 ------------- src/js/internal-for-testing.ts | 2 ++ 5 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 src/bun.js/bindings/NoOpForTesting.cpp create mode 100644 src/bun.js/bindings/NoOpForTesting.h diff --git a/bench/snippets/native-overhead.mjs b/bench/snippets/native-overhead.mjs index 32d459247e..43576b21d4 100644 --- a/bench/snippets/native-overhead.mjs +++ b/bench/snippets/native-overhead.mjs @@ -1,20 +1,14 @@ +import { noOpForTesting as noop } from "bun:internal-for-testing"; import { bench, run } from "../runner.mjs"; // These are no-op C++ functions that are exported to JS. -const lazy = globalThis[Symbol.for("Bun.lazy")]; -const noop = lazy("noop"); const fn = noop.function; -const regular = noop.functionRegular; const callback = noop.callback; bench("C++ callback into JS", () => { callback(() => {}); }); -bench("C++ fn regular", () => { - regular(); -}); - bench("C++ fn", () => { fn(); }); diff --git a/src/bun.js/bindings/NoOpForTesting.cpp b/src/bun.js/bindings/NoOpForTesting.cpp new file mode 100644 index 0000000000..919cd5b5f8 --- /dev/null +++ b/src/bun.js/bindings/NoOpForTesting.cpp @@ -0,0 +1,47 @@ + + +#include "root.h" + +#include "JavaScriptCore/CustomGetterSetter.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/JSObject.h" +#include + +namespace Bun { +using namespace JSC; + +JSC_DEFINE_HOST_FUNCTION(functionNoop, (JSC::JSGlobalObject*, JSC::CallFrame*)) +{ + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(functionCallback, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSObject* callback = jsCast(callFrame->uncheckedArgument(0)); + JSC::CallData callData = JSC::getCallData(callback); + return JSC::JSValue::encode(JSC::profiledCall(globalObject, ProfilingReason::API, callback, callData, JSC::jsUndefined(), JSC::MarkedArgumentBuffer())); +} + +JSC_DEFINE_CUSTOM_GETTER(noop_getter, (JSGlobalObject*, EncodedJSValue, PropertyName)) +{ + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(noop_setter, + (JSC::JSGlobalObject*, JSC::EncodedJSValue, + JSC::EncodedJSValue, JSC::PropertyName)) +{ + return true; +} + +JSC::JSObject* createNoOpForTesting(JSC::JSGlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + JSC::JSObject* object = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, String("function"_s)), 0, functionNoop, ImplementationVisibility::Public, JSC::NoIntrinsic, 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, String("callback"_s)), 0, functionCallback, ImplementationVisibility::Public, JSC::NoIntrinsic, 0); + object->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, String("getterSetter"_s)), JSC::CustomGetterSetter::create(vm, noop_getter, noop_setter), 0); + return object; +} + +} diff --git a/src/bun.js/bindings/NoOpForTesting.h b/src/bun.js/bindings/NoOpForTesting.h new file mode 100644 index 0000000000..e39e84daa8 --- /dev/null +++ b/src/bun.js/bindings/NoOpForTesting.h @@ -0,0 +1,3 @@ +namespace Bun { +JSC::JSObject* createNoOpForTesting(JSC::JSGlobalObject* globalObject); +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index e718a28527..1889b0c9be 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1883,30 +1883,6 @@ JSC_DEFINE_HOST_FUNCTION(functionCreateUninitializedArrayBuffer, RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSArrayBuffer::create(globalObject->vm(), globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTFMove(arrayBuffer)))); } -JSC_DEFINE_HOST_FUNCTION(functionNoop, (JSC::JSGlobalObject*, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_HOST_FUNCTION(functionCallback, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - JSFunction* callback = jsCast(callFrame->uncheckedArgument(0)); - JSC::CallData callData = JSC::getCallData(callback); - return JSC::JSValue::encode(JSC::profiledCall(globalObject, ProfilingReason::API, callback, callData, JSC::jsUndefined(), JSC::MarkedArgumentBuffer())); -} - -JSC_DEFINE_CUSTOM_GETTER(noop_getter, (JSGlobalObject*, EncodedJSValue, PropertyName)) -{ - return JSC::JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_CUSTOM_SETTER(noop_setter, - (JSC::JSGlobalObject*, JSC::EncodedJSValue, - JSC::EncodedJSValue, JSC::PropertyName)) -{ - return true; -} - static inline JSC::EncodedJSValue jsFunctionAddEventListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, Zig::GlobalObject* castedThis) { auto& vm = JSC::getVM(lexicalGlobalObject); diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 0cfaa5507e..893ff59006 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -149,3 +149,5 @@ export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as { add: (a: any, b: any) => number; requiredAndOptionalArg: (a: any, b?: any, c?: any, d?: any) => number; }; + +export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting"); From c713c0319b1120dba502a108ce694416060e30e9 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:16:52 -0800 Subject: [PATCH 17/56] fix(install): extra quotes in `bun.lock` (#16139) --- src/install/bun.lock.zig | 2 +- test/cli/install/bun-install-registry.test.ts | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/install/bun.lock.zig b/src/install/bun.lock.zig index 46db80c961..48b6290a22 100644 --- a/src/install/bun.lock.zig +++ b/src/install/bun.lock.zig @@ -958,7 +958,7 @@ pub const Stringifier = struct { for (optional_peers_buf.items) |optional_peer| { try writeIndent(writer, indent); try writer.print( - \\"{s}", + \\{}, \\ , .{ bun.fmt.formatJSONStringUTF8(optional_peer.slice(buf), .{}), diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 87b5ae2730..b010c41f63 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1856,6 +1856,67 @@ describe("text lockfile", () => { ); }); } + + test("optionalPeers", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + peerDependencies: { + "no-deps": "1.0.0", + }, + peerDependenciesMeta: { + "no-deps": { + optional: true, + }, + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // another install should recognize the peer dependency as `"optional": true` + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); + }); }); describe("bundledDependencies", () => { From 78498b42440a7f5f5f8a44d6d2edcb75890ceee1 Mon Sep 17 00:00:00 2001 From: 190n Date: Fri, 3 Jan 2025 17:33:17 -0800 Subject: [PATCH 18/56] Include array length and promise status in V8 heap snapshots (oven-sh/WebKit#75) (#16141) --- cmake/tools/SetupWebKit.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 19a59b77c5..22e95e7742 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION c30d63fe69913f17dce5fcbe17d06e670e0eaeff) + set(WEBKIT_VERSION e1a802a2287edfe7f4046a9dd8307c8b59f5d816) endif() if(WEBKIT_LOCAL) From fd9d9242d89a668d155c4191aec5d0e6fb770263 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 3 Jan 2025 17:54:07 -0800 Subject: [PATCH 19/56] Support absolute paths when bundling HTML (#16149) --- src/HTMLScanner.zig | 12 +++++-- test/bundler/bundler_html.test.ts | 56 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/HTMLScanner.zig b/src/HTMLScanner.zig index c6462882f9..dd69942977 100644 --- a/src/HTMLScanner.zig +++ b/src/HTMLScanner.zig @@ -28,12 +28,20 @@ pub fn deinit(this: *HTMLScanner) void { this.import_records.deinitWithAllocator(this.allocator); } -fn createImportRecord(this: *HTMLScanner, path: []const u8, kind: ImportKind) !void { +fn createImportRecord(this: *HTMLScanner, input_path: []const u8, kind: ImportKind) !void { + // In HTML, sometimes people do /src/index.js + // In that case, we don't want to use the absolute filesystem path, we want to use the path relative to the project root + const path_to_use = if (input_path.len > 1 and input_path[0] == '/') + bun.path.joinAbsString(bun.fs.FileSystem.instance.top_level_dir, &[_][]const u8{input_path[1..]}, .auto) + else + input_path; + const record = ImportRecord{ - .path = fs.Path.init(try this.allocator.dupe(u8, path)), + .path = fs.Path.init(try this.allocator.dupeZ(u8, path_to_use)), .kind = kind, .range = logger.Range.None, }; + try this.import_records.push(this.allocator, record); } diff --git a/test/bundler/bundler_html.test.ts b/test/bundler/bundler_html.test.ts index 29ac02c90f..9778ee55ea 100644 --- a/test/bundler/bundler_html.test.ts +++ b/test/bundler/bundler_html.test.ts @@ -721,4 +721,60 @@ body { expect(cssBundle).toContain("box-sizing: border-box"); }, }); + + // Test absolute paths in HTML + itBundled("html/absolute-paths", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

Absolute Paths

+ + +`, + "/styles/main.css": "body { margin: 0; }", + "/scripts/app.js": "console.log('App loaded')", + "/images/logo.png": "fake image content", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check that absolute paths are handled correctly + const htmlBundle = api.readFile("out/index.html"); + + // CSS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/styles/main.css"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + + // JS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/scripts/app.js"); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + + // Image should be hashed + api.expectFile("out/index.html").not.toContain("/images/logo.png"); + api.expectFile("out/index.html").toMatch(/src=".*\.png"/); + + // Get the bundled files and verify their contents + const cssMatch = htmlBundle.match(/href="(.*\.css)"/); + const jsMatch = htmlBundle.match(/src="(.*\.js)"/); + const imgMatch = htmlBundle.match(/src="(.*\.png)"/); + + expect(cssMatch).not.toBeNull(); + expect(jsMatch).not.toBeNull(); + expect(imgMatch).not.toBeNull(); + + const cssBundle = api.readFile("out/" + cssMatch![1]); + const jsBundle = api.readFile("out/" + jsMatch![1]); + + expect(cssBundle).toContain("margin: 0"); + expect(jsBundle).toContain("App loaded"); + }, + }); }); From fa7376b0423cc1f88c72619007e3d198895f073b Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:55:40 -0800 Subject: [PATCH 20/56] add `bun install --lockfile-only` (#16143) --- docs/install/index.md | 8 +- docs/install/lockfile.md | 12 + src/install/install.zig | 387 ++++++++++-------- test/cli/install/bun-install-registry.test.ts | 59 ++- 4 files changed, 300 insertions(+), 166 deletions(-) diff --git a/docs/install/index.md b/docs/install/index.md index 8f412a05e9..379f6fcd50 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -62,12 +62,18 @@ To exclude dependency types from installing, use `--omit` with `dev`, `optional` $ bun install --omit=dev --omit=optional ``` -To perform a dry run (i.e. don't actually install anything): +To perform a dry run (i.e. don't actually install anything or update the lockfile): ```bash $ bun install --dry-run ``` +To generate a lockfile without install packages: + +```bash +$ bun install --lockfile-only +``` + To modify logging verbosity: ```bash diff --git a/docs/install/lockfile.md b/docs/install/lockfile.md index 01df57fe0f..72e11c8944 100644 --- a/docs/install/lockfile.md +++ b/docs/install/lockfile.md @@ -49,6 +49,18 @@ Packages, metadata for those packages, the hoisted install order, dependencies f It uses linear arrays for all data. [Packages](https://github.com/oven-sh/bun/blob/be03fc273a487ac402f19ad897778d74b6d72963/src/install/install.zig#L1825) are referenced by an auto-incrementing integer ID or a hash of the package name. Strings longer than 8 characters are de-duplicated. Prior to saving on disk, the lockfile is garbage-collected & made deterministic by walking the package tree and cloning the packages in dependency order. +#### Generate a lockfile without installing? + +To generate a lockfile without installing to `node_modules` you can use the `--lockfile-only` flag. The lockfile will always be saved to disk, even if it is up-to-date with the `package.json`(s) for your project. + +```bash +$ bun install --lockfile-only +``` + +{% callout %} +**Note** - using `--lockfile-only` will still populate the global install cache with registry metadata and git/tarball dependencies. +{% endcallout %} + #### Can I opt out? To install without creating a lockfile: diff --git a/src/install/install.zig b/src/install/install.zig index c7a92109a2..9379e3010d 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -7125,6 +7125,8 @@ pub const PackageManager = struct { save_text_lockfile: bool = false, + lockfile_only: bool = false, + pub const PublishConfig = struct { access: ?Access = null, tag: string = "", @@ -7547,6 +7549,8 @@ pub const PackageManager = struct { this.save_text_lockfile = save_text_lockfile; } + this.lockfile_only = cli.lockfile_only; + const disable_progress_bar = default_disable_progress_bar or cli.no_progress; if (cli.verbose) { @@ -9526,6 +9530,7 @@ pub const PackageManager = struct { clap.parseParam("--network-concurrency Maximum number of concurrent network requests (default 48)") catch unreachable, clap.parseParam("--save-text-lockfile Save a text-based lockfile") catch unreachable, clap.parseParam("--omit ... Exclude 'dev', 'optional', or 'peer' dependencies from install") catch unreachable, + clap.parseParam("--lockfile-only Generate a lockfile without installing dependencies") catch unreachable, clap.parseParam("-h, --help Print this help menu") catch unreachable, }; @@ -9655,6 +9660,8 @@ pub const PackageManager = struct { save_text_lockfile: ?bool = null, + lockfile_only: bool = false, + const PatchOpts = union(enum) { nothing: struct {}, patch: struct {}, @@ -9987,6 +9994,7 @@ pub const PackageManager = struct { cli.trusted = args.flag("--trust"); cli.no_summary = args.flag("--no-summary"); cli.ca = args.options("--ca"); + cli.lockfile_only = args.flag("--lockfile-only"); if (args.option("--cache-dir")) |cache_dir| { cli.cache_dir = cache_dir; @@ -14912,6 +14920,45 @@ pub const PackageManager = struct { const lockfile_before_install = manager.lockfile; + const save_format: Lockfile.LoadResult.LockfileFormat = if (manager.options.save_text_lockfile) + .text + else switch (load_result) { + .not_found => .binary, + .err => |err| err.format, + .ok => |ok| ok.format, + }; + + if (manager.options.lockfile_only) { + // save the lockfile and exit. make sure metahash is generated for binary lockfile + + manager.lockfile.meta_hash = try manager.lockfile.generateMetaHash( + PackageManager.verbose_install or manager.options.do.print_meta_hash_string, + packages_len_before_install, + ); + + try manager.saveLockfile(&load_result, save_format, had_any_diffs, lockfile_before_install, packages_len_before_install, log_level); + + if (manager.options.do.summary) { + // TODO(dylan-conway): packages aren't installed but we can still print + // added/removed/updated direct dependencies. + Output.pretty( + \\ + \\Saved {s} ({d} package{s}) + , .{ + switch (save_format) { + .text => "bun.lock", + .binary => "bun.lockb", + }, + manager.lockfile.packages.len, + if (manager.lockfile.packages.len == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("\n", .{}); + } + Output.flush(); + return; + } + var install_summary = PackageInstall.Summary{}; if (manager.options.do.install_packages) { install_summary = try manager.installPackages( @@ -14947,79 +14994,8 @@ pub const PackageManager = struct { // It's unnecessary work to re-save the lockfile if there are no changes if (manager.options.do.save_lockfile and (should_save_lockfile or manager.lockfile.isEmpty() or manager.options.enable.force_save_lockfile)) - save: { - if (manager.lockfile.isEmpty()) { - if (!manager.options.dry_run) delete: { - const delete_format = switch (load_result) { - .not_found => break :delete, - .err => |err| err.format, - .ok => |ok| ok.format, - }; - - std.fs.cwd().deleteFileZ(if (delete_format == .text) "bun.lock" else "bun.lockb") catch |err| brk: { - // we don't care - if (err == error.FileNotFound) { - if (had_any_diffs) break :save; - break :brk; - } - - if (log_level != .silent) Output.prettyErrorln("\nerror: {s} deleting empty lockfile", .{@errorName(err)}); - break :save; - }; - } - if (!manager.options.global) { - if (log_level != .silent) { - switch (manager.subcommand) { - .remove => Output.prettyErrorln("\npackage.json has no dependencies! Deleted empty lockfile", .{}), - else => Output.prettyErrorln("No packages! Deleted empty lockfile", .{}), - } - } - } - - break :save; - } - - var save_node: *Progress.Node = undefined; - - if (comptime log_level.showProgress()) { - manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; - save_node = manager.progress.start(ProgressStrings.save(), 0); - save_node.activate(); - - manager.progress.refresh(); - } - - const save_format: Lockfile.LoadResult.LockfileFormat = if (manager.options.save_text_lockfile) - .text - else switch (load_result) { - .not_found => .binary, - .err => |err| err.format, - .ok => |ok| ok.format, - }; - - manager.lockfile.saveToDisk(save_format, manager.options.log_level.isVerbose()); - - if (comptime Environment.allow_assert) { - if (load_result.loadedFromTextLockfile()) { - if (!try manager.lockfile.eql(lockfile_before_install, packages_len_before_install, manager.allocator)) { - Output.panic("Lockfile non-deterministic after saving", .{}); - } - } else { - if (manager.lockfile.hasMetaHashChanged(false, packages_len_before_install) catch false) { - Output.panic("Lockfile metahash non-deterministic after saving", .{}); - } - } - } - - if (comptime log_level.showProgress()) { - save_node.end(); - manager.progress.refresh(); - manager.progress.root.end(); - manager.progress = .{}; - } else if (comptime log_level != .silent) { - Output.prettyErrorln("Saved lockfile", .{}); - Output.flush(); - } + { + try manager.saveLockfile(&load_result, save_format, had_any_diffs, lockfile_before_install, packages_len_before_install, log_level); } if (needs_new_lockfile) { @@ -15070,94 +15046,8 @@ pub const PackageManager = struct { } } - var printed_timestamp = false; if (comptime log_level != .silent) { - if (manager.options.do.summary) { - var printer = Lockfile.Printer{ - .lockfile = manager.lockfile, - .options = manager.options, - .updates = manager.update_requests, - .successfully_installed = install_summary.successfully_installed, - }; - - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - try Lockfile.Printer.Tree.print(&printer, manager, Output.WriterType, Output.writer(), enable_ansi_colors, log_level); - }, - } - - if (!did_meta_hash_change) { - manager.summary.remove = 0; - manager.summary.add = 0; - manager.summary.update = 0; - } - - if (install_summary.success > 0) { - // it's confusing when it shows 3 packages and says it installed 1 - const pkgs_installed = @max( - install_summary.success, - @as( - u32, - @truncate(manager.update_requests.len), - ), - ); - Output.pretty("{d} package{s} installed ", .{ pkgs_installed, if (pkgs_installed == 1) "" else "s" }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - - if (manager.summary.remove > 0) { - Output.pretty("Removed: {d}\n", .{manager.summary.remove}); - } - } else if (manager.summary.remove > 0) { - if (manager.subcommand == .remove) { - for (manager.update_requests) |request| { - Output.prettyln("- {s}", .{request.name}); - } - } - - Output.pretty("{d} package{s} removed ", .{ manager.summary.remove, if (manager.summary.remove == 1) "" else "s" }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } else if (install_summary.skipped > 0 and install_summary.fail == 0 and manager.update_requests.len == 0) { - const count = @as(PackageID, @truncate(manager.lockfile.packages.len)); - if (count != install_summary.skipped) { - Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ - install_summary.skipped, - if (install_summary.skipped == 1) "" else "s", - count, - if (count == 1) "" else "s", - }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } else { - Output.pretty("Done! Checked {d} package{s} (no changes) ", .{ - install_summary.skipped, - if (install_summary.skipped == 1) "" else "s", - }); - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - printed_timestamp = true; - printBlockedPackagesInfo(install_summary, manager.options.global); - } - } - - if (install_summary.fail > 0) { - Output.prettyln("Failed to install {d} package{s}\n", .{ install_summary.fail, if (install_summary.fail == 1) "" else "s" }); - Output.flush(); - } - } - } - - if (comptime log_level != .silent) { - if (manager.options.do.summary) { - if (!printed_timestamp) { - Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); - Output.prettyln(" done", .{}); - printed_timestamp = true; - } - } + try manager.printInstallSummary(ctx, &install_summary, did_meta_hash_change, log_level); } if (install_summary.fail > 0) { @@ -15167,7 +15057,182 @@ pub const PackageManager = struct { Output.flush(); } - fn printBlockedPackagesInfo(summary: PackageInstall.Summary, global: bool) void { + fn printInstallSummary( + this: *PackageManager, + ctx: Command.Context, + install_summary: *const PackageInstall.Summary, + did_meta_hash_change: bool, + comptime log_level: Options.LogLevel, + ) !void { + var printed_timestamp = false; + if (this.options.do.summary) { + var printer = Lockfile.Printer{ + .lockfile = this.lockfile, + .options = this.options, + .updates = this.update_requests, + .successfully_installed = install_summary.successfully_installed, + }; + + switch (Output.enable_ansi_colors) { + inline else => |enable_ansi_colors| { + try Lockfile.Printer.Tree.print(&printer, this, Output.WriterType, Output.writer(), enable_ansi_colors, log_level); + }, + } + + if (!did_meta_hash_change) { + this.summary.remove = 0; + this.summary.add = 0; + this.summary.update = 0; + } + + if (install_summary.success > 0) { + // it's confusing when it shows 3 packages and says it installed 1 + const pkgs_installed = @max( + install_summary.success, + @as( + u32, + @truncate(this.update_requests.len), + ), + ); + Output.pretty("{d} package{s} installed ", .{ pkgs_installed, if (pkgs_installed == 1) "" else "s" }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + + if (this.summary.remove > 0) { + Output.pretty("Removed: {d}\n", .{this.summary.remove}); + } + } else if (this.summary.remove > 0) { + if (this.subcommand == .remove) { + for (this.update_requests) |request| { + Output.prettyln("- {s}", .{request.name}); + } + } + + Output.pretty("{d} package{s} removed ", .{ this.summary.remove, if (this.summary.remove == 1) "" else "s" }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } else if (install_summary.skipped > 0 and install_summary.fail == 0 and this.update_requests.len == 0) { + const count = @as(PackageID, @truncate(this.lockfile.packages.len)); + if (count != install_summary.skipped) { + Output.pretty("Checked {d} install{s} across {d} package{s} (no changes) ", .{ + install_summary.skipped, + if (install_summary.skipped == 1) "" else "s", + count, + if (count == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } else { + Output.pretty("Done! Checked {d} package{s} (no changes) ", .{ + install_summary.skipped, + if (install_summary.skipped == 1) "" else "s", + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + printed_timestamp = true; + printBlockedPackagesInfo(install_summary, this.options.global); + } + } + + if (install_summary.fail > 0) { + Output.prettyln("Failed to install {d} package{s}\n", .{ install_summary.fail, if (install_summary.fail == 1) "" else "s" }); + Output.flush(); + } + } + + if (this.options.do.summary) { + if (!printed_timestamp) { + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.prettyln(" done", .{}); + printed_timestamp = true; + } + } + } + + fn saveLockfile( + this: *PackageManager, + load_result: *const Lockfile.LoadResult, + save_format: Lockfile.LoadResult.LockfileFormat, + had_any_diffs: bool, + // TODO(dylan-conway): this and `packages_len_before_install` can most likely be deleted + // now that git dependnecies don't append to lockfile during installation. + lockfile_before_install: *const Lockfile, + packages_len_before_install: usize, + log_level: Options.LogLevel, + ) OOM!void { + if (this.lockfile.isEmpty()) { + if (!this.options.dry_run) delete: { + const delete_format = switch (load_result.*) { + .not_found => break :delete, + .err => |err| err.format, + .ok => |ok| ok.format, + }; + + std.fs.cwd().deleteFileZ(if (delete_format == .text) "bun.lock" else "bun.lockb") catch |err| brk: { + // we don't care + if (err == error.FileNotFound) { + if (had_any_diffs) return; + break :brk; + } + + if (log_level != .silent) { + Output.err(err, "failed to delete empty lockfile", .{}); + } + return; + }; + } + if (!this.options.global) { + if (log_level != .silent) { + switch (this.subcommand) { + .remove => Output.prettyErrorln("\npackage.json has no dependencies! Deleted empty lockfile", .{}), + else => Output.prettyErrorln("No packages! Deleted empty lockfile", .{}), + } + } + } + + return; + } + + var save_node: *Progress.Node = undefined; + + if (log_level.showProgress()) { + this.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; + save_node = this.progress.start(ProgressStrings.save(), 0); + save_node.activate(); + + this.progress.refresh(); + } + + this.lockfile.saveToDisk(save_format, this.options.log_level.isVerbose()); + + if (comptime Environment.allow_assert) { + if (load_result.* != .not_found) { + if (load_result.loadedFromTextLockfile()) { + if (!try this.lockfile.eql(lockfile_before_install, packages_len_before_install, this.allocator)) { + Output.panic("Lockfile non-deterministic after saving", .{}); + } + } else { + if (this.lockfile.hasMetaHashChanged(false, packages_len_before_install) catch false) { + Output.panic("Lockfile metahash non-deterministic after saving", .{}); + } + } + } + } + + if (log_level.showProgress()) { + save_node.end(); + this.progress.refresh(); + this.progress.root.end(); + this.progress = .{}; + } else if (log_level != .silent) { + Output.prettyErrorln("Saved lockfile", .{}); + Output.flush(); + } + } + + fn printBlockedPackagesInfo(summary: *const PackageInstall.Summary, global: bool) void { const packages_count = summary.packages_with_blocked_scripts.count(); var scripts_count: usize = 0; for (summary.packages_with_blocked_scripts.values()) |count| scripts_count += count; diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index b010c41f63..5ed692c776 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1,17 +1,14 @@ import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { afterAll, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout, test } from "bun:test"; -import { ChildProcess } from "child_process"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, mkdir, readlink, rm, writeFile } from "fs/promises"; import { assertManifestsPopulated, bunExe, bunEnv as env, - isLinux, isWindows, mergeWindowEnvs, - randomPort, runBunInstall, runBunUpdate, pack, @@ -28,7 +25,7 @@ import { readdirSorted, VerdaccioRegistry, } from "harness"; -import { join, resolve, sep } from "path"; +import { join, resolve } from "path"; const { parseLockfile } = install_test_helpers; const { iniInternals } = require("bun:internal-for-testing"); const { loadNpmrc } = iniInternals; @@ -1919,6 +1916,60 @@ describe("text lockfile", () => { }); }); +test("--lockfile-only", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "^1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "package1", + dependencies: { + "two-range-deps": "1.0.0", + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + // nothing changes with another --lockfile-only + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); +}); + describe("bundledDependencies", () => { for (const textLockfile of [true, false]) { test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) basic`, async () => { From 5caeeb95494270b07fb3ac1bfa2b465a2ddfe936 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sat, 4 Jan 2025 12:56:00 +1100 Subject: [PATCH 21/56] docs: contributing windows link be absolute to bun.sh (#16127) --- CONTRIBUTING.md | 2 +- packages/bun-vscode/example/bun.lock | 261 ++++++++++++++++++++------- 2 files changed, 198 insertions(+), 65 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de4ea94c58..e1341d8149 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts. -If you are using Windows, please refer to [this guide](/docs/project/building-windows) +If you are using Windows, please refer to [this guide](https://bun.sh/docs/project/building-windows) ## Install Dependencies diff --git a/packages/bun-vscode/example/bun.lock b/packages/bun-vscode/example/bun.lock index 8eb6bfd86d..9b42110d97 100644 --- a/packages/bun-vscode/example/bun.lock +++ b/packages/bun-vscode/example/bun.lock @@ -26,183 +26,316 @@ "mime", ], "packages": { - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.30.4", "", {}, "sha512-wFuuDR+O1OAE2GL0q68h1Ty00RE6Ihcixr55A6TU5RCvOUHnwJw9LGuDVg9NxDiAp7m/YJpa+UaOuLAz0ziyOQ=="], - "@types/bun": ["@types/bun@1.1.14", "", { "dependencies": { "bun-types": "1.1.37" } }, "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA=="], + + "@types/bun": ["@types/bun@1.1.13", "", { "dependencies": { "bun-types": "1.1.34" } }, "sha512-KmQxSBgVWCl6RSuerlLGZlIWfdxkKqat0nxN61+qu4y1KDn0Ll3j7v1Pl8GnaL3a/U6GGWVTJh75ap62kR1E8Q=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + "@types/ws": ["@types/ws@8.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA=="], + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "acorn": ["acorn@8.14.0", "", {}, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + "acorn-loose": ["acorn-loose@8.4.0", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - "browserslist": ["browserslist@4.24.2", "", { "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.1" } }, "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg=="], + + "axios": ["axios@1.7.7", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q=="], + + "body-parser": ["body-parser@1.20.1", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw=="], + + "browserslist": ["browserslist@4.24.2", "", { "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.1.37", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA=="], + + "bun-types": ["bun-types@1.1.34", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-br5QygTEL/TwB4uQOb96Ky22j4Gq2WxWH/8Oqv20fk5HagwKXo/akB+LiYgSfzexCt6kkcUaVm+bKiPl71xPvw=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g=="], - "call-bound": ["call-bound@1.0.2", "", { "dependencies": { "call-bind": "^1.0.8", "get-intrinsic": "^1.2.5" } }, "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001688", "", {}, "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA=="], + + "call-bind": ["call-bind@1.0.2", "", { "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" } }, "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001680", "", {}, "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA=="], + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="], + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "dunder-proto": ["dunder-proto@1.0.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.73", "", {}, "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg=="], - "elysia": ["elysia@0.6.24", "", { "dependencies": { "@sinclair/typebox": "^0.30.4", "fast-querystring": "^1.1.2", "memoirist": "0.1.4", "mergician": "^1.1.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeerDependencies": ["typescript"] }, "sha512-qaN8b816tSecNIsgNwFCMOMlayOaChme9i/VHxCRZyPTgtdAAnrYDZaUQfatyt1jcHUdkf3IT4ny5GuS7NB26w=="], - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.58", "", {}, "sha512-al2l4r+24ZFL7WzyPTlyD0fC33LLzvxqLCwurtBibVPghRGO9hSTl+tis8t1kD7biPiH/en4U0I7o/nQbYeoVA=="], + + "elysia": ["elysia@0.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.30.4", "fast-querystring": "^1.1.2", "memoirist": "0.1.4", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-LhdH476fotAQuEUpnLdn8fAzwo3ZmwHVrYzQhujo+x+OpmMXGMJXT7L7/Ct+b5wwR2txP5xCxI1A0suxhRxgIQ=="], + + "encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "enhanced-resolve": ["enhanced-resolve@5.17.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@1.5.4", "", {}, "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="], - "es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "express": ["express@4.18.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", "serve-static": "1.15.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "finalhandler": ["finalhandler@1.2.0", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "get-intrinsic": ["get-intrinsic@1.2.6", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.0.0" } }, "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA=="], + + "function-bind": ["function-bind@1.1.1", "", {}, "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="], + + "get-intrinsic": ["get-intrinsic@1.2.1", "", { "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw=="], + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has": ["has@1.0.3", "", { "dependencies": { "function-bind": "^1.1.1" } }, "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "has-proto": ["has-proto@1.0.1", "", {}, "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="], + + "has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "math-intrinsics": ["math-intrinsics@1.0.0", "", {}, "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA=="], + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "memoirist": ["memoirist@0.1.4", "", {}, "sha512-D6GbPSqO2nUVOmm7VZjJc5tC60pkOVUPzLwkKl1vCiYP+2b1cG8N9q1O3P0JmNM68u8vsgefPbxRUCSGxSXD+g=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.1", "", {}, "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "mergician": ["mergician@1.1.0", "", {}, "sha512-FXbxzU6BBhGkV8XtUr8Sk015ZRaAALviit8Lle6OEgd1udX8wlu6tBeUMLGQGdz1MfHpAVNNQkXowyDnJuhXpA=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - "mime": ["mime@3.0.0", "", {}, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "node-releases": ["node-releases@2.0.18", "", {}, "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="], + + "object-inspect": ["object-inspect@1.12.3", "", {}, "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "qs": ["qs@6.11.0", "", { "dependencies": { "side-channel": "^1.0.4" } }, "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "react": ["react@0.0.0-fec00a869", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", "scheduler": "0.0.0-fec00a869" } }, "sha512-FaS3ViFU4ag7cuhDHQgGK3DAdWaD8YFXzEbO/Qzz33Si7VEzRRdnyoegFwg7VkEKxR6CvCVP6revi9Tm3Gq+WQ=="], - "react-dom": ["react-dom@0.0.0-fec00a869", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", "scheduler": "0.0.0-fec00a869" }, "peerDependencies": { "react": "0.0.0-fec00a869" } }, "sha512-atB5i2HgCvbvhtGXq9oaX/BCL2AFZjnccougU8S9eulRFNQbNrfGNwIcj04PRo3XU1ZsBw5syL/5l596UaolKA=="], - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "react-refresh": ["react-refresh@0.0.0-f77c7b9d7", "", {}, "sha512-mErwv0xcQz2sYnCJPaQ93D23Irnrfo5c+wG2k2KAgWOvFfqXPQdIUZ1j9S+gKYQI2kqgd0fdTJchEJydqroyJw=="], + + "raw-body": ["raw-body@2.5.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig=="], + + "react": ["react@0.0.0-experimental-380f5d67-20241113", "", {}, "sha512-QquU1j1TmZR+KgGSFvWTlOuwLvGrA8ldUJean+gT0nYIhSJ1ZkdXJQFnFRWqxoc74C7SY1o4NMz0yJxpUBoQ2w=="], + + "react-dom": ["react-dom@0.0.0-experimental-380f5d67-20241113", "", { "dependencies": { "scheduler": "0.0.0-experimental-380f5d67-20241113" }, "peerDependencies": { "react": "0.0.0-experimental-380f5d67-20241113" } }, "sha512-1ok9k5rAF7YuTveNefkPOvZHHuh5RLnCc5DU7sT7IL3i2K+LZmlsbSdlylMevjt9OzovxWQdsk04Fd4GKVCBWg=="], + + "react-refresh": ["react-refresh@0.0.0-experimental-380f5d67-20241113", "", {}, "sha512-PwTxoYh02oTSdM2DLV8r3ZzHwObVDIsS05fxNcajIZe+/kIFTWThmXYJpGMljzjIs0wwScVkMONU6URTRPQvHA=="], + "react-server-dom-bun": ["react-server-dom-bun@0.0.0-experimental-603e6108-20241029", "", { "dependencies": { "neo-async": "^2.6.1" } }, "sha512-FfteCHlOgJSnDJRatgIkIU74jQQ9M1+fH2e6kfY9Sibu8FAWEUjgApKQPDfiXgjrkY7w0ITQu0b2FezC0eGzCw=="], - "react-server-dom-webpack": ["react-server-dom-webpack@0.0.0-experimental-feed8f3f9-20240118", "", { "dependencies": { "acorn-loose": "^8.3.0", "loose-envify": "^1.1.0", "neo-async": "^2.6.1" }, "peerDependencies": { "react": "0.0.0-experimental-feed8f3f9-20240118", "react-dom": "0.0.0-experimental-feed8f3f9-20240118", "webpack": "^5.59.0" } }, "sha512-9+gS3ydJF5aYwKkvfzN+DtHfICzvQ+gYGv+2MVZo65gDSit1wC0vwOd0YebHqJNC2JruND+nEyd7wQAYmVdAZA=="], + + "react-server-dom-webpack": ["react-server-dom-webpack@0.0.0-experimental-380f5d67-20241113", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "0.0.0-experimental-380f5d67-20241113", "react-dom": "0.0.0-experimental-380f5d67-20241113", "webpack": "^5.59.0" } }, "sha512-hUluisy+9Srvrju5yS+qBOIAX82E+MRYOmoTNbV0kUsTi964ZZFLBzuruASAyUbbP1OhtFl0DwBxYN+UT0yUFQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.0.0-fec00a869", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-0U25jnyBP6dRPYwaVW4WMYB0jJSYlrIHFmIuXv27X+KIHJr7vyE9gcFTqZ61NQTuxYLYepAHnUs4KgQEUDlI+g=="], + + "scheduler": ["scheduler@0.0.0-experimental-380f5d67-20241113", "", {}, "sha512-UtSmlBSHar7hQvCXiozfIryfUFCL58+mqjrZONnLD06xdTlfgLrTcI5gS3Xo/RnNhUziLPV0DsinpI3a+q7Yzg=="], + "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "send": ["send@0.18.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "serve-static": ["serve-static@1.15.0", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" } }, "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "side-channel": ["side-channel@1.0.4", "", { "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" } }, "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - "terser": ["terser@5.37.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" } }, "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA=="], + + "terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="], + "terser-webpack-plugin": ["terser-webpack-plugin@5.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "typescript": ["typescript@5.7.2", "", {}, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], - "webpack": ["webpack@5.97.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" } }, "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg=="], + + "webpack": ["webpack@5.96.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA=="], + "webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "send/mime": ["mime@1.6.0", "", {}, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], } } From 2043613a62896fa4d43e8cf19497f47a97638e31 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:39:41 -0800 Subject: [PATCH 22/56] support `bun install --filter ` (#16093) --- src/bun.js/api/glob.zig | 2 +- src/cli/outdated_command.zig | 36 +-- src/glob/GlobWalker.zig | 2 +- src/glob/ascii.zig | 32 +- src/install/bun.lock.zig | 13 +- src/install/install.zig | 83 ++++- src/install/lockfile.zig | 114 ++++++- src/install/migration.zig | 2 +- src/resolver/package_json.zig | 2 +- src/resolver/resolve_path.zig | 4 +- src/resolver/resolver.zig | 4 +- .../bun-install-registry.test.ts.snap | 7 + .../__snapshots__/bun-install.test.ts.snap | 4 +- .../__snapshots__/bun-lock.test.ts.snap | 22 +- test/cli/install/bun-install-registry.test.ts | 1 + test/cli/install/bun-lock.test.ts | 4 +- test/cli/install/bun-workspaces.test.ts | 292 ++++++++++++++++++ 17 files changed, 561 insertions(+), 63 deletions(-) diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index edf341ec6a..3ace3a87de 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -406,7 +406,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame var str = str_arg.toSlice(globalThis, arena.allocator()); defer str.deinit(); - if (this.is_ascii and isAllAscii(str.slice())) return JSC.JSValue.jsBoolean(globImpl.Ascii.match(this.pattern, str.slice())); + if (this.is_ascii and isAllAscii(str.slice())) return JSC.JSValue.jsBoolean(globImpl.Ascii.match(this.pattern, str.slice()).matches()); const codepoints = codepoints: { if (this.pattern_codepoints) |cp| break :codepoints cp.items[0..]; diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index caa67f7b38..af827fcbcb 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -18,6 +18,8 @@ const FileSystem = bun.fs.FileSystem; const path = bun.path; const glob = bun.glob; const Table = bun.fmt.Table; +const WorkspaceFilter = PackageManager.WorkspaceFilter; +const OOM = bun.OOM; pub const OutdatedCommand = struct { pub fn exec(ctx: Command.Context) !void { @@ -138,7 +140,7 @@ pub const OutdatedCommand = struct { original_cwd: string, manager: *PackageManager, filters: []const string, - ) error{OutOfMemory}![]const PackageID { + ) OOM![]const PackageID { const lockfile = manager.lockfile; const packages = lockfile.packages.slice(); const pkg_names = packages.items(.name); @@ -152,36 +154,10 @@ pub const OutdatedCommand = struct { } const converted_filters = converted_filters: { - const buf = try allocator.alloc(FilterType, filters.len); + const buf = try allocator.alloc(WorkspaceFilter, filters.len); + var path_buf: bun.PathBuffer = undefined; for (filters, buf) |filter, *converted| { - if ((filter.len == 1 and filter[0] == '*') or strings.eqlComptime(filter, "**")) { - converted.* = .all; - continue; - } - - const is_path = filter.len > 0 and filter[0] == '.'; - - const joined_filter = if (is_path) - strings.withoutTrailingSlash(path.joinAbsString(original_cwd, &[_]string{filter}, .posix)) - else - filter; - - if (joined_filter.len == 0) { - converted.* = FilterType.init(&.{}, is_path); - continue; - } - - const length = bun.simdutf.length.utf32.from.utf8.le(joined_filter); - const convert_buf = try allocator.alloc(u32, length); - - const convert_result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(joined_filter, convert_buf); - if (!convert_result.isSuccessful()) { - // nothing would match - converted.* = FilterType.init(&.{}, false); - continue; - } - - converted.* = FilterType.init(convert_buf[0..convert_result.count], is_path); + converted.* = try WorkspaceFilter.init(allocator, filter, original_cwd, &path_buf); } break :converted_filters buf; }; diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index f41b47f7a6..6498fbb7d4 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -1358,7 +1358,7 @@ pub fn GlobWalker_( return GlobAscii.match( pattern_component.patternSlice(this.pattern), filepath, - ); + ).matches(); } const codepoints = this.componentStringUnicode(pattern_component); return matchImpl( diff --git a/src/glob/ascii.zig b/src/glob/ascii.zig index 69413f9505..c2e4724cb1 100644 --- a/src/glob/ascii.zig +++ b/src/glob/ascii.zig @@ -181,6 +181,18 @@ pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) } } +pub const MatchResult = enum { + no_match, + match, + + negate_no_match, + negate_match, + + pub fn matches(this: MatchResult) bool { + return this == .match or this == .negate_match; + } +}; + /// This function checks returns a boolean value if the pathname `path` matches /// the pattern `glob`. /// @@ -208,7 +220,7 @@ pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) /// Multiple "!" characters negate the pattern multiple times. /// "\" /// Used to escape any of the special characters above. -pub fn match(glob: []const u8, path: []const u8) bool { +pub fn match(glob: []const u8, path: []const u8) MatchResult { // This algorithm is based on https://research.swtch.com/glob var state = State{}; // Store the state when we see an opening '{' brace in a stack. @@ -290,7 +302,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) { if (state.skipBraces(glob, false) == .Invalid) - return false; // invalid pattern! + return .no_match; // invalid pattern! } continue; @@ -321,7 +333,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { var low = glob[state.glob_index]; if (!unescape(&low, glob, &state.glob_index)) - return false; // Invalid pattern + return .no_match; // Invalid pattern state.glob_index += 1; // If there is a - and the following character is not ], @@ -332,7 +344,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { state.glob_index += 1; var h = glob[state.glob_index]; if (!unescape(&h, glob, &state.glob_index)) - return false; // Invalid pattern! + return .no_match; // Invalid pattern! state.glob_index += 1; break :blk h; } else low; @@ -342,7 +354,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { first = false; } if (state.glob_index >= glob.len) - return false; // Invalid pattern! + return .no_match; // Invalid pattern! state.glob_index += 1; if (is_match != class_negated) { state.path_index += 1; @@ -351,7 +363,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { }, '{' => if (state.path_index < path.len) { if (brace_stack.len >= brace_stack.stack.len) - return false; // Invalid pattern! Too many nested braces. + return .no_match; // Invalid pattern! Too many nested braces. // Push old state to the stack, and reset current state. state = brace_stack.push(&state); @@ -380,7 +392,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { var cc = c; // Match escaped characters as literals. if (!unescape(&cc, glob, &state.glob_index)) - return false; // Invalid pattern; + return .no_match; // Invalid pattern; const is_match = if (cc == '/') isSeparator(path[state.path_index]) @@ -416,7 +428,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { if (brace_stack.len > 0) { // If in braces, find next option and reset path to index where we saw the '{' switch (state.skipBraces(glob, true)) { - .Invalid => return false, + .Invalid => return .no_match, .Comma => { state.path_index = brace_stack.last().path_index; continue; @@ -440,10 +452,10 @@ pub fn match(glob: []const u8, path: []const u8) bool { } } - return negated; + return if (negated) .negate_match else .no_match; } - return !negated; + return if (!negated) .match else .negate_no_match; } inline fn isSeparator(c: u8) bool { diff --git a/src/install/bun.lock.zig b/src/install/bun.lock.zig index 48b6290a22..b34d21e009 100644 --- a/src/install/bun.lock.zig +++ b/src/install/bun.lock.zig @@ -875,6 +875,17 @@ pub const Stringifier = struct { // need a way to detect new/deleted workspaces if (pkg_id == 0) { try writer.writeAll("\"\": {"); + const root_name = pkg_names[0].slice(buf); + if (root_name.len > 0) { + try writer.writeByte('\n'); + try incIndent(writer, indent); + try writer.print("\"name\": {}", .{ + bun.fmt.formatJSONStringUTF8(root_name, .{}), + }); + + // TODO(dylan-conway) should we save version? + any = true; + } } else { try writer.print("{}: {{", .{ bun.fmt.formatJSONStringUTF8(res.slice(buf), .{}), @@ -1625,7 +1636,7 @@ pub fn parseIntoBinaryLockfile( } } - lockfile.hoist(log, .resolvable, {}) catch |err| { + lockfile.resolve(log) catch |err| { switch (err) { error.OutOfMemory => |oom| return oom, else => { diff --git a/src/install/install.zig b/src/install/install.zig index 9379e3010d..3e1f3e89d9 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2772,6 +2772,67 @@ pub const PackageManager = struct { last_reported_slow_lifecycle_script_at: u64 = 0, cached_tick_for_slow_lifecycle_script_logging: u64 = 0, + pub const WorkspaceFilter = union(enum) { + all, + name: []const u32, + path: []const u32, + + pub fn init(allocator: std.mem.Allocator, input: string, cwd: string, path_buf: []u8) OOM!WorkspaceFilter { + if ((input.len == 1 and input[0] == '*') or strings.eqlComptime(input, "**")) { + return .all; + } + + var remain = input; + + var prepend_negate = false; + while (remain.len > 0 and remain[0] == '!') { + prepend_negate = !prepend_negate; + remain = remain[1..]; + } + + const is_path = remain.len > 0 and remain[0] == '.'; + + const filter = if (is_path) + strings.withoutTrailingSlash(bun.path.joinAbsStringBuf(cwd, path_buf, &.{remain}, .posix)) + else + remain; + + if (filter.len == 0) { + // won't match anything + return .{ .path = &.{} }; + } + + // TODO(dylan-conway): finish encoding agnostic glob matcher so we don't + // need to convert + const len = bun.simdutf.length.utf32.from.utf8.le(filter) + @intFromBool(prepend_negate); + const buf = try allocator.alloc(u32, len); + + const result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(filter, buf[@intFromBool(prepend_negate)..]); + if (!result.isSuccessful()) { + // won't match anything + return .{ .path = &.{} }; + } + + if (prepend_negate) { + buf[0] = '!'; + } + + const pattern = buf[0..len]; + + return if (is_path) + .{ .path = pattern } + else + .{ .name = pattern }; + } + + pub fn deinit(this: WorkspaceFilter, allocator: std.mem.Allocator) void { + switch (this) { + .path, .name => |pattern| allocator.free(pattern), + .all => {}, + } + } + }; + pub fn reportSlowLifecycleScripts(this: *PackageManager, log_level: Options.LogLevel) void { if (log_level == .silent) return; if (bun.getRuntimeFeatureFlag("BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING")) { @@ -8559,6 +8620,7 @@ pub const PackageManager = struct { pub fn supportsWorkspaceFiltering(this: Subcommand) bool { return switch (this) { .outdated => true, + .install => true, // .pack => true, else => false, }; @@ -9366,7 +9428,7 @@ pub const PackageManager = struct { } else { // bun link lodash switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } } } @@ -9539,6 +9601,7 @@ pub const PackageManager = struct { clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, + clap.parseParam("--filter ... Install packages for the matching workspaces") catch unreachable, clap.parseParam(" ... ") catch unreachable, }); @@ -10468,7 +10531,7 @@ pub const PackageManager = struct { } switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } if (manager.options.patch_features == .patch) { @@ -10599,6 +10662,7 @@ pub const PackageManager = struct { fn updatePackageJSONAndInstallWithManager( manager: *PackageManager, ctx: Command.Context, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { var update_requests = UpdateRequest.Array.initCapacity(manager.allocator, 64) catch bun.outOfMemory(); @@ -10632,6 +10696,7 @@ pub const PackageManager = struct { ctx, updates, manager.subcommand, + original_cwd, log_level, ); } @@ -10641,6 +10706,7 @@ pub const PackageManager = struct { ctx: Command.Context, updates: []UpdateRequest, subcommand: Subcommand, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { if (manager.log.errors > 0) { @@ -10906,7 +10972,7 @@ pub const PackageManager = struct { break :brk .{ root_package_json.source.contents, root_package_json_path_buf[0..root_package_json_path.len :0] }; }; - try manager.installWithManager(ctx, root_package_json_source, log_level); + try manager.installWithManager(ctx, root_package_json_source, original_cwd, log_level); if (subcommand == .update or subcommand == .add or subcommand == .link) { for (updates) |request| { @@ -12175,7 +12241,7 @@ pub const PackageManager = struct { // TODO(dylan-conway): print `bun install ` or `bun add ` before logs from `init`. // and cleanup install/add subcommand usage - var manager, _ = try init(ctx, cli, .install); + var manager, const original_cwd = try init(ctx, cli, .install); // switch to `bun add ` if (subcommand == .add) { @@ -12185,7 +12251,7 @@ pub const PackageManager = struct { Output.flush(); } return try switch (manager.options.log_level) { - inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), }; } @@ -12203,7 +12269,7 @@ pub const PackageManager = struct { }; try switch (manager.options.log_level) { - inline else => |log_level| manager.installWithManager(ctx, package_json_contents, log_level), + inline else => |log_level| manager.installWithManager(ctx, package_json_contents, original_cwd, log_level), }; if (manager.any_failed_to_install) { @@ -13837,12 +13903,13 @@ pub const PackageManager = struct { pub fn installPackages( this: *PackageManager, ctx: Command.Context, + original_cwd: string, comptime log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { const original_trees = this.lockfile.buffers.trees; const original_tree_dep_ids = this.lockfile.buffers.hoisted_dependencies; - try this.lockfile.hoist(this.log, .filter, this); + try this.lockfile.filter(this.log, this, original_cwd); defer { this.lockfile.buffers.trees = original_trees; @@ -14306,6 +14373,7 @@ pub const PackageManager = struct { manager: *PackageManager, ctx: Command.Context, root_package_json_contents: string, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { @@ -14963,6 +15031,7 @@ pub const PackageManager = struct { if (manager.options.do.install_packages) { install_summary = try manager.installPackages( ctx, + original_cwd, log_level, ); } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index e0a0f54a4a..d57a66ae93 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -15,6 +15,7 @@ const C = bun.C; const JSAst = bun.JSAst; const TextLockfile = @import("./bun.lock.zig"); const OOM = bun.OOM; +const WorkspaceFilter = PackageManager.WorkspaceFilter; const JSLexer = bun.js_lexer; const logger = bun.logger; @@ -688,6 +689,8 @@ pub const Tree = struct { lockfile: *const Lockfile, manager: if (method == .filter) *const PackageManager else void, sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, + workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{} else {}, + path_buf: []u8, pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; @@ -744,6 +747,13 @@ pub const Tree = struct { this.queue.deinit(); this.sort_buf.deinit(this.allocator); + if (comptime method == .filter) { + for (this.workspace_filters) |workspace_filter| { + workspace_filter.deinit(this.lockfile.allocator); + } + this.lockfile.allocator.free(this.workspace_filters); + } + // take over the `builder.list` pointer for only trees if (@intFromPtr(trees.ptr) != @intFromPtr(list_ptr)) { var new: [*]Tree = @ptrCast(list_ptr); @@ -859,6 +869,74 @@ pub const Tree = struct { continue; } + + if (builder.manager.subcommand == .install) { + // only do this when parent is root. workspaces are always dependencies of the root + // package, and the root package is always called with `processSubtree` + if (parent_pkg_id == 0 and builder.workspace_filters.len > 0) { + var match = false; + + for (builder.workspace_filters) |workspace_filter| { + const res_id = builder.resolutions[dep_id]; + + const pattern, const path_or_name = switch (workspace_filter) { + .name => |pattern| .{ pattern, if (builder.dependencies[dep_id].behavior.isWorkspaceOnly()) + pkg_names[res_id].slice(builder.buf()) + else + pkg_names[0].slice(builder.buf()) }, + + .path => |pattern| path: { + const res_path = if (builder.dependencies[dep_id].behavior.isWorkspaceOnly() and pkg_resolutions[res_id].tag == .workspace) + pkg_resolutions[res_id].value.workspace.slice(builder.buf()) + else + // dependnecy of the root package.json. use top level dir + FileSystem.instance.top_level_dir; + + // occupy `builder.path_buf` + var abs_res_path = strings.withoutTrailingSlash(bun.path.joinAbsStringBuf( + FileSystem.instance.top_level_dir, + builder.path_buf, + &.{res_path}, + .auto, + )); + + if (comptime Environment.isWindows) { + abs_res_path = abs_res_path[Path.windowsVolumeNameLen(abs_res_path)[0]..]; + Path.dangerouslyConvertPathToPosixInPlace(u8, builder.path_buf[0..abs_res_path.len]); + } + + break :path .{ + pattern, + abs_res_path, + }; + }, + + .all => { + match = true; + continue; + }, + }; + + switch (bun.glob.walk.matchImpl(pattern, path_or_name)) { + .match, .negate_match => match = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + match = false; + break; + }, + + .no_match => { + // keep current + }, + } + } + + if (!match) { + continue; + } + } + } } const hoisted: HoistDependencyResult = hoisted: { @@ -1478,7 +1556,7 @@ const Cloner = struct { this.manager.clearCachedItemsDependingOnLockfileBuffer(); if (this.lockfile.packages.len != 0) { - try this.lockfile.hoist(this.log, .resolvable, {}); + try this.lockfile.resolve(this.log); } // capacity is used for calculating byte size @@ -1488,15 +1566,35 @@ const Cloner = struct { } }; +pub fn resolve( + lockfile: *Lockfile, + log: *logger.Log, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .resolvable, {}, {}); +} + +pub fn filter( + lockfile: *Lockfile, + log: *logger.Log, + manager: *PackageManager, + cwd: string, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .filter, manager, cwd); +} + /// Sets `buffers.trees` and `buffers.hoisted_dependencies` pub fn hoist( lockfile: *Lockfile, log: *logger.Log, comptime method: Tree.BuilderMethod, manager: if (method == .filter) *PackageManager else void, + cwd: if (method == .filter) string else void, ) Tree.SubtreeError!void { const allocator = lockfile.allocator; var slice = lockfile.packages.slice(); + + var path_buf: bun.PathBuffer = undefined; + var builder = Tree.Builder(method){ .name_hashes = slice.items(.name_hash), .queue = TreeFiller.init(allocator), @@ -1507,8 +1605,20 @@ pub fn hoist( .log = log, .lockfile = lockfile, .manager = manager, + .path_buf = &path_buf, }; + if (comptime method == .filter) { + if (manager.options.filter_patterns.len > 0) { + var filters = try std.ArrayListUnmanaged(WorkspaceFilter).initCapacity(allocator, manager.options.filter_patterns.len); + for (manager.options.filter_patterns) |pattern| { + try filters.append(allocator, try WorkspaceFilter.init(allocator, pattern, cwd, &path_buf)); + } + + builder.workspace_filters = filters.items; + } + } + try (Tree{}).processSubtree( Tree.root_dep_id, Tree.invalid_id, @@ -7125,7 +7235,7 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag return digest; } -pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { +pub fn resolvePackageFromNameAndVersion(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { const name_hash = String.Builder.stringHash(package_name); const entry = this.package_index.get(name_hash) orelse return null; const buf = this.buffers.string_bytes.items; diff --git a/src/install/migration.zig b/src/install/migration.zig index 1810598c74..d19d3e44cb 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -1017,7 +1017,7 @@ pub fn migrateNPMLockfile( return error.NotAllPackagesGotResolved; } - try this.hoist(log, .resolvable, {}); + try this.resolve(log); // if (Environment.isDebug) { // const dump_file = try std.fs.cwd().createFileZ("after-clean.json", .{}); diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index b801b6c11c..c3435c06cb 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -864,7 +864,7 @@ pub const PackageJSON = struct { pm, )) |dependency_version| { if (dependency_version.value.npm.version.isExact()) { - if (pm.lockfile.resolve(package_json.name, dependency_version)) |resolved| { + if (pm.lockfile.resolvePackageFromNameAndVersion(package_json.name, dependency_version)) |resolved| { package_json.package_manager_package_id = resolved; if (resolved > 0) { break :update_dependencies; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index f073394c1f..bf7a341467 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -583,10 +583,10 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // This function is based on Go's volumeNameLen function // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. -fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { +pub fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { return windowsVolumeNameLenT(u8, path); } -fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { +pub fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 3ef06e90ee..d737f4af3d 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1877,7 +1877,7 @@ pub const Resolver = struct { ) orelse break :load_module_from_cache; } - if (manager.lockfile.resolve(esm.name, dependency_version)) |id| { + if (manager.lockfile.resolvePackageFromNameAndVersion(esm.name, dependency_version)) |id| { resolved_package_id = id; } } @@ -2186,7 +2186,7 @@ pub const Resolver = struct { var pm = r.getPackageManager(); if (comptime Environment.allow_assert) { // we should never be trying to resolve a dependency that is already resolved - assert(pm.lockfile.resolve(esm.name, version) == null); + assert(pm.lockfile.resolvePackageFromNameAndVersion(esm.name, version) == null); } // Add the containing package to the lockfile diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 9d2bebfe0c..c7af63e377 100644 --- a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -140,6 +140,7 @@ exports[`text lockfile workspace sorting 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -173,6 +174,7 @@ exports[`text lockfile workspace sorting 2`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -214,6 +216,7 @@ exports[`text lockfile --frozen-lockfile 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "a-dep": "^1.0.2", "no-deps": "^1.0.0", @@ -244,6 +247,7 @@ exports[`binaries each type of binary serializes correctly to text lockfile 1`] "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "dir-bin": "./dir-bin", "file-bin": "./file-bin", @@ -270,6 +274,7 @@ exports[`binaries root resolution bins 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", "no-deps": "1.0.0", @@ -290,6 +295,7 @@ exports[`hoisting text lockfile is hoisted 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "hoist-lockfile-1": "1.0.0", "hoist-lockfile-2": "1.0.0", @@ -317,6 +323,7 @@ exports[`it should ignore peerDependencies within workspaces 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "peerDependencies": { "no-deps": ">=1.0.0", }, diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index 96d0b00550..593bd7b0dd 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -61,7 +61,9 @@ exports[`should read install.saveTextLockfile from bunfig.toml 1`] = ` "{ "lockfileVersion": 0, "workspaces": { - "": {}, + "": { + "name": "foo", + }, "packages/pkg1": { "name": "pkg-one", "version": "1.0.0", diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap index 8a80309993..4f15c6c30c 100644 --- a/test/cli/install/__snapshots__/bun-lock.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -4,7 +4,9 @@ exports[`should escape names 1`] = ` "{ "lockfileVersion": 0, "workspaces": { - "": {}, + "": { + "name": "quote-in-dependency-name", + }, "packages/\\"": { "name": "\\"", }, @@ -23,3 +25,21 @@ exports[`should escape names 1`] = ` } " `; + +exports[`should write plaintext lockfiles 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test-package", + "dependencies": { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }, + }, + "packages": { + "dummy-package": ["bar@./bar-0.0.2.tgz", {}], + } +} +" +`; diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5ed692c776..da074098f7 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -4025,6 +4025,7 @@ describe("binaries", () => { "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", // out of date, no no-deps diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 8c99857070..f60725be5f 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -45,9 +45,7 @@ it("should write plaintext lockfiles", async () => { } expect(stat.mode).toBe(mode); - expect(await file.readFile({ encoding: "utf8" })).toEqual( - `{\n \"lockfileVersion\": 0,\n \"workspaces\": {\n \"\": {\n \"dependencies\": {\n \"dummy-package\": \"file:./bar-0.0.2.tgz\",\n },\n },\n },\n \"packages\": {\n \"dummy-package\": [\"bar@./bar-0.0.2.tgz\", {}],\n }\n}\n`, - ); + expect(await file.readFile({ encoding: "utf8" })).toMatchSnapshot(); }); // won't work on windows, " is not a valid character in a filename diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 28fed1a1a1..b282e6c1be 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1282,3 +1282,295 @@ for (const version of versions) { }); }); } + +describe("install --filter", () => { + test("basic", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, false]); + + // add workspace + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, true]); + }); + + test("all but one or two", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg2", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, false]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // exclude the root by name + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!root"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([false, true, true, true]); + }); + + test("matched workspace depends on filtered workspace", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, true, true]); + }); + + test("filter with a path", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "path-pattern", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + async function checkRoot() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([true, false, false]); + } + + async function checkWorkspace() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([false, { name: "no-deps", version: "2.0.0" }, true]); + } + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + }); +}); From d5fc928ca8ae1c3db1a3ae1623d2f37e32db8c46 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 3 Jan 2025 19:11:48 -0800 Subject: [PATCH 23/56] S3 cleanup (#16039) Co-authored-by: Ciro Spaciari --- packages/bun-types/bun.d.ts | 96 ++- src/bun.js/ConsoleObject.zig | 7 +- src/bun.js/api/BunObject.zig | 7 +- src/bun.js/bindings/BunClientData.cpp | 3 + src/bun.js/bindings/BunClientData.h | 1 + src/bun.js/bindings/BunCommonStrings.h | 3 +- src/bun.js/bindings/BunObject+exports.h | 2 +- src/bun.js/bindings/BunObject.cpp | 3 +- src/bun.js/bindings/ErrorCode.ts | 13 +- src/bun.js/bindings/JSDOMFile.cpp | 10 +- src/bun.js/bindings/JSS3Bucket.cpp | 253 ++++++ src/bun.js/bindings/JSS3Bucket.h | 50 ++ src/bun.js/bindings/JSS3File.cpp | 244 ++++-- src/bun.js/bindings/JSS3File.h | 40 +- src/bun.js/bindings/S3Error.cpp | 63 ++ src/bun.js/bindings/S3Error.h | 7 + src/bun.js/bindings/Sink.h | 2 +- src/bun.js/bindings/ZigGlobalObject.cpp | 79 +- src/bun.js/bindings/ZigGlobalObject.h | 16 +- src/bun.js/bindings/bindings.cpp | 2 +- src/bun.js/bindings/bindings.zig | 42 + src/bun.js/bindings/exports.zig | 4 +- src/bun.js/bindings/headers.h | 30 +- src/bun.js/bindings/headers.zig | 14 +- .../bindings/webcore/DOMClientIsoSubspaces.h | 2 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 + src/bun.js/node/types.zig | 25 + src/bun.js/webcore/S3Bucket.zig | 267 ++++++ src/bun.js/webcore/S3File.zig | 545 +++++++++++++ src/bun.js/webcore/blob.zig | 660 +++------------ src/bun.js/webcore/response.classes.ts | 8 +- src/bun.js/webcore/response.zig | 43 +- src/bun.js/webcore/streams.zig | 24 +- src/bun.zig | 3 + src/codegen/class-definitions.ts | 2 + src/codegen/generate-classes.ts | 23 +- src/codegen/generate-jssink.ts | 8 +- src/env_loader.zig | 7 + src/s3.zig | 768 +++++++++++++----- test/js/bun/s3/s3.test.ts | 445 ++++++++-- 40 files changed, 2699 insertions(+), 1124 deletions(-) create mode 100644 src/bun.js/bindings/JSS3Bucket.cpp create mode 100644 src/bun.js/bindings/JSS3Bucket.h create mode 100644 src/bun.js/bindings/S3Error.cpp create mode 100644 src/bun.js/bindings/S3Error.h create mode 100644 src/bun.js/webcore/S3Bucket.zig create mode 100644 src/bun.js/webcore/S3File.zig diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c9690785a7..d4a8d30c26 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1226,8 +1226,56 @@ declare module "bun" { */ unlink(): Promise; } + interface NetworkSink extends FileSink { + /** + * Write a chunk of data to the network. + * + * If the network is not writable yet, the data is buffered. + */ + write(chunk: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer): number; + /** + * Flush the internal buffer, committing the data to the network. + */ + flush(): number | Promise; + /** + * Finish the upload. This also flushes the internal buffer. + */ + end(error?: Error): number | Promise; + } - interface S3FileOptions extends BlobPropertyBag { + interface S3Options extends BlobPropertyBag { + /** + * The ACL to used to write the file to S3. by default will omit the ACL header/parameter. + */ + acl?: /** + * Owner gets FULL_CONTROL. No one else has access rights (default). + */ + | "private" + /** + * Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. + */ + | "public-read" + /** + * Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. + */ + | "public-read-write" + /** + * Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. + */ + | "aws-exec-read" + /** + * Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. + */ + | "authenticated-read" + /** + * Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + */ + | "bucket-owner-read" + /** + * Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + */ + | "bucket-owner-full-control" + | "log-delivery-write"; /** * The bucket to use for the S3 client. by default will use the `S3_BUCKET` and `AWS_BUCKET` environment variable, or deduce as first part of the path. */ @@ -1244,6 +1292,10 @@ declare module "bun" { * The secret access key to use for the S3 client. By default, it will use the `S3_SECRET_ACCESS_KEY and `AWS_SECRET_ACCESS_KEY` environment variable. */ secretAccessKey?: string; + /** + * The session token to use for the S3 client. By default, it will use the `S3_SESSION_TOKEN` and `AWS_SESSION_TOKEN` environment variable. + */ + sessionToken?: string; /** * The endpoint to use for the S3 client. Defaults to `https://s3.{region}.amazonaws.com`, it will also use the `S3_ENDPOINT` and `AWS_ENDPOINT` environment variable. @@ -1274,7 +1326,7 @@ declare module "bun" { highWaterMark?: number; } - interface S3FilePresignOptions extends S3FileOptions { + interface S3FilePresignOptions extends S3Options { /** * The number of seconds the presigned URL will be valid for. Defaults to 86400 (1 day). */ @@ -1290,7 +1342,7 @@ declare module "bun" { * @param path - The path to the file. If bucket options is not provided or set in the path, it will be deduced from the path. * @param options - The options to use for the S3 client. */ - new (path: string | URL, options?: S3FileOptions): S3File; + new (path: string | URL, options?: S3Options): S3File; /** * The size of the file in bytes. */ @@ -1327,9 +1379,9 @@ declare module "bun" { slice(contentType?: string): S3File; /** - * Incremental writer to stream writes to S3, this is equivalent of using MultipartUpload and is suitable for large files. + * Incremental writer to stream writes to the network, this is equivalent of using MultipartUpload and is suitable for large files. */ - writer(options?: S3FileOptions): FileSink; + writer(options?: S3Options): NetworkSink; /** * The readable stream of the file. @@ -1364,7 +1416,7 @@ declare module "bun" { */ write( data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob, - options?: S3FileOptions, + options?: S3Options, ): Promise; /** @@ -1379,38 +1431,43 @@ declare module "bun" { unlink(): Promise; } - namespace S3File { + interface S3Bucket { /** - * Uploads the data to S3. + * Get a file from the bucket. + * @param path - The path to the file. + */ + (path: string, options?: S3Options): S3File; + /** + * Uploads the data to S3. This will overwrite the file if it already exists. * @param data - The data to write. * @param options - The options to use for the S3 client. */ - function upload( - path: string | S3File, + write( + path: string, data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File, - options?: S3FileOptions, + options?: S3Options, ): Promise; /** * Returns a presigned URL for the file. * @param options - The options to use for the presigned URL. */ - function presign(path: string | S3File, options?: S3FilePresignOptions): string; + presign(path: string, options?: S3FilePresignOptions): string; /** * Deletes the file from S3. */ - function unlink(path: string | S3File, options?: S3FileOptions): Promise; + unlink(path: string, options?: S3Options): Promise; /** * The size of the file in bytes. */ - function size(path: string | S3File, options?: S3FileOptions): Promise; + size(path: string, options?: S3Options): Promise; /** - * The size of the file in bytes. + * Does the file exist? */ - function exists(path: string | S3File, options?: S3FileOptions): Promise; + exists(path: string, options?: S3Options): Promise; } /** @@ -3268,11 +3325,12 @@ declare module "bun" { * @param path - The path to the file. If bucket options is not provided or set in the path, it will be deduced from the path. * @param options - The options to use for the S3 client. */ - function s3(path: string | URL, options?: S3FileOptions): S3File; + function s3(path: string | URL, options?: S3Options): S3File; /** - * The S3 file class. + * Create a configured S3 bucket reference. + * @param options - The options to use for the S3 client. */ - const S3: typeof S3File; + function S3(options?: S3Options): S3Bucket; /** * Allocate a new [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) without zeroing the bytes. diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 8996a196da..06b282dde1 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -21,12 +21,13 @@ const default_allocator = bun.default_allocator; const JestPrettyFormat = @import("./test/pretty_format.zig").JestPrettyFormat; const JSPromise = JSC.JSPromise; const EventType = JSC.EventType; - +const S3Bucket = @import("./webcore/S3Bucket.zig"); pub const shim = Shimmer("Bun", "ConsoleObject", @This()); pub const Type = *anyopaque; pub const name = "Bun::ConsoleObject"; pub const include = "\"ConsoleObject.h\""; pub const namespace = shim.namespace; + const Counter = std.AutoHashMapUnmanaged(u64, u32); const BufferedWriter = std.io.BufferedWriter(4096, Output.WriterType); @@ -2216,6 +2217,10 @@ pub const Formatter = struct { ); }, .Class => { + if (S3Bucket.fromJS(value)) |s3bucket| { + S3Bucket.writeFormat(s3bucket, ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; + return; + } var printable = ZigString.init(&name_buf); value.getClassName(this.globalThis, &printable); this.addForNewLine(printable.len); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index de110e5bda..8736bd7809 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1,5 +1,6 @@ const conv = std.builtin.CallingConvention.Unspecified; - +const S3File = @import("../webcore/S3File.zig"); +const S3Bucket = @import("../webcore/S3Bucket.zig"); /// How to add a new function or property to the Bun global /// /// - Add a callback or property to the below struct @@ -30,7 +31,7 @@ pub const BunObject = struct { pub const registerMacro = toJSCallback(Bun.registerMacro); pub const resolve = toJSCallback(Bun.resolve); pub const resolveSync = toJSCallback(Bun.resolveSync); - pub const s3 = toJSCallback(WebCore.Blob.constructS3File); + pub const s3 = S3File.createJSS3File; pub const serve = toJSCallback(Bun.serve); pub const sha = toJSCallback(JSC.wrapStaticMethod(Crypto.SHA512_256, "hash_", true)); pub const shellEscape = toJSCallback(Bun.shellEscape); @@ -56,7 +57,6 @@ pub const BunObject = struct { pub const SHA384 = toJSGetter(Crypto.SHA384.getter); pub const SHA512 = toJSGetter(Crypto.SHA512.getter); pub const SHA512_256 = toJSGetter(Crypto.SHA512_256.getter); - pub const S3 = toJSGetter(JSC.WebCore.Blob.getJSS3FileConstructor); pub const TOML = toJSGetter(Bun.getTOMLObject); pub const Transpiler = toJSGetter(Bun.getTranspilerConstructor); pub const argv = toJSGetter(Bun.getArgv); @@ -109,7 +109,6 @@ pub const BunObject = struct { @export(BunObject.FileSystemRouter, .{ .name = getterName("FileSystemRouter") }); @export(BunObject.MD4, .{ .name = getterName("MD4") }); @export(BunObject.MD5, .{ .name = getterName("MD5") }); - @export(BunObject.S3, .{ .name = getterName("S3") }); @export(BunObject.SHA1, .{ .name = getterName("SHA1") }); @export(BunObject.SHA224, .{ .name = getterName("SHA224") }); @export(BunObject.SHA256, .{ .name = getterName("SHA256") }); diff --git a/src/bun.js/bindings/BunClientData.cpp b/src/bun.js/bindings/BunClientData.cpp index b5c037f0c2..ed746e0e15 100644 --- a/src/bun.js/bindings/BunClientData.cpp +++ b/src/bun.js/bindings/BunClientData.cpp @@ -23,7 +23,9 @@ #include "JSDOMWrapper.h" #include #include "NodeVM.h" +#include "JSS3Bucket.h" #include "../../bake/BakeGlobalObject.h" + namespace WebCore { using namespace JSC; @@ -32,6 +34,7 @@ RefPtr createBuiltinsSourceProvider(); JSHeapData::JSHeapData(Heap& heap) : m_heapCellTypeForJSWorkerGlobalScope(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForNodeVMGlobalObject(JSC::IsoHeapCellType::Args()) + , m_heapCellTypeForJSS3Bucket(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForBakeGlobalObject(JSC::IsoHeapCellType::Args()) , m_domBuiltinConstructorSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMBuiltinConstructorBase) , m_domConstructorSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMConstructorBase) diff --git a/src/bun.js/bindings/BunClientData.h b/src/bun.js/bindings/BunClientData.h index ef210b02ad..953918f0eb 100644 --- a/src/bun.js/bindings/BunClientData.h +++ b/src/bun.js/bindings/BunClientData.h @@ -59,6 +59,7 @@ public: JSC::IsoHeapCellType m_heapCellTypeForJSWorkerGlobalScope; JSC::IsoHeapCellType m_heapCellTypeForNodeVMGlobalObject; + JSC::IsoHeapCellType m_heapCellTypeForJSS3Bucket; JSC::IsoHeapCellType m_heapCellTypeForBakeGlobalObject; private: diff --git a/src/bun.js/bindings/BunCommonStrings.h b/src/bun.js/bindings/BunCommonStrings.h index 0abd69c1db..b74b2e7be8 100644 --- a/src/bun.js/bindings/BunCommonStrings.h +++ b/src/bun.js/bindings/BunCommonStrings.h @@ -11,7 +11,8 @@ // These ones don't need to be in BunBuiltinNames.h // If we don't use it as an identifier name, but we want to avoid allocating the string frequently, put it in this list. #define BUN_COMMON_STRINGS_EACH_NAME_NOT_BUILTIN_NAMES(macro) \ - macro(SystemError) + macro(SystemError) \ + macro(S3Error) // clang-format on #define BUN_COMMON_STRINGS_ACCESSOR_DEFINITION(name) \ diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index d4f267b822..b638d6eb26 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -17,7 +17,6 @@ macro(SHA512_256) \ macro(TOML) \ macro(Transpiler) \ - macro(S3) \ macro(argv) \ macro(assetPrefix) \ macro(cwd) \ @@ -59,6 +58,7 @@ macro(resolve) \ macro(resolveSync) \ macro(s3) \ + macro(S3) \ macro(serve) \ macro(sha) \ macro(shrink) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 576e9a9baa..9693629256 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -58,6 +58,7 @@ BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__getCacheStats); BUN_DECLARE_HOST_FUNCTION(Bun__fetch); BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect); BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7); +BUN_DECLARE_HOST_FUNCTION(Bun__S3Constructor); namespace Bun { using namespace JSC; @@ -620,7 +621,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj Glob BunObject_getter_wrap_Glob DontDelete|PropertyCallback MD4 BunObject_getter_wrap_MD4 DontDelete|PropertyCallback MD5 BunObject_getter_wrap_MD5 DontDelete|PropertyCallback - S3 BunObject_getter_wrap_S3 DontDelete|PropertyCallback SHA1 BunObject_getter_wrap_SHA1 DontDelete|PropertyCallback SHA224 BunObject_getter_wrap_SHA224 DontDelete|PropertyCallback SHA256 BunObject_getter_wrap_SHA256 DontDelete|PropertyCallback @@ -683,6 +683,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj revision constructBunRevision ReadOnly|DontDelete|PropertyCallback semver BunObject_getter_wrap_semver ReadOnly|DontDelete|PropertyCallback s3 BunObject_callback_s3 DontDelete|Function 1 + S3 Bun__S3Constructor DontDelete|Constructable|Function 1 sql constructBunSQLObject DontDelete|PropertyCallback serve BunObject_callback_serve DontDelete|Function 1 sha BunObject_callback_sha DontDelete|Function 1 diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 14e93b2c85..bc9b2bfe28 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -130,10 +130,11 @@ export default [ ["ERR_POSTGRES_CONNECTION_TIMEOUT", Error, "PostgresError"], ["ERR_POSTGRES_LIFETIME_TIMEOUT", Error, "PostgresError"], - // AWS - ["ERR_AWS_MISSING_CREDENTIALS", Error], - ["ERR_AWS_INVALID_METHOD", Error], - ["ERR_AWS_INVALID_PATH", Error], - ["ERR_AWS_INVALID_ENDPOINT", Error], - ["ERR_AWS_INVALID_SIGNATURE", Error], + // S3 + ["ERR_S3_MISSING_CREDENTIALS", Error], + ["ERR_S3_INVALID_METHOD", Error], + ["ERR_S3_INVALID_PATH", Error], + ["ERR_S3_INVALID_ENDPOINT", Error], + ["ERR_S3_INVALID_SIGNATURE", Error], + ["ERR_S3_INVALID_SESSION_TOKEN", Error], ] as ErrorCodeMapping; diff --git a/src/bun.js/bindings/JSDOMFile.cpp b/src/bun.js/bindings/JSDOMFile.cpp index 6b6f980062..c67cf8f62f 100644 --- a/src/bun.js/bindings/JSDOMFile.cpp +++ b/src/bun.js/bindings/JSDOMFile.cpp @@ -42,7 +42,7 @@ public: static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject) { - auto* zigGlobal = reinterpret_cast(globalObject); + auto* zigGlobal = defaultGlobalObject(globalObject); auto structure = createStructure(vm, globalObject, zigGlobal->functionPrototype()); auto* object = new (NotNull, JSC::allocateCell(vm)) JSDOMFile(vm, structure); object->finishCreation(vm); @@ -65,7 +65,7 @@ public: static JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) { - Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); JSC::VM& vm = globalObject->vm(); JSObject* newTarget = asObject(callFrame->newTarget()); auto* constructor = globalObject->JSDOMFileConstructor(); @@ -75,15 +75,15 @@ public: auto* functionGlobalObject = reinterpret_cast( // ShadowRealm functions belong to a different global object. - getFunctionRealm(globalObject, newTarget)); + getFunctionRealm(lexicalGlobalObject, newTarget)); RETURN_IF_EXCEPTION(scope, {}); structure = InternalFunction::createSubclassStructure( - globalObject, + lexicalGlobalObject, newTarget, functionGlobalObject->JSBlobStructure()); } - void* ptr = JSDOMFile__construct(globalObject, callFrame); + void* ptr = JSDOMFile__construct(lexicalGlobalObject, callFrame); if (UNLIKELY(!ptr)) { return JSValue::encode(JSC::jsUndefined()); diff --git a/src/bun.js/bindings/JSS3Bucket.cpp b/src/bun.js/bindings/JSS3Bucket.cpp new file mode 100644 index 0000000000..f9880a3415 --- /dev/null +++ b/src/bun.js/bindings/JSS3Bucket.cpp @@ -0,0 +1,253 @@ + +#include "root.h" + +#include "JavaScriptCore/JSType.h" +#include "JavaScriptCore/JSObject.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include +#include "ZigGeneratedClasses.h" + +#include "JSS3Bucket.h" +#include +#include +#include "JavaScriptCore/JSCJSValue.h" +#include "ErrorCode.h" + +namespace Bun { +using namespace JSC; + +// External C functions declarations +extern "C" { +SYSV_ABI void* JSS3Bucket__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__call(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__unlink(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__write(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__exists(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3Bucket__size(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI void* JSS3Bucket__deinit(void* ptr); +} + +// Forward declarations +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_unlink); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_write); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_presign); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_exists); +JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_size); + +static const HashTableValue JSS3BucketPrototypeTableValues[] = { + { "unlink"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, + { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_write, 1 } }, + { "presign"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_presign, 1 } }, + { "exists"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_exists, 1 } }, + { "size"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_size, 1 } }, +}; + +class JSS3BucketPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSS3BucketPrototype* create( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::Structure* structure) + { + JSS3BucketPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSS3BucketPrototype(vm, structure); + prototype->finishCreation(vm, globalObject); + return prototype; + } + + static JSC::Structure* createStructure( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSS3BucketPrototype, Base); + return &vm.plainObjectSpace(); + } + +protected: + JSS3BucketPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSS3BucketPrototypeTableValues, *this); + } +}; + +// Implementation of JSS3Bucket methods +void JSS3Bucket::destroy(JSCell* cell) +{ + static_cast(cell)->JSS3Bucket::~JSS3Bucket(); +} + +JSS3Bucket::~JSS3Bucket() +{ + if (ptr) { + JSS3Bucket__deinit(ptr); + } +} + +JSC::GCClient::IsoSubspace* JSS3Bucket::subspaceForImpl(JSC::VM& vm) +{ + // This needs it's own heapcell because of the destructor. + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSS3Bucket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSS3Bucket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSS3Bucket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSS3Bucket = std::forward(space); }, + [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForJSS3Bucket; }); +} + +JSC_HOST_CALL_ATTRIBUTES EncodedJSValue JSS3Bucket::call(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->jsCallee(); + auto* thisObject = jsDynamicCast(thisValue); + if (UNLIKELY(!thisObject)) { + Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + ASSERT(thisObject->ptr); + + return JSS3Bucket__call(thisObject->ptr, lexicalGlobalObject, callFrame); +} + +JSC_HOST_CALL_ATTRIBUTES EncodedJSValue JSS3Bucket::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, "S3Bucket is not constructable. To instantiate a bucket, do Bun.S3()"_s); + return {}; +} + +JSS3Bucket* JSS3Bucket::create(JSC::VM& vm, Zig::GlobalObject* globalObject, void* ptr) +{ + auto* structure = globalObject->m_JSS3BucketStructure.getInitializedOnMainThread(globalObject); + NativeExecutable* executable = vm.getHostFunction(&JSS3Bucket::call, ImplementationVisibility::Public, &JSS3Bucket::construct, String("S3Bucket"_s)); + JSS3Bucket* functionObject = new (NotNull, JSC::allocateCell(vm)) JSS3Bucket(vm, executable, globalObject, structure, ptr); + functionObject->finishCreation(vm, executable, 1, "S3Bucket"_s); + return functionObject; +} + +JSC::Structure* JSS3Bucket::createStructure(JSC::JSGlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + auto* prototype = JSS3BucketPrototype::create(vm, globalObject, JSS3BucketPrototype::createStructure(vm, globalObject, globalObject->functionPrototype())); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::JSFunctionType, StructureFlags), info(), NonArray); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_unlink, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__unlink(thisObject->ptr, globalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_write, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__write(thisObject->ptr, globalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_presign, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__presign(thisObject->ptr, globalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_exists, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__exists(thisObject->ptr, globalObject, callframe); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_size, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); + return {}; + } + + return JSS3Bucket__size(thisObject->ptr, globalObject, callframe); +} + +extern "C" { +SYSV_ABI void* BUN__getJSS3Bucket(JSC::EncodedJSValue value) +{ + JSValue thisValue = JSC::JSValue::decode(value); + auto* thisObject = jsDynamicCast(thisValue); + return thisObject ? thisObject->ptr : nullptr; +}; + +BUN_DEFINE_HOST_FUNCTION(Bun__S3Constructor, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + void* ptr = JSS3Bucket__construct(globalObject, callframe); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(ptr); + + return JSValue::encode(JSS3Bucket::create(vm, defaultGlobalObject(globalObject), ptr)); +} +} + +Structure* createJSS3BucketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSS3Bucket::createStructure(globalObject); +} + +const JSC::ClassInfo JSS3BucketPrototype::s_info = { "S3Bucket"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3BucketPrototype) }; +const JSC::ClassInfo JSS3Bucket::s_info = { "S3Bucket"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3Bucket) }; + +} // namespace Bun diff --git a/src/bun.js/bindings/JSS3Bucket.h b/src/bun.js/bindings/JSS3Bucket.h new file mode 100644 index 0000000000..84d0868e23 --- /dev/null +++ b/src/bun.js/bindings/JSS3Bucket.h @@ -0,0 +1,50 @@ +#pragma once + +namespace Zig { +class GlobalObject; +} + +namespace Bun { +using namespace JSC; + +class JSS3Bucket : public JSC::JSFunction { + using Base = JSC::JSFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + +public: + static constexpr bool needsDestruction = true; + + JSS3Bucket(JSC::VM& vm, NativeExecutable* executable, JSGlobalObject* globalObject, Structure* structure, void* ptr) + : Base(vm, executable, globalObject, structure) + { + this->ptr = ptr; + } + DECLARE_INFO; + + static void destroy(JSCell* cell); + ~JSS3Bucket(); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + + static JSC_HOST_CALL_ATTRIBUTES EncodedJSValue call(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame); + static JSC_HOST_CALL_ATTRIBUTES EncodedJSValue construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame); + + static JSS3Bucket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, void* ptr); + static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); + + void* ptr; +}; + +// Constructor helper +JSValue constructS3Bucket(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe); +Structure* createJSS3BucketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + +} // namespace Bun diff --git a/src/bun.js/bindings/JSS3File.cpp b/src/bun.js/bindings/JSS3File.cpp index 418c449f57..614d6a983d 100644 --- a/src/bun.js/bindings/JSS3File.cpp +++ b/src/bun.js/bindings/JSS3File.cpp @@ -1,125 +1,189 @@ + #include "root.h" + +#include "ZigGlobalObject.h" #include "ZigGeneratedClasses.h" -#include + +#include "JavaScriptCore/JSType.h" +#include "JavaScriptCore/JSObject.h" +#include "JavaScriptCore/JSGlobalObject.h" #include +#include #include -#include "JSS3File.h" +#include #include "JavaScriptCore/JSCJSValue.h" +#include "ErrorCode.h" +#include "JSS3File.h" + +namespace Bun { using namespace JSC; +using namespace WebCore; -extern "C" SYSV_ABI void* JSS3File__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); -extern "C" SYSV_ABI bool JSS3File__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); - +// External C functions declarations extern "C" { +SYSV_ABI void* JSS3File__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3File__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3File__bucket(void* ptr, JSC::JSGlobalObject*); +SYSV_ABI bool JSS3File__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); +} -JSC::EncodedJSValue BUN__createJSS3FileConstructor(JSGlobalObject* lexicalGlobalObject) +// Forward declarations +JSC_DECLARE_HOST_FUNCTION(functionS3File_presign); +static JSC_DECLARE_CUSTOM_GETTER(getterS3File_bucket); +static JSC_DEFINE_CUSTOM_GETTER(getterS3File_bucket, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { - Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); - return JSValue::encode(globalObject->JSS3FileConstructor()); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3File instance"_s); + return {}; + } + + return JSS3File__bucket(thisObject->wrapped(), globalObject); } -} - -// TODO: make this inehrit from JSBlob instead of InternalFunction -// That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain. -class JSS3File : public JSC::InternalFunction { - using Base = JSC::InternalFunction; - +static const HashTableValue JSS3FilePrototypeTableValues[] = { + { "presign"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3File_presign, 1 } }, + { "bucket"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor | PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, getterS3File_bucket, 0 } }, +}; +class JSS3FilePrototype final : public WebCore::JSBlobPrototype { public: - JSS3File(JSC::VM& vm, JSC::Structure* structure) - : Base(vm, structure, call, construct) + using Base = WebCore::JSBlobPrototype; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSS3FilePrototype* create( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::Structure* structure) { + JSS3FilePrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSS3FilePrototype(vm, globalObject, structure); + prototype->finishCreation(vm, globalObject); + return prototype; + } + + static JSC::Structure* createStructure( + JSC::VM& vm, + JSC::JSGlobalObject* globalObject, + JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; } DECLARE_INFO; - static constexpr unsigned StructureFlags = (Base::StructureFlags & ~ImplementsDefaultHasInstance) | ImplementsHasInstance; - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { - return &vm.internalFunctionSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(InternalFunctionType, StructureFlags), info()); + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSS3FilePrototype, Base); + return &vm.plainObjectSpace(); } - void finishCreation(JSC::VM& vm) +protected: + JSS3FilePrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, globalObject, structure) { - Base::finishCreation(vm, 2, "S3"_s); } - static JSS3File* create(JSC::VM& vm, JSGlobalObject* globalObject) + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { - auto* zigGlobal = reinterpret_cast(globalObject); - auto structure = createStructure(vm, globalObject, zigGlobal->functionPrototype()); - auto* object = new (NotNull, JSC::allocateCell(vm)) JSS3File(vm, structure); - object->finishCreation(vm); - - // This is not quite right. But we'll fix it if someone files an issue about it. - object->putDirect(vm, vm.propertyNames->prototype, zigGlobal->JSBlobPrototype(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); - - return object; - } - - static bool customHasInstance(JSObject* object, JSGlobalObject* globalObject, JSValue value) - { - if (!value.isObject()) - return false; - - // Note: this breaks [Symbol.hasInstance] - // We must do this for now until we update the code generator to export classes - return JSS3File__hasInstance(JSValue::encode(object), globalObject, JSValue::encode(value)); - } - - static JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) - { - Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); - JSC::VM& vm = globalObject->vm(); - JSObject* newTarget = asObject(callFrame->newTarget()); - auto* constructor = globalObject->JSS3FileConstructor(); - - Structure* structure = globalObject->JSBlobStructure(); - if (constructor != newTarget) { - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* functionGlobalObject = reinterpret_cast( - // ShadowRealm functions belong to a different global object. - getFunctionRealm(globalObject, newTarget)); - RETURN_IF_EXCEPTION(scope, {}); - structure = InternalFunction::createSubclassStructure( - globalObject, - newTarget, - functionGlobalObject->JSBlobStructure()); - } - - void* ptr = JSS3File__construct(globalObject, callFrame); - - if (UNLIKELY(!ptr)) { - return JSValue::encode(JSC::jsUndefined()); - } - - return JSValue::encode( - WebCore::JSBlob::create(vm, globalObject, structure, ptr)); - } - - static JSC_HOST_CALL_ATTRIBUTES EncodedJSValue call(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) - { - auto scope = DECLARE_THROW_SCOPE(lexicalGlobalObject->vm()); - throwTypeError(lexicalGlobalObject, scope, "Class constructor S3 cannot be invoked without 'new'"_s); - return {}; + Base::finishCreation(vm, globalObject); + ASSERT(inherits(info())); + reifyStaticProperties(vm, JSS3File::info(), JSS3FilePrototypeTableValues, *this); } }; -const JSC::ClassInfo JSS3File::s_info = { "S3"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3File) }; - -namespace Bun { - -JSC::JSObject* createJSS3FileConstructor(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +// Implementation of JSS3File methods +void JSS3File::destroy(JSCell* cell) { - return JSS3File::create(vm, globalObject); + static_cast(cell)->JSS3File::~JSS3File(); +} + +JSS3File::~JSS3File() +{ + // Base class destructor will be called automatically +} + +JSS3File* JSS3File::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr) +{ + JSS3File* thisObject = new (NotNull, JSC::allocateCell(vm)) JSS3File(vm, structure, ptr); + thisObject->finishCreation(vm); + return thisObject; +} + +JSValue constructS3FileInternal(JSC::JSGlobalObject* lexicalGlobalObject, void* ptr) +{ + ASSERT(ptr); + JSC::VM& vm = lexicalGlobalObject->vm(); + + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto* structure = globalObject->m_JSS3FileStructure.getInitializedOnMainThread(lexicalGlobalObject); + return JSS3File::create(vm, globalObject, structure, ptr); +} + +JSValue constructS3File(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + void* ptr = JSS3File__construct(globalObject, callframe); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(ptr); + + return constructS3FileInternal(globalObject, ptr); +} + +JSC::Structure* JSS3File::createStructure(JSC::JSGlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + + JSC::JSObject* superPrototype = defaultGlobalObject(globalObject)->JSBlobPrototype(); + auto* protoStructure = JSS3FilePrototype::createStructure(vm, globalObject, superPrototype); + auto* prototype = JSS3FilePrototype::create(vm, globalObject, protoStructure); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(0b11101110), StructureFlags), info(), NonArray); +} + +static bool customHasInstance(JSObject* object, JSGlobalObject* globalObject, JSValue value) +{ + if (!value.isObject()) + return false; + + return JSS3File__hasInstance(JSValue::encode(object), globalObject, JSValue::encode(value)); +} + +Structure* createJSS3FileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSS3File::createStructure(globalObject); +} + +JSC_DEFINE_HOST_FUNCTION(functionS3File_presign, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3File instance"_s); + return {}; + } + + return JSS3File__presign(thisObject->wrapped(), globalObject, callframe); +} + +const JSC::ClassInfo JSS3FilePrototype::s_info = { "S3File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3FilePrototype) }; +const JSC::ClassInfo JSS3File::s_info = { "S3File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3File) }; + +extern "C" { +SYSV_ABI EncodedJSValue BUN__createJSS3File(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) +{ + return JSValue::encode(constructS3File(globalObject, callframe)); +}; + +SYSV_ABI EncodedJSValue BUN__createJSS3FileUnsafely(JSC::JSGlobalObject* globalObject, void* ptr) +{ + return JSValue::encode(constructS3FileInternal(globalObject, ptr)); +}; } } diff --git a/src/bun.js/bindings/JSS3File.h b/src/bun.js/bindings/JSS3File.h index 63b8170b06..fab0927efb 100644 --- a/src/bun.js/bindings/JSS3File.h +++ b/src/bun.js/bindings/JSS3File.h @@ -1,7 +1,41 @@ #pragma once -#include "root.h" +namespace Zig { +class GlobalObject; +} namespace Bun { -JSC::JSObject* createJSS3FileConstructor(JSC::VM&, JSC::JSGlobalObject*); -} +using namespace JSC; + +class JSS3File : public WebCore::JSBlob { + using Base = WebCore::JSBlob; + +public: + static constexpr bool needsDestruction = true; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + JSS3File(JSC::VM& vm, Structure* structure, void* ptr) + : Base(vm, structure, ptr) + { + } + DECLARE_INFO; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::JSBlob::subspaceFor(vm); + } + + static void destroy(JSCell* cell); + ~JSS3File(); + + static JSS3File* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr); + static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); +}; + +// Constructor helper +JSValue constructS3File(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe); +Structure* createJSS3FileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + +} // namespace Bun diff --git a/src/bun.js/bindings/S3Error.cpp b/src/bun.js/bindings/S3Error.cpp new file mode 100644 index 0000000000..a3ae91651c --- /dev/null +++ b/src/bun.js/bindings/S3Error.cpp @@ -0,0 +1,63 @@ + +#include "root.h" + +#include +#include +#include "ZigGeneratedClasses.h" +#include "S3Error.h" + +namespace Bun { + +typedef struct S3Error { + BunString code; + BunString message; + BunString path; +} S3Error; + +Structure* createS3ErrorStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSC::ErrorInstance::createStructure(vm, globalObject, JSC::constructEmptyObject(globalObject, globalObject->errorPrototype())); +} + +extern "C" { +SYSV_ABI JSC::EncodedJSValue S3Error__toErrorInstance(const S3Error* arg0, + JSC::JSGlobalObject* globalObject) +{ + S3Error err = *arg0; + + JSC::VM& vm = globalObject->vm(); + + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSValue message = JSC::jsUndefined(); + if (err.message.tag != BunStringTag::Empty) { + message = Bun::toJS(globalObject, err.message); + } + + auto& names = WebCore::builtinNames(vm); + + JSC::JSValue options = JSC::jsUndefined(); + auto prototype = defaultGlobalObject(globalObject)->m_S3ErrorStructure.getInitializedOnMainThread(globalObject); + JSC::JSObject* result = JSC::ErrorInstance::create(globalObject, prototype, message, options); + result->putDirect( + vm, vm.propertyNames->name, + JSC::JSValue(defaultGlobalObject(globalObject)->commonStrings().S3ErrorString(globalObject)), + JSC::PropertyAttribute::DontEnum | 0); + if (err.code.tag != BunStringTag::Empty) { + JSC::JSValue code = Bun::toJS(globalObject, err.code); + result->putDirect(vm, names.codePublicName(), code, + JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | 0); + } + + if (err.path.tag != BunStringTag::Empty) { + JSC::JSValue path = Bun::toJS(globalObject, err.path); + result->putDirect(vm, names.pathPublicName(), path, + JSC::PropertyAttribute::DontDelete | 0); + } + + RETURN_IF_EXCEPTION(scope, {}); + scope.release(); + + return JSC::JSValue::encode(JSC::JSValue(result)); +} +} +} diff --git a/src/bun.js/bindings/S3Error.h b/src/bun.js/bindings/S3Error.h new file mode 100644 index 0000000000..516a9e907b --- /dev/null +++ b/src/bun.js/bindings/S3Error.h @@ -0,0 +1,7 @@ +#pragma once + +namespace Bun { +using namespace JSC; + +Structure* createS3ErrorStructure(VM& vm, JSGlobalObject* globalObject); +} diff --git a/src/bun.js/bindings/Sink.h b/src/bun.js/bindings/Sink.h index 0b07ad0f5c..60ded13833 100644 --- a/src/bun.js/bindings/Sink.h +++ b/src/bun.js/bindings/Sink.h @@ -9,7 +9,7 @@ enum SinkID : uint8_t { HTMLRewriterSink = 3, HTTPResponseSink = 4, HTTPSResponseSink = 5, - FetchTaskletChunkedRequestSink = 6, + NetworkSink = 6, }; static constexpr unsigned numberOfSinkIDs diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 1889b0c9be..ce2cf49ca5 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1,4 +1,5 @@ #include "root.h" + #include "JavaScriptCore/PropertySlot.h" #include "ZigGlobalObject.h" #include "helpers.h" @@ -33,6 +34,7 @@ #include "JavaScriptCore/JSLock.h" #include "JavaScriptCore/JSMap.h" #include "JavaScriptCore/JSMicrotask.h" + #include "JavaScriptCore/JSModuleLoader.h" #include "JavaScriptCore/JSModuleNamespaceObject.h" #include "JavaScriptCore/JSModuleNamespaceObjectInlines.h" @@ -84,7 +86,6 @@ #include "JSDOMConvertUnion.h" #include "JSDOMException.h" #include "JSDOMFile.h" -#include "JSS3File.h" #include "JSDOMFormData.h" #include "JSDOMURL.h" #include "JSEnvironmentVariableMap.h" @@ -158,6 +159,9 @@ #include "JSPerformanceResourceTiming.h" #include "JSPerformanceTiming.h" +#include "JSS3Bucket.h" +#include "JSS3File.h" +#include "S3Error.h" #if ENABLE(REMOTE_INSPECTOR) #include "JavaScriptCore/RemoteInspectorServer.h" #endif @@ -2793,37 +2797,6 @@ JSC_DEFINE_CUSTOM_SETTER(moduleNamespacePrototypeSetESModuleMarker, (JSGlobalObj return true; } -extern "C" JSC::EncodedJSValue JSS3File__upload(JSGlobalObject*, JSC::CallFrame*); -extern "C" JSC::EncodedJSValue JSS3File__presign(JSGlobalObject*, JSC::CallFrame*); -extern "C" JSC::EncodedJSValue JSS3File__unlink(JSGlobalObject*, JSC::CallFrame*); -extern "C" JSC::EncodedJSValue JSS3File__exists(JSGlobalObject*, JSC::CallFrame*); -extern "C" JSC::EncodedJSValue JSS3File__size(JSGlobalObject*, JSC::CallFrame*); - -JSC_DEFINE_HOST_FUNCTION(jsS3Upload, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - return JSS3File__upload(lexicalGlobalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(jsS3Presign, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - return JSS3File__presign(lexicalGlobalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(jsS3Unlink, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - return JSS3File__unlink(lexicalGlobalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(jsS3Exists, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - return JSS3File__exists(lexicalGlobalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(jsS3Size, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - return JSS3File__size(lexicalGlobalObject, callFrame); -} - void GlobalObject::finishCreation(VM& vm) { Base::finishCreation(vm); @@ -2845,18 +2818,6 @@ void GlobalObject::finishCreation(VM& vm) init.set(fileConstructor); }); - m_JSS3FileConstructor.initLater( - [](const Initializer& init) { - JSObject* s3Constructor = Bun::createJSS3FileConstructor(init.vm, init.owner); - s3Constructor->putDirectNativeFunction(init.vm, init.owner, JSC::Identifier::fromString(init.vm, "upload"_s), 3, jsS3Upload, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0); - s3Constructor->putDirectNativeFunction(init.vm, init.owner, JSC::Identifier::fromString(init.vm, "unlink"_s), 3, jsS3Unlink, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0); - s3Constructor->putDirectNativeFunction(init.vm, init.owner, JSC::Identifier::fromString(init.vm, "presign"_s), 3, jsS3Presign, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0); - s3Constructor->putDirectNativeFunction(init.vm, init.owner, JSC::Identifier::fromString(init.vm, "exists"_s), 3, jsS3Exists, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0); - s3Constructor->putDirectNativeFunction(init.vm, init.owner, JSC::Identifier::fromString(init.vm, "size"_s), 3, jsS3Size, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0); - - init.set(s3Constructor); - }); - m_cryptoObject.initLater( [](const Initializer& init) { JSC::JSGlobalObject* globalObject = init.owner; @@ -2904,6 +2865,20 @@ void GlobalObject::finishCreation(VM& vm) init.set(result.toObject(init.owner)); }); + m_JSS3BucketStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createJSS3BucketStructure(init.vm, init.owner)); + }); + m_JSS3FileStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createJSS3FileStructure(init.vm, init.owner)); + }); + + m_S3ErrorStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createS3ErrorStructure(init.vm, init.owner)); + }); + m_commonJSModuleObjectStructure.initLater( [](const Initializer& init) { init.set(Bun::createCommonJSModuleStructure(reinterpret_cast(init.owner))); @@ -3152,7 +3127,7 @@ void GlobalObject::finishCreation(VM& vm) m_JSFetchTaskletChunkedRequestControllerPrototype.initLater( [](const JSC::LazyProperty::Initializer& init) { - auto* prototype = createJSSinkControllerPrototype(init.vm, init.owner, WebCore::SinkID::FetchTaskletChunkedRequestSink); + auto* prototype = createJSSinkControllerPrototype(init.vm, init.owner, WebCore::SinkID::NetworkSink); init.set(prototype); }); @@ -3284,11 +3259,11 @@ void GlobalObject::finishCreation(VM& vm) init.setConstructor(constructor); }); - m_JSFetchTaskletChunkedRequestSinkClassStructure.initLater( + m_JSNetworkSinkClassStructure.initLater( [](LazyClassStructure::Initializer& init) { - auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::FetchTaskletChunkedRequestSink); - auto* structure = JSFetchTaskletChunkedRequestSink::createStructure(init.vm, init.global, prototype); - auto* constructor = JSFetchTaskletChunkedRequestSinkConstructor::create(init.vm, init.global, JSFetchTaskletChunkedRequestSinkConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), jsCast(prototype)); + auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::NetworkSink); + auto* structure = JSNetworkSink::createStructure(init.vm, init.global, prototype); + auto* constructor = JSNetworkSinkConstructor::create(init.vm, init.global, JSNetworkSinkConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), jsCast(prototype)); init.setPrototype(prototype); init.setStructure(structure); init.setConstructor(constructor); @@ -3831,7 +3806,9 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSCryptoKey.visit(visitor); thisObject->m_lazyStackCustomGetterSetter.visit(visitor); thisObject->m_JSDOMFileConstructor.visit(visitor); - thisObject->m_JSS3FileConstructor.visit(visitor); + thisObject->m_JSS3BucketStructure.visit(visitor); + thisObject->m_JSS3FileStructure.visit(visitor); + thisObject->m_S3ErrorStructure.visit(visitor); thisObject->m_JSFFIFunctionStructure.visit(visitor); thisObject->m_JSFileSinkClassStructure.visit(visitor); thisObject->m_JSFileSinkControllerPrototype.visit(visitor); @@ -3839,7 +3816,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSHTTPResponseSinkClassStructure.visit(visitor); thisObject->m_JSHTTPSResponseControllerPrototype.visit(visitor); thisObject->m_JSHTTPSResponseSinkClassStructure.visit(visitor); - thisObject->m_JSFetchTaskletChunkedRequestSinkClassStructure.visit(visitor); + thisObject->m_JSNetworkSinkClassStructure.visit(visitor); thisObject->m_JSFetchTaskletChunkedRequestControllerPrototype.visit(visitor); thisObject->m_JSSocketAddressStructure.visit(visitor); thisObject->m_JSSQLStatementStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 33beb34c7e..fb6d919ba5 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -210,10 +210,10 @@ public: JSC::JSValue HTTPSResponseSinkPrototype() const { return m_JSHTTPSResponseSinkClassStructure.prototypeInitializedOnMainThread(this); } JSC::JSValue JSReadableHTTPSResponseSinkControllerPrototype() const { return m_JSHTTPSResponseControllerPrototype.getInitializedOnMainThread(this); } - JSC::Structure* FetchTaskletChunkedRequestSinkStructure() const { return m_JSFetchTaskletChunkedRequestSinkClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* FetchTaskletChunkedRequestSink() { return m_JSFetchTaskletChunkedRequestSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue FetchTaskletChunkedRequestSinkPrototype() const { return m_JSFetchTaskletChunkedRequestSinkClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSValue JSReadableFetchTaskletChunkedRequestSinkControllerPrototype() const { return m_JSFetchTaskletChunkedRequestControllerPrototype.getInitializedOnMainThread(this); } + JSC::Structure* NetworkSinkStructure() const { return m_JSNetworkSinkClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* NetworkSink() { return m_JSNetworkSinkClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue NetworkSinkPrototype() const { return m_JSNetworkSinkClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue JSReadableNetworkSinkControllerPrototype() const { return m_JSFetchTaskletChunkedRequestControllerPrototype.getInitializedOnMainThread(this); } JSC::Structure* JSBufferListStructure() const { return m_JSBufferListClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* JSBufferList() { return m_JSBufferListClassStructure.constructorInitializedOnMainThread(this); } @@ -478,9 +478,12 @@ public: LazyProperty m_processEnvObject; + LazyProperty m_JSS3BucketStructure; + LazyProperty m_JSS3FileStructure; + LazyProperty m_S3ErrorStructure; + JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); } JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); } - JSObject* JSS3FileConstructor() const { return m_JSS3FileConstructor.getInitializedOnMainThread(this); } Bun::CommonStrings& commonStrings() { return m_commonStrings; } Bun::Http2CommonStrings& http2CommonStrings() { return m_http2_commongStrings; } @@ -521,7 +524,7 @@ public: LazyClassStructure m_JSFileSinkClassStructure; LazyClassStructure m_JSHTTPResponseSinkClassStructure; LazyClassStructure m_JSHTTPSResponseSinkClassStructure; - LazyClassStructure m_JSFetchTaskletChunkedRequestSinkClassStructure; + LazyClassStructure m_JSNetworkSinkClassStructure; LazyClassStructure m_JSStringDecoderClassStructure; LazyClassStructure m_NapiClassStructure; @@ -574,7 +577,6 @@ public: LazyProperty m_importMetaObjectStructure; LazyProperty m_asyncBoundFunctionStructure; LazyProperty m_JSDOMFileConstructor; - LazyProperty m_JSS3FileConstructor; LazyProperty m_JSCryptoKey; LazyProperty m_NapiExternalStructure; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 19ba3bb1df..4bb4689510 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1952,7 +1952,7 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, if (err.code.tag != BunStringTag::Empty) { JSC::JSValue code = Bun::toJS(globalObject, err.code); result->putDirect(vm, names.codePublicName(), code, - JSC::PropertyAttribute::DontDelete | 0); + JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | 0); result->putDirect(vm, vm.propertyNames->name, code, JSC::PropertyAttribute::DontEnum | 0); } else { diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 4fc4b0ddee..964eab3dee 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6789,6 +6789,12 @@ pub const JSHostFunctionType = fn (*JSGlobalObject, *CallFrame) callconv(JSC.con pub const JSHostFunctionTypeWithCCallConvForAssertions = fn (*JSGlobalObject, *CallFrame) callconv(.C) JSValue; pub const JSHostFunctionPtr = *const JSHostFunctionType; pub const JSHostZigFunction = fn (*JSGlobalObject, *CallFrame) bun.JSError!JSValue; +pub fn JSHostZigFunctionWithContext(comptime ContextType: type) type { + return fn (*ContextType, *JSGlobalObject, *CallFrame) bun.JSError!JSValue; +} +pub fn JSHostFunctionTypeWithContext(comptime ContextType: type) type { + return fn (*ContextType, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue; +} pub fn toJSHostFunction(comptime Function: JSHostZigFunction) JSC.JSHostFunctionType { return struct { @@ -6826,6 +6832,42 @@ pub fn toJSHostFunction(comptime Function: JSHostZigFunction) JSC.JSHostFunction } }.function; } +pub fn toJSHostFunctionWithContext(comptime ContextType: type, comptime Function: JSHostZigFunctionWithContext(ContextType)) JSHostFunctionTypeWithContext(ContextType) { + return struct { + pub fn function(ctx: *ContextType, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + if (bun.Environment.allow_assert and bun.Environment.is_canary) { + const value = Function(ctx, globalThis, callframe) catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; + if (comptime bun.Environment.isDebug) { + if (value != .zero) { + if (globalThis.hasException()) { + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + bun.Output.prettyErrorln( + \\Assertion failed: Native function returned a non-zero JSValue while an exception is pending + \\ + \\ fn: {s} + \\ value: {} + \\ + , .{ + &Function, // use `(lldb) image lookup --address 0x1ec4` to discover what function failed + value.toFmt(&formatter), + }); + Output.flush(); + } + } + } + bun.assert((value == .zero) == globalThis.hasException()); + return value; + } + return @call(.always_inline, Function, .{ ctx, globalThis, callframe }) catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; + } + }.function; +} pub fn toJSHostValue(globalThis: *JSGlobalObject, value: error{ OutOfMemory, JSError }!JSValue) JSValue { if (bun.Environment.allow_assert and bun.Environment.is_canary) { diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index b7374f2705..a581958296 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -144,7 +144,7 @@ pub const JSArrayBufferSink = JSC.WebCore.ArrayBufferSink.JSSink; pub const JSHTTPSResponseSink = JSC.WebCore.HTTPSResponseSink.JSSink; pub const JSHTTPResponseSink = JSC.WebCore.HTTPResponseSink.JSSink; pub const JSFileSink = JSC.WebCore.FileSink.JSSink; -pub const JSFetchTaskletChunkedRequestSink = JSC.WebCore.FetchTaskletChunkedRequestSink.JSSink; +pub const JSNetworkSink = JSC.WebCore.NetworkSink.JSSink; // WebSocket pub const WebSocketHTTPClient = @import("../../http/websocket_http_client.zig").WebSocketHTTPClient; @@ -967,7 +967,7 @@ comptime { JSArrayBufferSink.shim.ref(); JSHTTPResponseSink.shim.ref(); JSHTTPSResponseSink.shim.ref(); - JSFetchTaskletChunkedRequestSink.shim.ref(); + JSNetworkSink.shim.ref(); JSFileSink.shim.ref(); JSFileSink.shim.ref(); _ = ZigString__free; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 9bdf332b16..ab9f3ca437 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -686,24 +686,24 @@ ZIG_DECL void FileSink__updateRef(void* arg0, bool arg1); BUN_DECLARE_HOST_FUNCTION(FileSink__write); #endif -CPP_DECL JSC__JSValue FetchTaskletChunkedRequestSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue FetchTaskletChunkedRequestSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); -CPP_DECL void FetchTaskletChunkedRequestSink__detachPtr(JSC__JSValue JSValue0); -CPP_DECL void* FetchTaskletChunkedRequestSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); -CPP_DECL void FetchTaskletChunkedRequestSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); -CPP_DECL void FetchTaskletChunkedRequestSink__onReady(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSValue JSValue2); +CPP_DECL JSC__JSValue NetworkSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); +CPP_DECL JSC__JSValue NetworkSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); +CPP_DECL void NetworkSink__detachPtr(JSC__JSValue JSValue0); +CPP_DECL void* NetworkSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); +CPP_DECL void NetworkSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); +CPP_DECL void NetworkSink__onReady(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSValue JSValue2); #ifdef __cplusplus -ZIG_DECL JSC__JSValue FetchTaskletChunkedRequestSink__close(JSC__JSGlobalObject* arg0, void* arg1); -BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__construct); -BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__end); -ZIG_DECL JSC__JSValue SYSV_ABI SYSV_ABI FetchTaskletChunkedRequestSink__endWithSink(void* arg0, JSC__JSGlobalObject* arg1); -ZIG_DECL void FetchTaskletChunkedRequestSink__finalize(void* arg0); -BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__flush); -BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__start); -ZIG_DECL void FetchTaskletChunkedRequestSink__updateRef(void* arg0, bool arg1); -BUN_DECLARE_HOST_FUNCTION(FetchTaskletChunkedRequestSink__write); +ZIG_DECL JSC__JSValue NetworkSink__close(JSC__JSGlobalObject* arg0, void* arg1); +BUN_DECLARE_HOST_FUNCTION(NetworkSink__construct); +BUN_DECLARE_HOST_FUNCTION(NetworkSink__end); +ZIG_DECL JSC__JSValue SYSV_ABI SYSV_ABI NetworkSink__endWithSink(void* arg0, JSC__JSGlobalObject* arg1); +ZIG_DECL void NetworkSink__finalize(void* arg0); +BUN_DECLARE_HOST_FUNCTION(NetworkSink__flush); +BUN_DECLARE_HOST_FUNCTION(NetworkSink__start); +ZIG_DECL void NetworkSink__updateRef(void* arg0, bool arg1); +BUN_DECLARE_HOST_FUNCTION(NetworkSink__write); #endif #ifdef __cplusplus diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index c4f37d3490..91264230dc 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -377,13 +377,13 @@ pub extern fn FileSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usi pub extern fn FileSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; pub extern fn FileSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn FileSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; -pub extern fn FetchTaskletChunkedRequestSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn FetchTaskletChunkedRequestSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; -pub extern fn FetchTaskletChunkedRequestSink__detachPtr(JSValue0: JSC__JSValue) void; -pub extern fn FetchTaskletChunkedRequestSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; -pub extern fn FetchTaskletChunkedRequestSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; -pub extern fn FetchTaskletChunkedRequestSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; -pub extern fn FetchTaskletChunkedRequestSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; +pub extern fn NetworkSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; +pub extern fn NetworkSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; +pub extern fn NetworkSink__detachPtr(JSValue0: JSC__JSValue) void; +pub extern fn NetworkSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; +pub extern fn NetworkSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; +pub extern fn NetworkSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; +pub extern fn NetworkSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn ZigException__fromException(arg0: [*c]bindings.Exception) ZigException; pub const JSC__GetterSetter = bindings.GetterSetter; diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index dc805895d2..2dffbe8465 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -58,6 +58,8 @@ public: std::unique_ptr m_clientSubspaceForFunctionTemplate; std::unique_ptr m_clientSubspaceForV8Function; std::unique_ptr m_clientSubspaceForNodeVMGlobalObject; + std::unique_ptr m_clientSubspaceForJSS3Bucket; + std::unique_ptr m_clientSubspaceForJSS3File; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 2d4eb091c5..5af65d80ac 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -58,6 +58,8 @@ public: std::unique_ptr m_subspaceForFunctionTemplate; std::unique_ptr m_subspaceForV8Function; std::unique_ptr m_subspaceForNodeVMGlobalObject; + std::unique_ptr m_subspaceForJSS3Bucket; + std::unique_ptr m_subspaceForJSS3File; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 72232be196..1a7440ebe8 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -2193,6 +2193,31 @@ pub const Process = struct { pub export const Bun__versions_zstd: [*:0]const u8 = bun.Global.versions.zstd; }; +pub const PathOrBlob = union(enum) { + path: JSC.Node.PathOrFileDescriptor, + blob: Blob, + + const Blob = JSC.WebCore.Blob; + + pub fn fromJSNoCopy(ctx: *JSC.JSGlobalObject, args: *JSC.Node.ArgumentsSlice) bun.JSError!PathOrBlob { + if (try JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, bun.default_allocator)) |path| { + return PathOrBlob{ + .path = path, + }; + } + + const arg = args.nextEat() orelse { + return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", .undefined); + }; + if (arg.as(Blob)) |blob| { + return PathOrBlob{ + .blob = blob.*, + }; + } + return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", arg); + } +}; + comptime { std.testing.refAllDecls(Process); } diff --git a/src/bun.js/webcore/S3Bucket.zig b/src/bun.js/webcore/S3Bucket.zig new file mode 100644 index 0000000000..7c8eed5392 --- /dev/null +++ b/src/bun.js/webcore/S3Bucket.zig @@ -0,0 +1,267 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const Blob = JSC.WebCore.Blob; +const PathOrBlob = JSC.Node.PathOrBlob; +const ZigString = JSC.ZigString; +const Method = bun.http.Method; +const S3File = @import("./S3File.zig"); +const AWSCredentials = bun.AWSCredentials; + +const S3BucketOptions = struct { + credentials: *AWSCredentials, + options: bun.S3.MultiPartUpload.MultiPartUploadOptions = .{}, + acl: ?bun.S3.ACL = null, + pub usingnamespace bun.New(@This()); + + pub fn deinit(this: *@This()) void { + this.credentials.deref(); + this.destroy(); + } +}; + +pub fn writeFormatCredentials(credentials: *AWSCredentials, options: bun.S3.MultiPartUpload.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll("\n"); + + { + const Writer = @TypeOf(writer); + + formatter.indent += 1; + defer formatter.indent -|= 1; + + const endpoint = if (credentials.endpoint.len > 0) credentials.endpoint else "https://s3..amazonaws.com"; + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("endpoint: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{endpoint}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + const region = if (credentials.region.len > 0) credentials.region else AWSCredentials.guessRegion(credentials.endpoint); + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("region: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{region}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + // PS: We don't want to print the credentials if they are empty just signal that they are there without revealing them + if (credentials.accessKeyId.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("accessKeyId: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.secretAccessKey.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("secretAccessKey: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.sessionToken.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("sessionToken: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (acl) |acl_value| { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("acl: ", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{acl_value.toString()}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("partSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.partSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("queueSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.queueSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("retry: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.retry), .NumberObject, enable_ansi_colors); + try writer.writeAll("\n"); + } +} +pub fn writeFormat(this: *S3BucketOptions, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime bun.Output.prettyFmt("S3Bucket", enable_ansi_colors)); + if (this.credentials.bucket.len > 0) { + try writer.print( + comptime bun.Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), + .{ + this.credentials.bucket, + }, + ); + } else { + try writer.writeAll(comptime bun.Output.prettyFmt(" {{", enable_ansi_colors)); + } + + try writeFormatCredentials(this.credentials, this.options, this.acl, Formatter, formatter, writer, enable_ansi_colors); + try formatter.writeIndent(@TypeOf(writer), writer); + try writer.writeAll("}"); + formatter.resetLine(); +} +extern fn BUN__getJSS3Bucket(value: JSValue) callconv(JSC.conv) ?*S3BucketOptions; + +pub fn fromJS(value: JSValue) ?*S3BucketOptions { + return BUN__getJSS3Bucket(value); +} + +pub fn call(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path ", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = Blob.new(try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); + blob.allocator = bun.default_allocator; + return blob.toJS(globalThis); +} + +pub fn presign(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to presign", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to presign", .{}); + }; + errdefer path.deinit(); + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.getPresignUrlFrom(&blob, globalThis, options); +} + +pub fn exists(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check if it exists", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check if it exists", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.exists(globalThis, &blob); +} + +pub fn size(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check the size of", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check the size of", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.size(globalThis, &blob); +} + +pub fn write(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to write to", .{}).throw(); + }; + errdefer path.deinit(); + const data = args.nextEat() orelse { + return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to write", .{}).throw(); + }; + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + var blob_internal: PathOrBlob = .{ .blob = blob }; + return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); +} + +pub fn unlink(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to unlink", .{}).throw(); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); +} +pub fn construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) ?*S3BucketOptions { + const arguments = callframe.arguments_old(1).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const options = args.nextEat() orelse { + globalThis.ERR_MISSING_ARGS("Expected S3 options to be passed", .{}).throw() catch return null; + }; + if (options.isEmptyOrUndefinedOrNull() or !options.isObject()) { + globalThis.throwInvalidArguments("Expected S3 options to be passed", .{}) catch return null; + } + var aws_options = AWSCredentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getAWSCredentials(), .{}, options, null, globalThis) catch return null; + defer aws_options.deinit(); + return S3BucketOptions.new(.{ + .credentials = aws_options.credentials.dupe(), + .options = aws_options.options, + .acl = aws_options.acl, + }); +} +pub fn finalize(ptr: *S3BucketOptions) callconv(JSC.conv) void { + ptr.deinit(); +} +pub const exports = struct { + pub const JSS3Bucket__exists = JSC.toJSHostFunctionWithContext(S3BucketOptions, exists); + pub const JSS3Bucket__size = JSC.toJSHostFunctionWithContext(S3BucketOptions, size); + pub const JSS3Bucket__write = JSC.toJSHostFunctionWithContext(S3BucketOptions, write); + pub const JSS3Bucket__unlink = JSC.toJSHostFunctionWithContext(S3BucketOptions, unlink); + pub const JSS3Bucket__presign = JSC.toJSHostFunctionWithContext(S3BucketOptions, presign); + pub const JSS3Bucket__call = JSC.toJSHostFunctionWithContext(S3BucketOptions, call); +}; + +comptime { + @export(exports.JSS3Bucket__exists, .{ .name = "JSS3Bucket__exists" }); + @export(exports.JSS3Bucket__size, .{ .name = "JSS3Bucket__size" }); + @export(exports.JSS3Bucket__write, .{ .name = "JSS3Bucket__write" }); + @export(exports.JSS3Bucket__unlink, .{ .name = "JSS3Bucket__unlink" }); + @export(exports.JSS3Bucket__presign, .{ .name = "JSS3Bucket__presign" }); + @export(exports.JSS3Bucket__call, .{ .name = "JSS3Bucket__call" }); + @export(finalize, .{ .name = "JSS3Bucket__deinit" }); + @export(construct, .{ .name = "JSS3Bucket__construct" }); +} diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig new file mode 100644 index 0000000000..a7a4a272a5 --- /dev/null +++ b/src/bun.js/webcore/S3File.zig @@ -0,0 +1,545 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const Blob = JSC.WebCore.Blob; +const PathOrBlob = JSC.Node.PathOrBlob; +const ZigString = JSC.ZigString; +const Method = bun.http.Method; +const strings = bun.strings; +const Output = bun.Output; +const S3Bucket = @import("./S3Bucket.zig"); + +pub fn writeFormat(s3: *Blob.S3Store, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime Output.prettyFmt("S3Ref", enable_ansi_colors)); + const credentials = s3.getCredentials(); + + if (credentials.bucket.len > 0) { + try writer.print( + comptime Output.prettyFmt(" (\"{s}/{s}\") {{", enable_ansi_colors), + .{ + credentials.bucket, + s3.path(), + }, + ); + } else { + try writer.print( + comptime Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), + .{ + s3.path(), + }, + ); + } + + try S3Bucket.writeFormatCredentials(credentials, s3.options, s3.acl, Formatter, formatter, writer, enable_ansi_colors); + try formatter.writeIndent(@TypeOf(writer), writer); + try writer.writeAll("}"); + formatter.resetLine(); +} +pub fn presign(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); + } + + switch (path_or_blob) { + .path => |path| { + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); + } + const options = args.nextEat(); + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + return try getPresignUrlFrom(&blob, globalThis, options); + }, + .blob => return try getPresignUrlFrom(&path_or_blob.blob, globalThis, args.nextEat()), + } +} + +pub fn unlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); + } + + switch (path_or_blob) { + .path => |path| { + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); + } + const options = args.nextEat(); + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); + }, + .blob => |blob| { + return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, args.nextEat()); + }, + } +} + +pub fn upload(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); + } + + const data = args.nextEat() orelse { + return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to upload", .{}).throw(); + }; + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + var blob_internal: PathOrBlob = .{ .blob = blob }; + return try Blob.writeFileInternal(globalThis, &blob_internal, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); + }, + .blob => return try Blob.writeFileInternal(globalThis, &path_or_blob, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = args.nextEat(), + }), + } +} + +pub fn size(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.size(globalThis, &blob); + }, + .blob => |*blob| { + return Blob.getSize(blob, globalThis); + }, + } +} +pub fn exists(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.exists(globalThis, &blob); + }, + .blob => |*blob| { + return Blob.getExists(blob, globalThis, callframe); + }, + } +} + +fn constructS3FileInternalStore( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!Blob { + // get credentials from env + const existing_credentials = globalObject.bunVM().transpiler.env.getAWSCredentials(); + return constructS3FileWithAWSCredentials(globalObject, path, options, existing_credentials); +} +/// if the credentials have changed, we need to clone it, if not we can just ref/deref it +pub fn constructS3FileWithAWSCredentialsAndOptions( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, + default_credentials: *AWS, + default_options: bun.S3.MultiPartUpload.MultiPartUploadOptions, + default_acl: ?bun.S3.ACL, +) bun.JSError!Blob { + var aws_options = try AWS.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, globalObject); + defer aws_options.deinit(); + + const store = brk: { + if (aws_options.changed_credentials) { + break :brk Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); + } else { + break :brk Blob.Store.initS3WithReferencedCredentials(path, null, default_credentials, bun.default_allocator) catch bun.outOfMemory(); + } + }; + errdefer store.deinit(); + store.data.s3.options = aws_options.options; + store.data.s3.acl = aws_options.acl; + var blob = Blob.initWithStore(store, globalObject); + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "type")) |file_type| { + inner: { + if (file_type.isString()) { + var allocator = bun.default_allocator; + var str = file_type.toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + blob.content_type_was_set = true; + if (globalObject.bunVM().mimeType(str.slice())) |entry| { + blob.content_type = entry.value; + break :inner; + } + const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + blob.content_type = strings.copyLowercase(slice, content_type_buf); + blob.content_type_allocated = true; + } + } + } + } + } + return blob; +} + +pub fn constructS3FileWithAWSCredentials( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, + existing_credentials: AWS, +) bun.JSError!Blob { + var aws_options = try AWS.getCredentialsWithOptions(existing_credentials, .{}, options, null, globalObject); + defer aws_options.deinit(); + const store = Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); + errdefer store.deinit(); + store.data.s3.options = aws_options.options; + store.data.s3.acl = aws_options.acl; + var blob = Blob.initWithStore(store, globalObject); + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "type")) |file_type| { + inner: { + if (file_type.isString()) { + var allocator = bun.default_allocator; + var str = file_type.toSlice(globalObject, bun.default_allocator); + defer str.deinit(); + const slice = str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + blob.content_type_was_set = true; + if (globalObject.bunVM().mimeType(str.slice())) |entry| { + blob.content_type = entry.value; + break :inner; + } + const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); + blob.content_type = strings.copyLowercase(slice, content_type_buf); + blob.content_type_allocated = true; + } + } + } + } + } + return blob; +} +fn constructS3FileInternal( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!*Blob { + var ptr = Blob.new(try constructS3FileInternalStore(globalObject, path, options)); + ptr.allocator = bun.default_allocator; + return ptr; +} + +const AWS = bun.S3.AWSCredentials; + +pub const S3BlobStatTask = struct { + promise: JSC.JSPromise.Strong, + store: *Blob.Store, + usingnamespace bun.New(S3BlobStatTask); + + pub fn onS3ExistsResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + switch (result) { + .not_found => { + this.promise.resolve(globalThis, .false); + }, + .success => |_| { + // calling .exists() should not prevent it to download a bigger file + // this would make it download a slice of the actual value, if the file changes before we download it + // if (this.blob.size == Blob.max_size) { + // this.blob.size = @truncate(stat.size); + // } + this.promise.resolve(globalThis, .true); + }, + .failure => |err| { + this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn onS3SizeResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + + switch (result) { + .success => |stat| { + this.promise.resolve(globalThis, JSValue.jsNumber(stat.size)); + }, + inline .not_found, .failure => |err| { + this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn exists(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } + + pub fn size(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + + credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } + + pub fn deinit(this: *S3BlobStatTask) void { + this.store.deref(); + this.promise.deinit(); + this.destroy(); + } +}; + +pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { + if (!this.isS3()) { + return globalThis.ERR_INVALID_THIS("presign is only possible for s3:// files", .{}).throw(); + } + + var method: bun.http.Method = .GET; + var expires: usize = 86400; // 1 day default + + var credentialsWithOptions: AWS.AWSCredentialsWithOptions = .{ + .credentials = this.store.?.data.s3.getCredentials().*, + }; + defer { + credentialsWithOptions.deinit(); + } + const s3 = &this.store.?.data.s3; + + if (extra_options) |options| { + if (options.isObject()) { + if (try options.getTruthyComptime(globalThis, "method")) |method_| { + method = Method.fromJS(globalThis, method_) orelse { + return globalThis.throwInvalidArguments("method must be GET, PUT, DELETE or HEAD when using s3 protocol", .{}); + }; + } + if (try options.getOptional(globalThis, "expiresIn", i32)) |expires_| { + if (expires_ <= 0) return globalThis.throwInvalidArguments("expiresIn must be greather than 0", .{}); + expires = @intCast(expires_); + } + } + credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis); + } + const path = s3.path(); + + const result = credentialsWithOptions.credentials.signRequest(.{ + .path = path, + .method = method, + .acl = credentialsWithOptions.acl, + }, .{ .expires = expires }) catch |sign_err| { + return AWS.throwSignError(sign_err, globalThis); + }; + defer result.deinit(); + var str = bun.String.fromUTF8(result.url); + return str.transferToJS(this.globalThis); +} +pub fn getBucketName( + this: *const Blob, +) ?[]const u8 { + const store = this.store orelse return null; + if (store.data != .s3) return null; + const credentials = store.data.s3.getCredentials(); + var full_path = store.data.s3.path(); + if (strings.startsWith(full_path, "/")) { + full_path = full_path[1..]; + } + var bucket: []const u8 = credentials.bucket; + + if (bucket.len == 0) { + if (strings.indexOf(full_path, "/")) |end| { + bucket = full_path[0..end]; + if (bucket.len > 0) { + return bucket; + } + } + return null; + } + return bucket; +} + +pub fn getBucket( + this: *Blob, + globalThis: *JSC.JSGlobalObject, +) callconv(JSC.conv) JSValue { + if (getBucketName(this)) |name| { + var str = bun.String.createUTF8(name); + return str.transferToJS(globalThis); + } + return .undefined; +} +pub fn getPresignUrl(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const args = callframe.arguments_old(1); + return getPresignUrlFrom(this, globalThis, if (args.len > 0) args.ptr[0] else null); +} + +pub fn constructInternalJS( + globalObject: *JSC.JSGlobalObject, + path: JSC.Node.PathLike, + options: ?JSC.JSValue, +) bun.JSError!JSValue { + const blob = try constructS3FileInternal(globalObject, path, options); + return blob.toJS(globalObject); +} + +pub fn toJSUnchecked( + globalObject: *JSC.JSGlobalObject, + this: *Blob, +) JSValue { + return BUN__createJSS3FileUnsafely(globalObject, this); +} + +pub fn constructInternal( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!*Blob { + const vm = globalObject.bunVM(); + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(vm, arguments); + defer args.deinit(); + + const path = (try JSC.Node.PathLike.fromJS(globalObject, &args)) orelse { + return globalObject.throwInvalidArguments("Expected file path string", .{}); + }; + return constructS3FileInternal(globalObject, path, args.nextEat()); +} + +pub fn construct( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) callconv(JSC.conv) ?*Blob { + return constructInternal(globalObject, callframe) catch |err| switch (err) { + error.JSError => null, + error.OutOfMemory => { + _ = globalObject.throwOutOfMemoryValue(); + return null; + }, + }; +} +pub fn hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { + JSC.markBinding(@src()); + const blob = value.as(Blob) orelse return false; + return blob.isS3(); +} + +comptime { + @export(exports.JSS3File__presign, .{ .name = "JSS3File__presign" }); + @export(construct, .{ .name = "JSS3File__construct" }); + @export(hasInstance, .{ .name = "JSS3File__hasInstance" }); + @export(getBucket, .{ .name = "JSS3File__bucket" }); +} + +pub const exports = struct { + pub const JSS3File__presign = JSC.toJSHostFunctionWithContext(Blob, getPresignUrl); +}; +extern fn BUN__createJSS3File(*JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSValue; +extern fn BUN__createJSS3FileUnsafely(*JSC.JSGlobalObject, *Blob) callconv(JSC.conv) JSValue; +pub fn createJSS3File(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { + return BUN__createJSS3File(globalObject, callframe); +} diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index a154a8cb75..23e9f3b03b 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -43,33 +43,11 @@ const Request = JSC.WebCore.Request; const libuv = bun.windows.libuv; -const AWSCredentials = @import("../../s3.zig").AWSCredentials; -const S3MultiPartUpload = @import("../../s3.zig").MultiPartUpload; +const S3 = @import("../../s3.zig"); +const AWSCredentials = S3.AWSCredentials; +const S3MultiPartUpload = S3.MultiPartUpload; const AWS = AWSCredentials; - -const PathOrBlob = union(enum) { - path: JSC.Node.PathOrFileDescriptor, - blob: Blob, - - pub fn fromJSNoCopy(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice) bun.JSError!PathOrBlob { - if (try JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, bun.default_allocator)) |path| { - return PathOrBlob{ - .path = path, - }; - } - - const arg = args.nextEat() orelse { - return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", .undefined); - }; - if (arg.as(Blob)) |blob| { - return PathOrBlob{ - .blob = blob.*, - }; - } - return ctx.throwInvalidArgumentTypeValue("destination", "path, file descriptor, or Blob", arg); - } -}; - +const PathOrBlob = JSC.Node.PathOrBlob; const WriteFilePromise = @import("./blob/WriteFile.zig").WriteFilePromise; const WriteFileWaitFromLockedValueTask = @import("./blob/WriteFile.zig").WriteFileWaitFromLockedValueTask; const NewReadFileHandler = @import("./blob/ReadFile.zig").NewReadFileHandler; @@ -77,6 +55,8 @@ const WriteFile = @import("./blob/WriteFile.zig").WriteFile; const ReadFile = @import("./blob/ReadFile.zig").ReadFile; const WriteFileWindows = @import("./blob/WriteFile.zig").WriteFileWindows; +const S3File = @import("./S3File.zig"); + pub const Blob = struct { const bloblog = Output.scoped(.Blob, false); @@ -718,14 +698,8 @@ pub const Blob = struct { { const store = this.store.?; switch (store.data) { - .s3 => |s3| { - try writer.writeAll(comptime Output.prettyFmt("S3Ref", enable_ansi_colors)); - try writer.print( - comptime Output.prettyFmt(" (\"{s}\")", enable_ansi_colors), - .{ - s3.pathlike.slice(), - }, - ); + .s3 => |*s3| { + try S3File.writeFormat(s3, Formatter, formatter, writer, enable_ansi_colors); }, .file => |file| { try writer.writeAll(comptime Output.prettyFmt("FileRef", enable_ansi_colors)); @@ -923,6 +897,7 @@ pub const Blob = struct { const Wrapper = struct { promise: JSC.JSPromise.Strong, + store: *Store, pub usingnamespace bun.New(@This()); pub fn resolve(result: AWS.S3UploadResult, this: *@This()) void { @@ -930,7 +905,7 @@ pub const Blob = struct { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(0)), .failure => |err| { - this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject)); + this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, this.store.getPath())); }, } } @@ -939,6 +914,8 @@ pub const Blob = struct { fn deinit(this: *@This()) void { this.promise.deinit(); + this.store.deref(); + this.destroy(); } }; @@ -946,8 +923,10 @@ pub const Blob = struct { const promise_value = promise.value(); const proxy = ctx.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - aws_options.credentials.s3Upload(s3.path(), "", destination_blob.contentTypeOrMimeType(), proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ + destination_blob.store.?.ref(); + aws_options.credentials.s3Upload(s3.path(), "", destination_blob.contentTypeOrMimeType(), aws_options.acl, proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ .promise = promise, + .store = destination_blob.store.?, })); return promise_value; } @@ -1064,7 +1043,7 @@ pub const Blob = struct { source_blob, @truncate(s3.options.partSize * S3MultiPartUpload.OneMiB), ), ctx)) |stream| { - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, aws_options.options, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); } else { return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); } @@ -1079,7 +1058,7 @@ pub const Blob = struct { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(this.store.data.bytes.len)), .failure => |err| { - this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject)); + this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, this.store.getPath())); }, } } @@ -1095,7 +1074,7 @@ pub const Blob = struct { const promise = JSC.JSPromise.Strong.init(ctx); const promise_value = promise.value(); - aws_options.credentials.s3Upload(s3.path(), bytes.slice(), destination_blob.contentTypeOrMimeType(), proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ + aws_options.credentials.s3Upload(s3.path(), bytes.slice(), destination_blob.contentTypeOrMimeType(), aws_options.acl, proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ .store = store, .promise = promise, })); @@ -1109,7 +1088,7 @@ pub const Blob = struct { source_blob, @truncate(s3.options.partSize * S3MultiPartUpload.OneMiB), ), ctx)) |stream| { - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, s3.options, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, s3.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); } else { return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); } @@ -1287,7 +1266,7 @@ pub const Blob = struct { const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); } destination_blob.detach(); return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); @@ -1335,7 +1314,7 @@ pub const Blob = struct { } const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); } destination_blob.detach(); return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); @@ -1593,269 +1572,6 @@ pub const Blob = struct { return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written)); } - - pub fn JSS3File_upload_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - errdefer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - - if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { - return globalThis.throwInvalidArguments("S3.upload(pathOrS3, blob) expects a S3 or path to upload", .{}); - } - - const data = args.nextEat() orelse { - return globalThis.throwInvalidArguments("S3.upload(pathOrS3, blob) expects a Blob-y thing to upload", .{}); - }; - - switch (path_or_blob) { - .path => |path| { - const options = args.nextEat(); - if (path == .fd) { - return globalThis.throwInvalidArguments("S3.upload(pathOrS3, blob) expects a S3 or path to upload", .{}); - } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); - defer blob.deinit(); - - var blob_internal: PathOrBlob = .{ .blob = blob }; - return try writeFileInternal(globalThis, &blob_internal, data, .{ - .mkdirp_if_not_exists = false, - .extra_options = options, - }); - }, - .blob => return try writeFileInternal(globalThis, &path_or_blob, data, .{ - .mkdirp_if_not_exists = false, - .extra_options = args.nextEat(), - }), - } - } - - pub fn JSS3File_size_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - errdefer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - - if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { - return globalThis.throwInvalidArguments("S3.size(pathOrS3) expects a S3 or path to get size", .{}); - } - - switch (path_or_blob) { - .path => |path| { - const options = args.nextEat(); - if (path == .fd) { - return globalThis.throwInvalidArguments("S3.size(pathOrS3) expects a S3 or path to get size", .{}); - } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); - defer blob.deinit(); - - return S3BlobStatTask.size(globalThis, &blob); - }, - .blob => |*blob| { - return getSize(blob, globalThis); - }, - } - } - pub fn JSS3File_exists_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - errdefer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - - if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { - return globalThis.throwInvalidArguments("S3.exists(pathOrS3) expects a S3 or path to check if it exists", .{}); - } - - switch (path_or_blob) { - .path => |path| { - const options = args.nextEat(); - if (path == .fd) { - return globalThis.throwInvalidArguments("S3.exists(pathOrS3) expects a S3 or path to check if it exists", .{}); - } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); - defer blob.deinit(); - - return S3BlobStatTask.exists(globalThis, &blob); - }, - .blob => |*blob| { - return getExists(blob, globalThis, callframe); - }, - } - } - - pub export fn JSS3File__exists(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { - return JSS3File_exists_(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return .zero; - }, - }; - } - pub export fn JSS3File__size(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { - return JSS3File_size_(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return .zero; - }, - }; - } - pub export fn JSS3File__upload(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { - return JSS3File_upload_(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return .zero; - }, - }; - } - pub fn JSS3File_presign_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - errdefer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - - if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { - return globalThis.throwInvalidArguments("S3.presign(pathOrS3, options) expects a S3 or path to presign", .{}); - } - - switch (path_or_blob) { - .path => |path| { - if (path == .fd) { - return globalThis.throwInvalidArguments("S3.presign(pathOrS3, options) expects a S3 or path to presign", .{}); - } - const options = args.nextEat(); - var blob = try constructS3FileInternalStore(globalThis, path.path, options); - defer blob.deinit(); - return try getPresignUrlFrom(&blob, globalThis, options); - }, - .blob => return try getPresignUrlFrom(&path_or_blob.blob, globalThis, args.nextEat()), - } - } - - pub export fn JSS3File__presign(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { - return JSS3File_presign_(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return .zero; - }, - }; - } - pub fn JSS3File_unlink_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - // accept a path or a blob - var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); - errdefer { - if (path_or_blob == .path) { - path_or_blob.path.deinit(); - } - } - if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { - return globalThis.throwInvalidArguments("S3.unlink(pathOrS3) expects a S3 or path to delete", .{}); - } - - switch (path_or_blob) { - .path => |path| { - if (path == .fd) { - return globalThis.throwInvalidArguments("S3.unlink(pathOrS3) expects a S3 or path to delete", .{}); - } - const options = args.nextEat(); - var blob = try constructS3FileInternalStore(globalThis, path.path, options); - defer blob.deinit(); - return try blob.store.?.data.s3.unlink(globalThis, options); - }, - .blob => |blob| { - return try blob.store.?.data.s3.unlink(globalThis, args.nextEat()); - }, - } - } - - pub export fn JSS3File__unlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSValue { - return JSS3File_unlink_(globalThis, callframe) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return .zero; - }, - }; - } - pub export fn JSS3File__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { - JSC.markBinding(@src()); - const blob = value.as(Blob) orelse return false; - return blob.isS3(); - } - - pub export fn JSDOMFile__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { - JSC.markBinding(@src()); - const blob = value.as(Blob) orelse return false; - return blob.is_jsdom_file; - } - extern fn BUN__createJSS3FileConstructor(*JSC.JSGlobalObject) JSValue; - - pub fn getJSS3FileConstructor( - globalObject: *JSC.JSGlobalObject, - _: *JSC.JSObject, - ) callconv(JSC.conv) JSValue { - return BUN__createJSS3FileConstructor(globalObject); - } - export fn JSS3File__construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) ?*Blob { - const vm = globalThis.bunVM(); - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(vm, arguments); - defer args.deinit(); - - const path_or_fd = (JSC.Node.PathLike.fromJS(globalThis, &args)) catch |err| switch (err) { - error.JSError => null, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return null; - }, - }; - if (path_or_fd == null) { - globalThis.throwInvalidArguments("Expected file path string", .{}) catch return null; - return null; - } - return constructS3FileInternal(globalThis, path_or_fd.?, args.nextEat()) catch |err| switch (err) { - error.JSError => null, - error.OutOfMemory => { - globalThis.throwOutOfMemory() catch {}; - return null; - }, - }; - } export fn JSDOMFile__construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) ?*Blob { return JSDOMFile__construct_(globalThis, callframe) catch |err| switch (err) { error.JSError => null, @@ -1986,66 +1702,7 @@ pub const Blob = struct { } comptime { - if (!JSC.is_bindgen) { - _ = JSDOMFile__hasInstance; - } - } - - fn constructS3FileInternalStore( - globalObject: *JSC.JSGlobalObject, - path: JSC.Node.PathLike, - options: ?JSC.JSValue, - ) bun.JSError!Blob { - - // get ENV config - var aws_options = try AWS.getCredentialsWithOptions(globalObject.bunVM().transpiler.env.getAWSCredentials(), options, globalObject); - defer aws_options.deinit(); - const store = Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); - errdefer store.deinit(); - store.data.s3.options = aws_options.options; - - var blob = Blob.initWithStore(store, globalObject); - if (options) |opts| { - if (try opts.getTruthy(globalObject, "type")) |file_type| { - inner: { - if (file_type.isString()) { - var allocator = bun.default_allocator; - var str = file_type.toSlice(globalObject, bun.default_allocator); - defer str.deinit(); - const slice = str.slice(); - if (!strings.isAllASCII(slice)) { - break :inner; - } - blob.content_type_was_set = true; - if (globalObject.bunVM().mimeType(str.slice())) |entry| { - blob.content_type = entry.value; - break :inner; - } - const content_type_buf = allocator.alloc(u8, slice.len) catch bun.outOfMemory(); - blob.content_type = strings.copyLowercase(slice, content_type_buf); - blob.content_type_allocated = true; - } - } - } - } - return blob; - } - fn constructS3FileInternal( - globalObject: *JSC.JSGlobalObject, - path: JSC.Node.PathLike, - options: ?JSC.JSValue, - ) bun.JSError!*Blob { - var ptr = Blob.new(try constructS3FileInternalStore(globalObject, path, options)); - ptr.allocator = bun.default_allocator; - return ptr; - } - fn constructS3FileInternalJS( - globalObject: *JSC.JSGlobalObject, - path: JSC.Node.PathLike, - options: ?JSC.JSValue, - ) bun.JSError!JSC.JSValue { - var ptr = try constructS3FileInternal(globalObject, path, options); - return ptr.toJS(globalObject); + _ = JSDOMFile__hasInstance; } pub fn constructBunFile( @@ -2063,8 +1720,8 @@ pub const Blob = struct { const options = if (arguments.len >= 2) arguments[1] else null; if (path == .path) { - if (strings.startsWith(path.path.slice(), "s3://")) { - return try constructS3FileInternalJS(globalObject, path.path, options); + if (strings.hasPrefixComptime(path.path.slice(), "s3://")) { + return try S3File.constructInternalJS(globalObject, path.path, options); } } defer path.deinitAndUnprotect(); @@ -2105,21 +1762,6 @@ pub const Blob = struct { return ptr.toJS(globalObject); } - pub fn constructS3File( - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const vm = globalObject.bunVM(); - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(vm, arguments); - defer args.deinit(); - - const path = (try JSC.Node.PathLike.fromJS(globalObject, &args)) orelse { - return globalObject.throwInvalidArguments("Expected file path string", .{}); - }; - return constructS3FileInternalJS(globalObject, path, args.nextEat()); - } - pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject, comptime check_s3: bool) Blob { var vm = globalThis.bunVM(); const allocator = bun.default_allocator; @@ -2208,6 +1850,14 @@ pub const Blob = struct { } else 0; } + pub fn getPath(this: *const Store) ?[]const u8 { + return switch (this.data) { + .bytes => |*bytes| if (bytes.stored_name.len > 0) bytes.stored_name.slice() else null, + .file => |*file| if (file.pathlike == .path) file.pathlike.path.slice() else null, + .s3 => |*s3| s3.pathlike.slice(), + }; + } + pub fn size(this: *const Store) SizeType { return switch (this.data) { .bytes => this.data.bytes.len, @@ -2248,7 +1898,34 @@ pub const Blob = struct { var this = bun.cast(*Store, ptr); this.deref(); } + pub fn initS3WithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS, allocator: std.mem.Allocator) !*Store { + var path = pathlike; + // this actually protects/refs the pathlike + path.toThreadSafe(); + const store = Blob.Store.new(.{ + .data = .{ + .s3 = S3Store.initWithReferencedCredentials( + path, + mime_type orelse brk: { + const sliced = path.slice(); + if (sliced.len > 0) { + var extname = std.fs.path.extension(sliced); + extname = std.mem.trim(u8, extname, "."); + if (http.MimeType.byExtensionNoDefault(extname)) |mime| { + break :brk mime; + } + } + break :brk null; + }, + credentials, + ), + }, + .allocator = allocator, + .ref_count = std.atomic.Value(u32).init(1), + }); + return store; + } pub fn initS3(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; // this actually protects/refs the pathlike @@ -3772,6 +3449,7 @@ pub const Blob = struct { mime_type: http.MimeType = http.MimeType.other, credentials: ?*AWSCredentials, options: S3MultiPartUpload.MultiPartUploadOptions = .{}, + acl: ?S3.ACL = null, pub fn isSeekable(_: *const @This()) ?bool { return true; } @@ -3782,7 +3460,7 @@ pub const Blob = struct { } pub fn getCredentialsWithOptions(this: *const @This(), options: ?JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!AWS.AWSCredentialsWithOptions { - return AWS.getCredentialsWithOptions(this.getCredentials().*, options, globalObject); + return AWS.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, globalObject); } pub fn path(this: *@This()) []const u8 { @@ -3790,16 +3468,21 @@ pub const Blob = struct { // normalize start and ending if (strings.endsWith(path_name, "/")) { path_name = path_name[0..path_name.len]; + } else if (strings.endsWith(path_name, "\\")) { + path_name = path_name[0 .. path_name.len - 1]; } if (strings.startsWith(path_name, "/")) { path_name = path_name[1..]; + } else if (strings.startsWith(path_name, "\\")) { + path_name = path_name[1..]; } return path_name; } - pub fn unlink(this: *@This(), globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { + pub fn unlink(this: *@This(), store: *Store, globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { const Wrapper = struct { promise: JSC.JSPromise.Strong, + store: *Store, pub usingnamespace bun.New(@This()); @@ -3810,18 +3493,14 @@ pub const Blob = struct { .success => { self.promise.resolve(globalObject, .true); }, - .not_found => { - const js_err = globalObject.createErrorInstance("File not found", .{}); - js_err.put(globalObject, ZigString.static("code"), ZigString.init("FileNotFound").toJS(globalObject)); - self.promise.reject(globalObject, js_err); - }, - .failure => |err| { - self.promise.rejectOnNextTick(globalObject, err.toJS(globalObject)); + inline .not_found, .failure => |err| { + self.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, self.store.getPath())); }, } } fn deinit(self: *@This()) void { + self.store.deref(); self.promise.deinit(); self.destroy(); } @@ -3834,11 +3513,20 @@ pub const Blob = struct { defer aws_options.deinit(); aws_options.credentials.s3Delete(this.path(), @ptrCast(&Wrapper.resolve), Wrapper.new(.{ .promise = promise, + .store = store, // store is needed in case of not found error }), proxy); + store.ref(); return value; } - + pub fn initWithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS) S3Store { + credentials.ref(); + return .{ + .credentials = credentials, + .pathlike = pathlike, + .mime_type = mime_type orelse http.MimeType.other, + }; + } pub fn init(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials) S3Store { return .{ .credentials = credentials.dupe(), @@ -4156,13 +3844,8 @@ pub const Blob = struct { } JSC.AnyPromise.wrap(.{ .normal = this.promise.get() }, this.globalThis, S3BlobDownloadTask.callHandler, .{ this, bytes }); }, - .not_found => { - const js_err = this.globalThis.createErrorInstance("File not found", .{}); - js_err.put(this.globalThis, ZigString.static("code"), ZigString.init("FileNotFound").toJS(this.globalThis)); - this.promise.reject(this.globalThis, js_err); - }, - .failure => |err| { - this.promise.rejectOnNextTick(this.globalThis, err.toJS(this.globalThis)); + inline .not_found, .failure => |err| { + this.promise.rejectOnNextTick(this.globalThis, err.toJS(this.globalThis, this.blob.store.?.getPath())); }, } } @@ -4198,83 +3881,7 @@ pub const Blob = struct { pub fn deinit(this: *S3BlobDownloadTask) void { this.blob.store.?.deref(); - this.poll_ref.unrefOnNextTick(this.globalThis.bunVM()); - this.promise.deinit(); - this.destroy(); - } - }; - - const S3BlobStatTask = struct { - promise: JSC.JSPromise.Strong, - usingnamespace bun.New(S3BlobStatTask); - - pub fn onS3ExistsResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { - defer this.deinit(); - const globalThis = this.promise.globalObject().?; - switch (result) { - .not_found => { - this.promise.resolve(globalThis, .false); - }, - .success => |_| { - // calling .exists() should not prevent it to download a bigger file - // this would make it download a slice of the actual value, if the file changes before we download it - // if (this.blob.size == Blob.max_size) { - // this.blob.size = @truncate(stat.size); - // } - this.promise.resolve(globalThis, .true); - }, - .failure => |err| { - this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis)); - }, - } - } - - pub fn onS3SizeResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { - defer this.deinit(); - const globalThis = this.promise.globalObject().?; - - switch (result) { - .not_found => { - const js_err = globalThis.createErrorInstance("File not Found", .{}); - js_err.put(globalThis, ZigString.static("code"), ZigString.static("FileNotFound").toJS(globalThis)); - this.promise.rejectOnNextTick(globalThis, js_err); - }, - .success => |stat| { - this.promise.resolve(globalThis, JSValue.jsNumber(stat.size)); - }, - .failure => |err| { - this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis)); - }, - } - } - - pub fn exists(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { - const this = S3BlobStatTask.new(.{ - .promise = JSC.JSPromise.Strong.init(globalThis), - }); - const promise = this.promise.value(); - const credentials = blob.store.?.data.s3.getCredentials(); - const path = blob.store.?.data.s3.path(); - const env = globalThis.bunVM().transpiler.env; - - credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); - return promise; - } - - pub fn size(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { - const this = S3BlobStatTask.new(.{ - .promise = JSC.JSPromise.Strong.init(globalThis), - }); - const promise = this.promise.value(); - const credentials = blob.store.?.data.s3.getCredentials(); - const path = blob.store.?.data.s3.path(); - const env = globalThis.bunVM().transpiler.env; - - credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); - return promise; - } - - pub fn deinit(this: *S3BlobStatTask) void { + this.poll_ref.unref(this.globalThis.bunVM()); this.promise.deinit(); this.destroy(); } @@ -4340,7 +3947,7 @@ pub const Blob = struct { return JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is detached", .{})); }; return switch (store.data) { - .s3 => |*s3| try s3.unlink(globalThis, args.nextEat()), + .s3 => |*s3| try s3.unlink(store, globalThis, args.nextEat()), .file => |file| file.unlink(globalThis), else => JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is read-only", .{})), }; @@ -4353,57 +3960,11 @@ pub const Blob = struct { _: *JSC.CallFrame, ) bun.JSError!JSValue { if (this.isS3()) { - return S3BlobStatTask.exists(globalThis, this); + return S3File.S3BlobStatTask.exists(globalThis, this); } return JSC.JSPromise.resolvedPromiseValue(globalThis, this.getExistsSync()); } - pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_options: ?JSValue) bun.JSError!JSValue { - if (this.isS3()) { - var method: bun.http.Method = .GET; - var expires: usize = 86400; // 1 day default - - var credentialsWithOptions: AWS.AWSCredentialsWithOptions = .{ - .credentials = this.store.?.data.s3.getCredentials().*, - }; - defer { - credentialsWithOptions.deinit(); - } - if (extra_options) |options| { - if (options.isObject()) { - if (try options.getTruthyComptime(globalThis, "method")) |method_| { - method = Method.fromJS(globalThis, method_) orelse { - return globalThis.throwInvalidArguments("method must be GET, PUT, DELETE or HEAD when using s3 protocol", .{}); - }; - } - if (try options.getOptional(globalThis, "expiresIn", i32)) |expires_| { - if (expires_ <= 0) return globalThis.throwInvalidArguments("expiresIn must be greather than 0", .{}); - expires = @intCast(expires_); - } - } - credentialsWithOptions = try this.store.?.data.s3.getCredentialsWithOptions(options, globalThis); - } - const path = this.store.?.data.s3.path(); - - const result = credentialsWithOptions.credentials.signRequest(.{ - .path = path, - .method = method, - }, .{ .expires = expires }) catch |sign_err| { - return AWS.throwSignError(sign_err, globalThis); - }; - defer result.deinit(); - var str = bun.String.fromUTF8(result.url); - return str.transferToJS(this.globalThis); - } - - return globalThis.throwError(error.NotSupported, "is only possible to presign s3:// files"); - } - - pub fn getPresignUrl(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const args = callframe.arguments_old(1); - return getPresignUrlFrom(this, globalThis, if (args.len > 0) args.ptr[0] else null); - } - pub const FileStreamWrapper = struct { promise: JSC.JSPromise.Strong, readable_stream_ref: JSC.WebCore.ReadableStream.Strong, @@ -4472,7 +4033,7 @@ pub const Blob = struct { const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(path, readable_stream, globalThis, aws_options.options, this.contentTypeOrMimeType(), proxy_url, null, undefined); + return (if (extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(path, readable_stream, globalThis, aws_options.options, aws_options.acl, this.contentTypeOrMimeType(), proxy_url, null, undefined); } if (store.data != .file) { @@ -4987,17 +4548,6 @@ pub const Blob = struct { return if (this.getNameString()) |name| name.toJS(globalThis) else .undefined; } - pub fn getBucket( - this: *Blob, - globalThis: *JSC.JSGlobalObject, - ) JSValue { - if (this.getBucketName()) |name| { - var str = bun.String.createUTF8(name); - return str.transferToJS(globalThis); - } - return .undefined; - } - pub fn setName( this: *Blob, jsThis: JSC.JSValue, @@ -5048,30 +4598,6 @@ pub const Blob = struct { return null; } - pub fn getBucketName( - this: *const Blob, - ) ?[]const u8 { - const store = this.store orelse return null; - if (store.data != .s3) return null; - const credentials = store.data.s3.getCredentials(); - var full_path = store.data.s3.path(); - if (strings.startsWith(full_path, "/")) { - full_path = full_path[1..]; - } - var bucket: []const u8 = credentials.bucket; - - if (bucket.len == 0) { - if (strings.indexOf(full_path, "/")) |end| { - bucket = full_path[0..end]; - if (bucket.len > 0) { - return bucket; - } - } - return null; - } - return bucket; - } - // TODO: Move this to a separate `File` object or BunFile pub fn getLastModified( this: *Blob, @@ -5126,7 +4652,7 @@ pub const Blob = struct { pub fn getSize(this: *Blob, globalThis: *JSC.JSGlobalObject) JSValue { if (this.size == Blob.max_size) { if (this.isS3()) { - return S3BlobStatTask.size(globalThis, this); + return S3File.S3BlobStatTask.size(globalThis, this); } this.resolveSize(); if (this.size == Blob.max_size and this.store != null) { @@ -5441,8 +4967,12 @@ pub const Blob = struct { // if (comptime Environment.allow_assert) { // assert(this.allocator != null); // } - this.calculateEstimatedByteSize(); + + if (this.isS3()) { + return S3File.toJSUnchecked(globalObject, this); + } + return Blob.toJSUnchecked(globalObject, this); } @@ -6606,3 +6136,9 @@ pub const InlineBlob = extern struct { }; const assert = bun.assert; + +pub export fn JSDOMFile__hasInstance(_: JSC.JSValue, _: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(JSC.conv) bool { + JSC.markBinding(@src()); + const blob = value.as(Blob) orelse return false; + return blob.is_jsdom_file; +} diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index ba7e022fa3..d09c7a0c1d 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -125,6 +125,7 @@ export default [ }), define({ name: "Blob", + final: false, construct: true, finalize: true, JSType: "0b11101110", @@ -168,13 +169,6 @@ export default [ // Non-standard, s3 + BunFile support unlink: { fn: "doUnlink", length: 0 }, write: { fn: "doWrite", length: 2 }, - // Non-standard, s3 support - bucket: { - cache: true, - getter: "getBucket", - }, - presign: { fn: "getPresignUrl", length: 1 }, - size: { getter: "getSize", }, diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 38e737ea52..b0e7ff6056 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -804,7 +804,7 @@ pub const Fetch = struct { }; pub const FetchTasklet = struct { - pub const FetchTaskletStream = JSC.WebCore.FetchTaskletChunkedRequestSink; + pub const FetchTaskletStream = JSC.WebCore.NetworkSink; const log = Output.scoped(.FetchTasklet, false); sink: ?*FetchTaskletStream.JSSink = null, @@ -3255,6 +3255,7 @@ pub const Fetch = struct { var credentialsWithOptions: s3.AWSCredentials.AWSCredentialsWithOptions = .{ .credentials = globalThis.bunVM().transpiler.env.getAWSCredentials(), .options = .{}, + .acl = null, }; defer { credentialsWithOptions.deinit(); @@ -3264,7 +3265,7 @@ pub const Fetch = struct { if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| { if (s3_options.isObject()) { s3_options.ensureStillAlive(); - credentialsWithOptions = try s3.AWSCredentials.getCredentialsWithOptions(credentialsWithOptions.credentials, s3_options, globalThis); + credentialsWithOptions = try s3.AWSCredentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, globalThis); } } } @@ -3338,6 +3339,7 @@ pub const Fetch = struct { body.ReadableStream.get().?, globalThis, credentialsWithOptions.options, + credentialsWithOptions.acl, if (headers) |h| h.getContentType() else null, proxy_url, @ptrCast(&Wrapper.resolve), @@ -3379,42 +3381,15 @@ pub const Fetch = struct { } const content_type = if (headers) |h| h.getContentType() else null; + var header_buffer: [10]picohttp.Header = undefined; if (range) |range_| { - const _headers = result.headers(); - var headersWithRange: [5]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - .{ .name = "range", .value = range_ }, - }; - - setHeaders(&headers, &headersWithRange, allocator); + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + setHeaders(&headers, _headers, allocator); } else if (content_type) |ct| { if (ct.len > 0) { - const _headers = result.headers(); - if (_headers.len > 4) { - var headersWithContentType: [6]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - _headers[4], - .{ .name = "Content-Type", .value = ct }, - }; - setHeaders(&headers, &headersWithContentType, allocator); - } else { - var headersWithContentType: [5]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - .{ .name = "Content-Type", .value = ct }, - }; - - setHeaders(&headers, &headersWithContentType, allocator); - } + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = ct }); + setHeaders(&headers, _headers, allocator); } else { setHeaders(&headers, result.headers(), allocator); } diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 0d437358ea..8516f12158 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -482,7 +482,7 @@ pub const StreamStart = union(Tag) { FileSink: FileSinkOptions, HTTPSResponseSink: void, HTTPResponseSink: void, - FetchTaskletChunkedRequestSink: void, + NetworkSink: void, ready: void, owned_and_done: bun.ByteList, done: bun.ByteList, @@ -509,7 +509,7 @@ pub const StreamStart = union(Tag) { FileSink, HTTPSResponseSink, HTTPResponseSink, - FetchTaskletChunkedRequestSink, + NetworkSink, ready, owned_and_done, done, @@ -660,7 +660,7 @@ pub const StreamStart = union(Tag) { }, }; }, - .FetchTaskletChunkedRequestSink, .HTTPSResponseSink, .HTTPResponseSink => { + .NetworkSink, .HTTPSResponseSink, .HTTPResponseSink => { var empty = true; var chunk_size: JSC.WebCore.Blob.SizeType = 2048; @@ -2650,7 +2650,7 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { } pub const HTTPSResponseSink = HTTPServerWritable(true); pub const HTTPResponseSink = HTTPServerWritable(false); -pub const FetchTaskletChunkedRequestSink = struct { +pub const NetworkSink = struct { task: ?HTTPWritableStream = null, signal: Signal = .{}, globalThis: *JSGlobalObject = undefined, @@ -2658,13 +2658,14 @@ pub const FetchTaskletChunkedRequestSink = struct { buffer: bun.io.StreamBuffer, ended: bool = false, done: bool = false, + cancel: bool = false, encoded: bool = true, endPromise: JSC.JSPromise.Strong = .{}, auto_flusher: AutoFlusher = AutoFlusher{}, - pub usingnamespace bun.New(FetchTaskletChunkedRequestSink); + pub usingnamespace bun.New(NetworkSink); const HTTPWritableStream = union(enum) { fetch: *JSC.WebCore.Fetch.FetchTasklet, s3_upload: *S3MultiPartUpload, @@ -2689,6 +2690,16 @@ pub const FetchTaskletChunkedRequestSink = struct { AutoFlusher.registerDeferredMicrotaskWithTypeUnchecked(@This(), this, this.globalThis.bunVM()); } + pub fn path(this: *@This()) ?[]const u8 { + if (this.task) |task| { + return switch (task) { + .s3_upload => |s3| s3.path, + else => null, + }; + } + return null; + } + pub fn onAutoFlush(this: *@This()) bool { if (this.done) { this.auto_flusher.registered = false; @@ -2819,6 +2830,7 @@ pub const FetchTaskletChunkedRequestSink = struct { this.ended = true; this.done = true; this.signal.close(null); + this.cancel = true; this.finalize(); } @@ -2963,7 +2975,7 @@ pub const FetchTaskletChunkedRequestSink = struct { return this.buffer.memoryCost(); } - const name = "FetchTaskletChunkedRequestSink"; + const name = "NetworkSink"; pub const JSSink = NewJSSink(@This(), name); }; pub const BufferedReadableStreamAction = enum { diff --git a/src/bun.zig b/src/bun.zig index 77d75c1f0d..db49641f88 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -4221,3 +4221,6 @@ pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPath pub fn deleteAll() void {} }; pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; + +pub const S3 = @import("./s3.zig"); +pub const AWSCredentials = S3.AWSCredentials; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index daf15ed5b5..64d5272f8c 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -59,6 +59,8 @@ export interface ClassDefinition { JSType?: string; noConstructor?: boolean; + final?: boolean; + // Do not try to track the `this` value in the constructor automatically. // That is a memory leak. wantsThis?: never; diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index c3333635a4..1875972f61 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -456,11 +456,11 @@ void ${proto}::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) `; } -function generatePrototypeHeader(typename) { +function generatePrototypeHeader(typename, final = true) { const proto = prototypeName(typename); return ` -class ${proto} final : public JSC::JSNonFinalObject { +class ${proto} ${final ? "final" : ""} : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; @@ -483,7 +483,7 @@ class ${proto} final : public JSC::JSNonFinalObject { return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - private: + protected: ${proto}(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, structure) { @@ -537,7 +537,7 @@ class ${name} final : public JSC::InternalFunction { static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*); DECLARE_EXPORT_INFO; - private: + protected: ${name}(JSC::VM& vm, JSC::Structure* structure); void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, ${prototypeName(typeName)}* prototype); }; @@ -1250,8 +1250,10 @@ function generateClassHeader(typeName, obj: ClassDefinition) { suffix += `JSC::JSValue getInternalProperties(JSC::VM &vm, JSC::JSGlobalObject *globalObject, ${name}*);`; } + const final = obj.final ?? true; + return ` - class ${name} final : public JSC::JSDestructibleObject { + class ${name}${final ? " final" : ""} : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; static ${name}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); @@ -1652,7 +1654,12 @@ ${DEFINE_VISIT_CHILDREN} } function generateHeader(typeName, obj) { - return generateClassHeader(typeName, obj).trim() + "\n\n"; + const fields = [ + generateClassHeader(typeName, obj).trim() + "\n\n", + !(obj.final ?? true) ? generatePrototypeHeader(typeName, false) : null, + ].filter(Boolean); + + return "\n" + fields.join("\n").trim(); } function generateImpl(typeName, obj) { @@ -1660,7 +1667,7 @@ function generateImpl(typeName, obj) { const proto = obj.proto; return [ - generatePrototypeHeader(typeName), + (obj.final ?? true) ? generatePrototypeHeader(typeName, true) : null, !obj.noConstructor ? generateConstructorHeader(typeName).trim() + "\n" : null, generatePrototype(typeName, obj).trim(), !obj.noConstructor ? generateConstructorImpl(typeName, obj).trim() : null, @@ -2059,7 +2066,7 @@ function generateLazyClassStructureHeader(typeName, { klass = {}, proto = {}, zi return ` JSC::Structure* ${className(typeName)}Structure() const { return m_${className(typeName)}.getInitializedOnMainThread(this); } JSC::JSObject* ${className(typeName)}Constructor() const { return m_${className(typeName)}.constructorInitializedOnMainThread(this); } - JSC::JSValue ${className(typeName)}Prototype() const { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } + JSC::JSObject* ${className(typeName)}Prototype() const { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } JSC::LazyClassStructure m_${className(typeName)}; `.trim(); } diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index afd9b36bdc..7ec71fa427 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -1,12 +1,6 @@ import { join, resolve } from "path"; -const classes = [ - "ArrayBufferSink", - "FileSink", - "HTTPResponseSink", - "HTTPSResponseSink", - "FetchTaskletChunkedRequestSink", -]; +const classes = ["ArrayBufferSink", "FileSink", "HTTPResponseSink", "HTTPSResponseSink", "NetworkSink"]; function names(name) { return { diff --git a/src/env_loader.zig b/src/env_loader.zig index 8ea780553f..29cfcb7c08 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -125,6 +125,7 @@ pub const Loader = struct { var region: []const u8 = ""; var endpoint: []const u8 = ""; var bucket: []const u8 = ""; + var session_token: []const u8 = ""; if (this.get("S3_ACCESS_KEY_ID")) |access_key| { accessKeyId = access_key; @@ -152,12 +153,18 @@ pub const Loader = struct { } else if (this.get("AWS_BUCKET")) |bucket_| { bucket = bucket_; } + if (this.get("S3_SESSION_TOKEN")) |token| { + session_token = token; + } else if (this.get("AWS_SESSION_TOKEN")) |token| { + session_token = token; + } this.aws_credentials = .{ .accessKeyId = accessKeyId, .secretAccessKey = secretAccessKey, .region = region, .endpoint = endpoint, .bucket = bucket, + .sessionToken = session_token, }; return this.aws_credentials.?; diff --git a/src/s3.zig b/src/s3.zig index aa12dadd49..396ee291c9 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -7,12 +7,55 @@ pub const RareData = @import("./bun.js/rare_data.zig"); const JSC = bun.JSC; const strings = bun.strings; +pub const ACL = enum { + /// Owner gets FULL_CONTROL. No one else has access rights (default). + private, + /// Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. + public_read, + /// Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. + public_read_write, + /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. + aws_exec_read, + /// Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. + authenticated_read, + /// Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_read, + /// Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_full_control, + log_delivery_write, + + pub fn toString(this: @This()) []const u8 { + return switch (this) { + .private => "private", + .public_read => "public-read", + .public_read_write => "public-read-write", + .aws_exec_read => "aws-exec-read", + .authenticated_read => "authenticated-read", + .bucket_owner_read => "bucket-owner-read", + .bucket_owner_full_control => "bucket-owner-full-control", + .log_delivery_write => "log-delivery-write", + }; + } + + pub const Map = bun.ComptimeStringMap(ACL, .{ + .{ "private", .private }, + .{ "public-read", .public_read }, + .{ "public-read-write", .public_read_write }, + .{ "aws-exec-read", .aws_exec_read }, + .{ "authenticated-read", .authenticated_read }, + .{ "bucket-owner-read", .bucket_owner_read }, + .{ "bucket-owner-full-control", .bucket_owner_full_control }, + .{ "log-delivery-write", .log_delivery_write }, + }); +}; + pub const AWSCredentials = struct { accessKeyId: []const u8, secretAccessKey: []const u8, region: []const u8, endpoint: []const u8, bucket: []const u8, + sessionToken: []const u8, ref_count: u32 = 1, pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); @@ -24,12 +67,16 @@ pub const AWSCredentials = struct { pub const AWSCredentialsWithOptions = struct { credentials: AWSCredentials, options: MultiPartUpload.MultiPartUploadOptions = .{}, + acl: ?ACL = null, + /// indicates if the credentials have changed + changed_credentials: bool = false, _accessKeyIdSlice: ?JSC.ZigString.Slice = null, _secretAccessKeySlice: ?JSC.ZigString.Slice = null, _regionSlice: ?JSC.ZigString.Slice = null, _endpointSlice: ?JSC.ZigString.Slice = null, _bucketSlice: ?JSC.ZigString.Slice = null, + _sessionTokenSlice: ?JSC.ZigString.Slice = null, pub fn deinit(this: *@This()) void { if (this._accessKeyIdSlice) |slice| slice.deinit(); @@ -37,13 +84,31 @@ pub const AWSCredentials = struct { if (this._regionSlice) |slice| slice.deinit(); if (this._endpointSlice) |slice| slice.deinit(); if (this._bucketSlice) |slice| slice.deinit(); + if (this._sessionTokenSlice) |slice| slice.deinit(); } }; - pub fn getCredentialsWithOptions(this: AWSCredentials, options: ?JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!AWSCredentialsWithOptions { + + fn hashConst(acl: []const u8) u64 { + var hasher = std.hash.Wyhash.init(0); + var remain = acl; + + var buf: [@sizeOf(@TypeOf(hasher.buf))]u8 = undefined; + + while (remain.len > 0) { + const end = @min(hasher.buf.len, remain.len); + + hasher.update(strings.copyLowercaseIfNeeded(remain[0..end], &buf)); + remain = remain[end..]; + } + + return hasher.final(); + } + pub fn getCredentialsWithOptions(this: AWSCredentials, default_options: MultiPartUpload.MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, globalObject: *JSC.JSGlobalObject) bun.JSError!AWSCredentialsWithOptions { // get ENV config var new_credentials = AWSCredentialsWithOptions{ .credentials = this, - .options = .{}, + .options = default_options, + .acl = default_acl, }; errdefer { new_credentials.deinit(); @@ -59,6 +124,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._accessKeyIdSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.accessKeyId = new_credentials._accessKeyIdSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("accessKeyId", "string", js_value); @@ -73,6 +139,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._secretAccessKeySlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.secretAccessKey = new_credentials._secretAccessKeySlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("secretAccessKey", "string", js_value); @@ -87,6 +154,7 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._regionSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.region = new_credentials._regionSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("region", "string", js_value); @@ -103,6 +171,7 @@ pub const AWSCredentials = struct { const normalized_endpoint = bun.URL.parse(new_credentials._endpointSlice.?.slice()).host; if (normalized_endpoint.len > 0) { new_credentials.credentials.endpoint = normalized_endpoint; + new_credentials.changed_credentials = true; } } } else { @@ -118,6 +187,23 @@ pub const AWSCredentials = struct { if (str.tag != .Empty and str.tag != .Dead) { new_credentials._bucketSlice = str.toUTF8(bun.default_allocator); new_credentials.credentials.bucket = new_credentials._bucketSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); + } + } + } + + if (try opts.getTruthyComptime(globalObject, "sessionToken")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._sessionTokenSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.sessionToken = new_credentials._sessionTokenSlice.?.slice(); + new_credentials.changed_credentials = true; } } else { return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); @@ -147,6 +233,21 @@ pub const AWSCredentials = struct { new_credentials.options.queueSize = @intCast(@max(queueSize, std.math.maxInt(u8))); } } + + if (try opts.getOptional(globalObject, "retry", i32)) |retry| { + if (retry < 0 and retry > 255) { + return globalObject.throwRangeError(retry, .{ + .min = 0, + .max = 255, + .field_name = "retry", + }); + } else { + new_credentials.options.retry = @intCast(retry); + } + } + if (try opts.getOptionalEnum(globalObject, "acl", ACL)) |acl| { + new_credentials.acl = acl; + } } } return new_credentials; @@ -177,6 +278,11 @@ pub const AWSCredentials = struct { bun.default_allocator.dupe(u8, this.bucket) catch bun.outOfMemory() else "", + + .sessionToken = if (this.sessionToken.len > 0) + bun.default_allocator.dupe(u8, this.sessionToken) catch bun.outOfMemory() + else + "", }); } pub fn deinit(this: *@This()) void { @@ -195,6 +301,9 @@ pub const AWSCredentials = struct { if (this.bucket.len > 0) { bun.default_allocator.free(this.bucket); } + if (this.sessionToken.len > 0) { + bun.default_allocator.free(this.sessionToken); + } this.destroy(); } @@ -250,19 +359,43 @@ pub const AWSCredentials = struct { authorization: []const u8, url: []const u8, - content_disposition: []const u8, - _headers: [5]picohttp.Header, - _headers_len: u8 = 4, + content_disposition: []const u8 = "", + session_token: []const u8 = "", + acl: ?ACL = null, + _headers: [7]picohttp.Header = .{ + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + }, + _headers_len: u8 = 0, pub fn headers(this: *const @This()) []const picohttp.Header { return this._headers[0..this._headers_len]; } + pub fn mixWithHeader(this: *const @This(), headers_buffer: []picohttp.Header, header: picohttp.Header) []const picohttp.Header { + // copy the headers to buffer + const len = this._headers_len; + for (this._headers[0..len], 0..len) |existing_header, i| { + headers_buffer[i] = existing_header; + } + headers_buffer[len] = header; + return headers_buffer[0 .. len + 1]; + } + pub fn deinit(this: *const @This()) void { if (this.amz_date.len > 0) { bun.default_allocator.free(this.amz_date); } + if (this.session_token.len > 0) { + bun.default_allocator.free(this.session_token); + } + if (this.content_disposition.len > 0) { bun.default_allocator.free(this.content_disposition); } @@ -284,15 +417,16 @@ pub const AWSCredentials = struct { pub const SignQueryOptions = struct { expires: usize = 86400, }; - pub const SignOptions = struct { path: []const u8, method: bun.http.Method, content_hash: ?[]const u8 = null, search_params: ?[]const u8 = null, content_disposition: ?[]const u8 = null, + acl: ?ACL = null, }; - fn guessRegion(endpoint: []const u8) []const u8 { + + pub fn guessRegion(endpoint: []const u8) []const u8 { if (endpoint.len > 0) { if (strings.endsWith(endpoint, ".r2.cloudflarestorage.com")) return "auto"; if (strings.indexOf(endpoint, ".amazonaws.com")) |end| { @@ -310,7 +444,7 @@ pub const AWSCredentials = struct { else => error.InvalidHexChar, }; } - fn encodeURIComponent(input: []const u8, buffer: []u8) ![]const u8 { + fn encodeURIComponent(input: []const u8, buffer: []u8, comptime encode_slash: bool) ![]const u8 { var written: usize = 0; for (input) |c| { @@ -323,6 +457,12 @@ pub const AWSCredentials = struct { }, // All other characters need to be percent-encoded else => { + if (!encode_slash and (c == '/' or c == '\\')) { + if (written >= buffer.len) return error.BufferTooSmall; + buffer[written] = if (c == '\\') '/' else c; + written += 1; + continue; + } if (written + 3 > buffer.len) return error.BufferTooSmall; buffer[written] = '%'; // Convert byte to hex @@ -344,40 +484,46 @@ pub const AWSCredentials = struct { }; fn getSignErrorMessage(comptime err: anyerror) [:0]const u8 { return switch (err) { - error.MissingCredentials => return "missing s3 credentials", - error.InvalidMethod => return "method must be GET, PUT, DELETE or HEAD when using s3 protocol", - error.InvalidPath => return "invalid s3 bucket, key combination", - error.InvalidEndpoint => return "invalid s3 endpoint", - else => return "failed to retrieve s3 content check your credentials", + error.MissingCredentials => return "Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required", + error.InvalidMethod => return "Method must be GET, PUT, DELETE or HEAD when using s3:// protocol", + error.InvalidPath => return "Invalid S3 bucket, key combination", + error.InvalidEndpoint => return "Invalid S3 endpoint", + error.InvalidSessionToken => return "Invalid session token", + else => return "Failed to retrieve S3 content. Are the credentials correct?", }; } pub fn getJSSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) JSC.JSValue { return switch (err) { - error.MissingCredentials => return globalThis.ERR_AWS_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).toJS(), - error.InvalidMethod => return globalThis.ERR_AWS_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).toJS(), - error.InvalidPath => return globalThis.ERR_AWS_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).toJS(), - error.InvalidEndpoint => return globalThis.ERR_AWS_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).toJS(), - else => return globalThis.ERR_AWS_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).toJS(), + error.MissingCredentials => return globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).toJS(), + error.InvalidMethod => return globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).toJS(), + error.InvalidPath => return globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).toJS(), + error.InvalidEndpoint => return globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).toJS(), + error.InvalidSessionToken => return globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).toJS(), + else => return globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).toJS(), }; } pub fn throwSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) bun.JSError { return switch (err) { - error.MissingCredentials => globalThis.ERR_AWS_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).throw(), - error.InvalidMethod => globalThis.ERR_AWS_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).throw(), - error.InvalidPath => globalThis.ERR_AWS_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).throw(), - error.InvalidEndpoint => globalThis.ERR_AWS_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).throw(), - else => globalThis.ERR_AWS_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).throw(), + error.MissingCredentials => globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).throw(), + error.InvalidMethod => globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).throw(), + error.InvalidPath => globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).throw(), + error.InvalidEndpoint => globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).throw(), + error.InvalidSessionToken => globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).throw(), + else => globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).throw(), }; } pub fn getSignErrorCodeAndMessage(err: anyerror) ErrorCodeAndMessage { + // keep error codes consistent for internal errors return switch (err) { - error.MissingCredentials => .{ .code = "MissingCredentials", .message = getSignErrorMessage(error.MissingCredentials) }, - error.InvalidMethod => .{ .code = "InvalidMethod", .message = getSignErrorMessage(error.InvalidMethod) }, - error.InvalidPath => .{ .code = "InvalidPath", .message = getSignErrorMessage(error.InvalidPath) }, - error.InvalidEndpoint => .{ .code = "InvalidEndpoint", .message = getSignErrorMessage(error.InvalidEndpoint) }, - else => .{ .code = "SignError", .message = getSignErrorMessage(error.SignError) }, + error.MissingCredentials => .{ .code = "ERR_S3_MISSING_CREDENTIALS", .message = getSignErrorMessage(error.MissingCredentials) }, + error.InvalidMethod => .{ .code = "ERR_S3_INVALID_METHOD", .message = getSignErrorMessage(error.InvalidMethod) }, + error.InvalidPath => .{ .code = "ERR_S3_INVALID_PATH", .message = getSignErrorMessage(error.InvalidPath) }, + error.InvalidEndpoint => .{ .code = "ERR_S3_INVALID_ENDPOINT", .message = getSignErrorMessage(error.InvalidEndpoint) }, + error.InvalidSessionToken => .{ .code = "ERR_S3_INVALID_SESSION_TOKEN", .message = getSignErrorMessage(error.InvalidSessionToken) }, + else => .{ .code = "ERR_S3_INVALID_SIGNATURE", .message = getSignErrorMessage(error.SignError) }, }; } + pub fn signRequest(this: *const @This(), signOptions: SignOptions, signQueryOption: ?SignQueryOptions) !SignResult { const method = signOptions.method; const request_path = signOptions.path; @@ -388,6 +534,9 @@ pub const AWSCredentials = struct { if (content_disposition != null and content_disposition.?.len == 0) { content_disposition = null; } + const session_token: ?[]const u8 = if (this.sessionToken.len == 0) null else this.sessionToken; + + const acl: ?[]const u8 = if (signOptions.acl) |acl_value| acl_value.toString() else null; if (this.accessKeyId.len == 0 or this.secretAccessKey.len == 0) return error.MissingCredentials; const signQuery = signQueryOption != null; @@ -403,9 +552,13 @@ pub const AWSCredentials = struct { const region = if (this.region.len > 0) this.region else guessRegion(this.endpoint); var full_path = request_path; + // handle \\ on bucket name if (strings.startsWith(full_path, "/")) { full_path = full_path[1..]; + } else if (strings.startsWith(full_path, "\\")) { + full_path = full_path[1..]; } + var path: []const u8 = full_path; var bucket: []const u8 = this.bucket; @@ -414,25 +567,41 @@ pub const AWSCredentials = struct { // guess bucket using path if (strings.indexOf(full_path, "/")) |end| { + if (strings.indexOf(full_path, "\\")) |backslash_index| { + if (backslash_index < end) { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; + } + } bucket = full_path[0..end]; path = full_path[end + 1 ..]; + } else if (strings.indexOf(full_path, "\\")) |backslash_index| { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; } else { return error.InvalidPath; } } if (strings.endsWith(path, "/")) { path = path[0..path.len]; + } else if (strings.endsWith(path, "\\")) { + path = path[0 .. path.len - 1]; } if (strings.startsWith(path, "/")) { path = path[1..]; + } else if (strings.startsWith(path, "\\")) { + path = path[1..]; } // if we allow path.len == 0 it will list the bucket for now we disallow if (path.len == 0) return error.InvalidPath; - var path_buffer: [1024 + 63 + 2]u8 = undefined; // 1024 max key size and 63 max bucket name - - const normalizedPath = std.fmt.bufPrint(&path_buffer, "/{s}/{s}", .{ bucket, path }) catch return error.InvalidPath; + var normalized_path_buffer: [1024 + 63 + 2]u8 = undefined; // 1024 max key size and 63 max bucket name + var path_buffer: [1024]u8 = undefined; + var bucket_buffer: [63]u8 = undefined; + bucket = encodeURIComponent(bucket, &bucket_buffer, false) catch return error.InvalidPath; + path = encodeURIComponent(path, &path_buffer, false) catch return error.InvalidPath; + const normalizedPath = std.fmt.bufPrint(&normalized_path_buffer, "/{s}/{s}", .{ bucket, path }) catch return error.InvalidPath; const date_result = getAMZDate(bun.default_allocator); const amz_date = date_result.date; @@ -440,10 +609,34 @@ pub const AWSCredentials = struct { const amz_day = amz_date[0..8]; const signed_headers = if (signQuery) "host" else brk: { - if (content_disposition != null) { - break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date"; + if (acl != null) { + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-content-sha256;x-amz-date"; + } + } } else { - break :brk "host;x-amz-content-sha256;x-amz-date"; + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-content-sha256;x-amz-date"; + } + } } }; // detect service name and host from region or endpoint @@ -451,7 +644,7 @@ pub const AWSCredentials = struct { var encoded_host: []const u8 = ""; const host = brk_host: { if (this.endpoint.len > 0) { - encoded_host = encodeURIComponent(this.endpoint, &encoded_host_buffer) catch return error.InvalidEndpoint; + encoded_host = encodeURIComponent(this.endpoint, &encoded_host_buffer, true) catch return error.InvalidEndpoint; break :brk_host try bun.default_allocator.dupe(u8, this.endpoint); } else { break :brk_host try std.fmt.allocPrint(bun.default_allocator, "s3.{s}.amazonaws.com", .{region}); @@ -462,7 +655,7 @@ pub const AWSCredentials = struct { errdefer bun.default_allocator.free(host); const aws_content_hash = if (content_hash) |hash| hash else ("UNSIGNED-PAYLOAD"); - var tmp_buffer: [2048]u8 = undefined; + var tmp_buffer: [4096]u8 = undefined; const authorization = brk: { // we hash the hash so we need 2 buffers @@ -485,26 +678,93 @@ pub const AWSCredentials = struct { break :brk_sign result; }; if (signQuery) { - const canonical = try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + var token_encoded_buffer: [2048]u8 = undefined; // token is normaly like 600-700 but can be up to 2k + var encoded_session_token: ?[]const u8 = null; + if (session_token) |token| { + encoded_session_token = encodeURIComponent(token, &token_encoded_buffer, true) catch return error.InvalidSessionToken; + } + const canonical = brk_canonical: { + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } + } else { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } + } + }; var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "https://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "https://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "https://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } else { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "https://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "https://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } } else { var encoded_content_disposition_buffer: [255]u8 = undefined; - const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer) catch return error.ContentTypeIsTooLong else ""; + const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer, true) catch return error.ContentTypeIsTooLong else ""; const canonical = brk_canonical: { - if (content_disposition != null) { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + if (acl) |acl_value| { + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } } }; var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); @@ -531,61 +791,86 @@ pub const AWSCredentials = struct { .amz_date = "", .host = "", .authorization = "", + .acl = signOptions.acl, .url = authorization, - .content_disposition = "", - ._headers = .{ - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - }, - ._headers_len = 0, }; } - if (content_disposition) |cd| { - const content_disposition_value = bun.default_allocator.dupe(u8, cd) catch bun.outOfMemory(); - return SignResult{ - .amz_date = amz_date, - .host = host, - .authorization = authorization, - .url = try std.fmt.allocPrint(bun.default_allocator, "https://{s}{s}{s}", .{ host, normalizedPath, if (search_params) |s| s else "" }), - .content_disposition = content_disposition_value, - ._headers = .{ - .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, - .{ .name = "x-amz-date", .value = amz_date }, - .{ .name = "Authorization", .value = authorization[0..] }, - .{ .name = "Host", .value = host }, - .{ .name = "Content-Disposition", .value = content_disposition_value }, - }, - ._headers_len = 5, - }; - } - return SignResult{ + var result = SignResult{ .amz_date = amz_date, .host = host, .authorization = authorization, + .acl = signOptions.acl, .url = try std.fmt.allocPrint(bun.default_allocator, "https://{s}{s}{s}", .{ host, normalizedPath, if (search_params) |s| s else "" }), - .content_disposition = "", - ._headers = .{ + ._headers = [_]picohttp.Header{ .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, .{ .name = "x-amz-date", .value = amz_date }, .{ .name = "Authorization", .value = authorization[0..] }, .{ .name = "Host", .value = host }, .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, }, ._headers_len = 4, }; + + if (acl) |acl_value| { + result._headers[result._headers_len] = .{ .name = "x-amz-acl", .value = acl_value }; + result._headers_len += 1; + } + + if (session_token) |token| { + const session_token_value = bun.default_allocator.dupe(u8, token) catch bun.outOfMemory(); + result.session_token = session_token_value; + result._headers[result._headers_len] = .{ .name = "x-amz-security-token", .value = session_token_value }; + result._headers_len += 1; + } + + if (content_disposition) |cd| { + const content_disposition_value = bun.default_allocator.dupe(u8, cd) catch bun.outOfMemory(); + result.content_disposition = content_disposition_value; + result._headers[result._headers_len] = .{ .name = "Content-Disposition", .value = content_disposition_value }; + result._headers_len += 1; + } + + return result; } + const JSS3Error = extern struct { + code: bun.String = bun.String.empty, + message: bun.String = bun.String.empty, + path: bun.String = bun.String.empty, + + pub fn init(code: []const u8, message: []const u8, path: ?[]const u8) @This() { + return .{ + // lets make sure we can reuse code and message and keep it service independent + .code = bun.String.createAtomIfPossible(code), + .message = bun.String.createAtomIfPossible(message), + .path = if (path) |p| bun.String.init(p) else bun.String.empty, + }; + } + + pub fn deinit(this: *const @This()) void { + this.path.deref(); + this.code.deref(); + this.message.deref(); + } + + pub fn toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) JSC.JSValue { + defer this.deinit(); + + return S3Error__toErrorInstance(this, global); + } + extern fn S3Error__toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue; + }; + pub const S3Error = struct { code: []const u8, message: []const u8, - pub fn toJS(err: *const @This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const js_err = globalObject.createErrorInstance("{s}", .{err.message}); - js_err.put(globalObject, JSC.ZigString.static("code"), JSC.ZigString.init(err.code).toJS(globalObject)); - return js_err; + pub fn toJS(err: *const @This(), globalObject: *JSC.JSGlobalObject, path: ?[]const u8) JSC.JSValue { + const value = JSS3Error.init(err.code, err.message, path).toErrorInstance(globalObject); + bun.assert(!globalObject.hasException()); + return value; } }; pub const S3StatResult = union(enum) { @@ -594,7 +879,7 @@ pub const AWSCredentials = struct { /// etag is not owned and need to be copied if used after this callback etag: []const u8 = "", }, - not_found: void, + not_found: S3Error, /// failure error is not owned and need to be copied if used after this callback failure: S3Error, @@ -606,7 +891,7 @@ pub const AWSCredentials = struct { /// body is owned and dont need to be copied, but dont forget to free it body: bun.MutableString, }, - not_found: void, + not_found: S3Error, /// failure error is not owned and need to be copied if used after this callback failure: S3Error, }; @@ -617,7 +902,7 @@ pub const AWSCredentials = struct { }; pub const S3DeleteResult = union(enum) { success: void, - not_found: void, + not_found: S3Error, /// failure error is not owned and need to be copied if used after this callback failure: S3Error, @@ -678,6 +963,20 @@ pub const AWSCredentials = struct { }, context), } } + pub fn notFound(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { + switch (this) { + inline .download, + .stat, + .delete, + => |callback| callback(.{ + .not_found = .{ + .code = code, + .message = message, + }, + }, context), + else => this.fail(code, message, context), + } + } }; pub fn deinit(this: *@This()) void { if (this.result.certificate_info) |*certificate| { @@ -697,11 +996,17 @@ pub const AWSCredentials = struct { this.destroy(); } - fn fail(this: *@This()) void { + const ErrorType = enum { + not_found, + failure, + }; + fn errorWithBody(this: @This(), comptime error_type: ErrorType) void { var code: []const u8 = "UnknownError"; var message: []const u8 = "an unexpected error has occurred"; + var has_error_code = false; if (this.result.fail) |err| { code = @errorName(err); + has_error_code = true; } else if (this.result.body) |body| { const bytes = body.list.items; if (bytes.len > 0) { @@ -709,6 +1014,7 @@ pub const AWSCredentials = struct { if (strings.indexOf(bytes, "")) |start| { if (strings.indexOf(bytes, "")) |end| { code = bytes[start + "".len .. end]; + has_error_code = true; } } if (strings.indexOf(bytes, "")) |start| { @@ -718,7 +1024,16 @@ pub const AWSCredentials = struct { } } } - this.callback.fail(code, message, this.callback_context); + + if (error_type == .not_found) { + if (!has_error_code) { + code = "NoSuchKey"; + message = "The specified key does not exist."; + } + this.callback.notFound(code, message, this.callback_context); + } else { + this.callback.fail(code, message, this.callback_context); + } } fn failIfContainsError(this: *@This(), status: u32) bool { @@ -759,7 +1074,7 @@ pub const AWSCredentials = struct { pub fn onResponse(this: *@This()) void { defer this.deinit(); if (!this.result.isSuccess()) { - this.fail(); + this.errorWithBody(.failure); return; } bun.assert(this.result.metadata != null); @@ -767,9 +1082,6 @@ pub const AWSCredentials = struct { switch (this.callback) { .stat => |callback| { switch (response.status_code) { - 404 => { - callback(.{ .not_found = {} }, this.callback_context); - }, 200 => { callback(.{ .success = .{ @@ -778,21 +1090,24 @@ pub const AWSCredentials = struct { }, }, this.callback_context); }, + 404 => { + this.errorWithBody(.not_found); + }, else => { - this.fail(); + this.errorWithBody(.failure); }, } }, .delete => |callback| { switch (response.status_code) { - 404 => { - callback(.{ .not_found = {} }, this.callback_context); - }, 200, 204 => { callback(.{ .success = {} }, this.callback_context); }, + 404 => { + this.errorWithBody(.not_found); + }, else => { - this.fail(); + this.errorWithBody(.failure); }, } }, @@ -802,15 +1117,12 @@ pub const AWSCredentials = struct { callback(.{ .success = {} }, this.callback_context); }, else => { - this.fail(); + this.errorWithBody(.failure); }, } }, .download => |callback| { switch (response.status_code) { - 404 => { - callback(.{ .not_found = {} }, this.callback_context); - }, 200, 204, 206 => { const body = this.response_buffer; this.response_buffer = .{ @@ -827,9 +1139,12 @@ pub const AWSCredentials = struct { }, }, this.callback_context); }, + 404 => { + this.errorWithBody(.not_found); + }, else => { //error - this.fail(); + this.errorWithBody(.failure); }, } }, @@ -844,7 +1159,7 @@ pub const AWSCredentials = struct { if (response.headers.get("etag")) |etag| { callback(.{ .etag = etag }, this.callback_context); } else { - this.fail(); + this.errorWithBody(.failure); } } }, @@ -978,14 +1293,7 @@ pub const AWSCredentials = struct { } } } - if (state.status_code == 404) { - if (!has_body_code) { - code = "FileNotFound"; - } - if (!has_body_message) { - message = "File not found"; - } - } + err = .{ .code = code, .message = message, @@ -1085,6 +1393,7 @@ pub const AWSCredentials = struct { body: []const u8, proxy_url: ?[]const u8 = null, range: ?[]const u8 = null, + acl: ?ACL = null, }; pub fn executeSimpleS3Request( @@ -1098,6 +1407,7 @@ pub const AWSCredentials = struct { .method = options.method, .search_params = options.search_params, .content_disposition = options.content_disposition, + .acl = options.acl, }, null) catch |sign_err| { if (options.range) |range_| bun.default_allocator.free(range_); const error_code_and_message = getSignErrorCodeAndMessage(sign_err); @@ -1106,40 +1416,15 @@ pub const AWSCredentials = struct { }; const headers = brk: { + var header_buffer: [10]picohttp.Header = undefined; if (options.range) |range_| { - const _headers = result.headers(); - var headersWithRange: [5]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - .{ .name = "range", .value = range_ }, - }; - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(&headersWithRange, bun.default_allocator) catch bun.outOfMemory(); + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); } else { if (options.content_type) |content_type| { if (content_type.len > 0) { - const _headers = result.headers(); - if (_headers.len > 4) { - var headersWithContentType: [6]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - _headers[4], - .{ .name = "Content-Type", .value = content_type }, - }; - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(&headersWithContentType, bun.default_allocator) catch bun.outOfMemory(); - } - - var headersWithContentType: [5]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - .{ .name = "Content-Type", .value = content_type }, - }; - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(&headersWithContentType, bun.default_allocator) catch bun.outOfMemory(); + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = content_type }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); } } @@ -1252,17 +1537,11 @@ pub const AWSCredentials = struct { return; }; + var header_buffer: [10]picohttp.Header = undefined; const headers = brk: { if (range) |range_| { - const _headers = result.headers(); - var headersWithRange: [5]picohttp.Header = .{ - _headers[0], - _headers[1], - _headers[2], - _headers[3], - .{ .name = "range", .value = range_ }, - }; - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(&headersWithRange, bun.default_allocator) catch bun.outOfMemory(); + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); } else { break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); } @@ -1328,12 +1607,14 @@ pub const AWSCredentials = struct { .ptr = .{ .Bytes = &reader.context }, .value = readable_value, }, globalThis), + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), })); return readable_value; } const S3DownloadStreamWrapper = struct { readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + path: []const u8, pub usingnamespace bun.New(@This()); pub fn callback(chunk: bun.MutableString, has_more: bool, request_err: ?S3Error, this: *@This()) void { @@ -1348,7 +1629,7 @@ pub const AWSCredentials = struct { readable.ptr.Bytes.onData( .{ - .err = .{ .JSValue = err.toJS(globalThis) }, + .err = .{ .JSValue = err.toJS(globalThis, this.path) }, }, bun.default_allocator, ); @@ -1381,6 +1662,7 @@ pub const AWSCredentials = struct { pub fn deinit(this: *@This()) void { this.readable_stream_ref.deinit(); + bun.default_allocator.free(this.path); this.destroy(); } }; @@ -1394,37 +1676,41 @@ pub const AWSCredentials = struct { }, .{ .delete = callback }, callback_context); } - pub fn s3Upload(this: *const @This(), path: []const u8, content: []const u8, content_type: ?[]const u8, proxy_url: ?[]const u8, callback: *const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) void { + pub fn s3Upload(this: *const @This(), path: []const u8, content: []const u8, content_type: ?[]const u8, acl: ?ACL, proxy_url: ?[]const u8, callback: *const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) void { this.executeSimpleS3Request(.{ .path = path, .method = .PUT, .proxy_url = proxy_url, .body = content, .content_type = content_type, + .acl = acl, }, .{ .upload = callback }, callback_context); } const S3UploadStreamWrapper = struct { readable_stream_ref: JSC.WebCore.ReadableStream.Strong, - sink: *JSC.WebCore.FetchTaskletChunkedRequestSink, + sink: *JSC.WebCore.NetworkSink, + task: *MultiPartUpload, callback: ?*const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque, ref_count: u32 = 1, + path: []const u8, // this is owned by the task not by the wrapper pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); pub fn resolve(result: S3UploadResult, self: *@This()) void { const sink = self.sink; defer self.deref(); - - if (sink.endPromise.globalObject()) |globalObject| { - switch (result) { - .success => sink.endPromise.resolve(globalObject, JSC.jsNumber(0)), - .failure => |err| { - if (!sink.done) { - sink.abort(); - return; - } - sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject)); - }, + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + switch (result) { + .success => sink.endPromise.resolve(globalObject, JSC.jsNumber(0)), + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } + sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject, self.path)); + }, + } } } if (self.callback) |callback| { @@ -1436,6 +1722,7 @@ pub const AWSCredentials = struct { self.readable_stream_ref.deinit(); self.sink.finalize(); self.sink.destroy(); + self.task.deref(); self.destroy(); } }; @@ -1443,13 +1730,12 @@ pub const AWSCredentials = struct { var args = callframe.arguments_old(2); var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); defer this.deref(); - if (this.sink.endPromise.hasValue()) { - this.sink.endPromise.resolve(globalThis, JSC.jsNumber(0)); - } + if (this.readable_stream_ref.get()) |stream| { stream.done(globalThis); } this.readable_stream_ref.deinit(); + this.task.continueStream(); return .undefined; } @@ -1458,6 +1744,7 @@ pub const AWSCredentials = struct { const args = callframe.arguments_old(2); var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); defer this.deref(); + const err = args.ptr[0]; if (this.sink.endPromise.hasValue()) { this.sink.endPromise.rejectOnNextTick(globalThis, err); @@ -1475,6 +1762,8 @@ pub const AWSCredentials = struct { }); } } + this.task.continueStream(); + return .undefined; } pub const shim = JSC.Shimmer("Bun", "S3UploadStream", @This()); @@ -1491,10 +1780,34 @@ pub const AWSCredentials = struct { } /// consumes the readable stream and upload to s3 - pub fn s3UploadStream(this: *@This(), path: []const u8, readable_stream: JSC.WebCore.ReadableStream, globalThis: *JSC.JSGlobalObject, options: MultiPartUpload.MultiPartUploadOptions, content_type: ?[]const u8, proxy: ?[]const u8, callback: ?*const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) JSC.JSValue { + pub fn s3UploadStream(this: *@This(), path: []const u8, readable_stream: JSC.WebCore.ReadableStream, globalThis: *JSC.JSGlobalObject, options: MultiPartUpload.MultiPartUploadOptions, acl: ?ACL, content_type: ?[]const u8, proxy: ?[]const u8, callback: ?*const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) JSC.JSValue { this.ref(); // ref the credentials const proxy_url = (proxy orelse ""); + if (readable_stream.isDisturbed(globalThis)) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is already disturbed").toErrorInstance(globalThis)); + } + + switch (readable_stream.ptr) { + .Invalid => { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is invalid").toErrorInstance(globalThis)); + }, + inline .File, .Bytes => |stream| { + if (stream.pending.result == .err) { + // we got an error, fail early + const err = stream.pending.result.err; + stream.pending = .{ .result = .{ .done = {} } }; + const js_err, const was_strong = err.toJSWeak(globalThis); + if (was_strong == .Strong) { + js_err.unprotect(); + } + js_err.ensureStillAlive(); + return JSC.JSPromise.rejectedPromise(globalThis, js_err).asValue(globalThis); + } + }, + else => {}, + } + const task = MultiPartUpload.new(.{ .credentials = this, .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), @@ -1503,32 +1816,42 @@ pub const AWSCredentials = struct { .callback = @ptrCast(&S3UploadStreamWrapper.resolve), .callback_context = undefined, .globalThis = globalThis, + .state = .wait_stream_check, .options = options, + .acl = acl, .vm = JSC.VirtualMachine.get(), }); task.poll_ref.ref(task.vm); - task.ref(); // + 1 for the stream + task.ref(); // + 1 for the stream sink - var response_stream = JSC.WebCore.FetchTaskletChunkedRequestSink.new(.{ + var response_stream = JSC.WebCore.NetworkSink.new(.{ .task = .{ .s3_upload = task }, .buffer = .{}, .globalThis = globalThis, .encoded = false, .endPromise = JSC.JSPromise.Strong.init(globalThis), }).toSink(); + task.ref(); // + 1 for the stream wrapper + const endPromise = response_stream.sink.endPromise.value(); const ctx = S3UploadStreamWrapper.new(.{ .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable_stream, globalThis), .sink = &response_stream.sink, .callback = callback, .callback_context = callback_context, + .path = task.path, + .task = task, }); task.callback_context = @ptrCast(ctx); + // keep the task alive until we are done configuring the signal + task.ref(); + defer task.deref(); + var signal = &response_stream.sink.signal; - signal.* = JSC.WebCore.FetchTaskletChunkedRequestSink.JSSink.SinkSignal.init(.zero); + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); // explicitly set it to a dead pointer // we use this memory address to disable signals being sent @@ -1536,7 +1859,7 @@ pub const AWSCredentials = struct { bun.assert(signal.isDead()); // We are already corked! - const assignment_result: JSC.JSValue = JSC.WebCore.FetchTaskletChunkedRequestSink.JSSink.assignToStream( + const assignment_result: JSC.JSValue = JSC.WebCore.NetworkSink.JSSink.assignToStream( globalThis, readable_stream.value, response_stream, @@ -1549,14 +1872,15 @@ pub const AWSCredentials = struct { bun.assert(!signal.isDead()); if (assignment_result.toError()) |err| { - readable_stream.cancel(globalThis); if (response_stream.sink.endPromise.hasValue()) { response_stream.sink.endPromise.rejectOnNextTick(globalThis, err); } + task.fail(.{ .code = "UnknownError", .message = "ReadableStream ended with an error", }); + readable_stream.cancel(globalThis); return endPromise; } @@ -1568,40 +1892,54 @@ pub const AWSCredentials = struct { if (assignment_result.asAnyPromise()) |promise| { switch (promise.status(globalThis.vm())) { .pending => { + // if we eended and its not canceled the promise is the endPromise + // because assignToStream can return the sink.end() promise + // we set the endPromise in the NetworkSink so we need to resolve it + if (response_stream.sink.ended and !response_stream.sink.cancel) { + task.continueStream(); + + readable_stream.done(globalThis); + return endPromise; + } ctx.ref(); + assignment_result.then( globalThis, task.callback_context, onUploadStreamResolveRequestStream, onUploadStreamRejectRequestStream, ); + // we need to wait the promise to resolve because can be an error/cancel here + if (!task.ended) + task.continueStream(); }, .fulfilled => { + task.continueStream(); + readable_stream.done(globalThis); - if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.resolve(globalThis, JSC.jsNumber(0)); - } }, .rejected => { - readable_stream.cancel(globalThis); if (response_stream.sink.endPromise.hasValue()) { response_stream.sink.endPromise.rejectOnNextTick(globalThis, promise.result(globalThis.vm())); } + task.fail(.{ .code = "UnknownError", .message = "ReadableStream ended with an error", }); + readable_stream.cancel(globalThis); }, } } else { - readable_stream.cancel(globalThis); if (response_stream.sink.endPromise.hasValue()) { response_stream.sink.endPromise.rejectOnNextTick(globalThis, assignment_result); } + task.fail(.{ .code = "UnknownError", .message = "ReadableStream ended with an error", }); + readable_stream.cancel(globalThis); } } return endPromise; @@ -1609,23 +1947,25 @@ pub const AWSCredentials = struct { /// returns a writable stream that writes to the s3 path pub fn s3WritableStream(this: *@This(), path: []const u8, globalThis: *JSC.JSGlobalObject, options: MultiPartUpload.MultiPartUploadOptions, content_type: ?[]const u8, proxy: ?[]const u8) bun.JSError!JSC.JSValue { const Wrapper = struct { - pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.FetchTaskletChunkedRequestSink) void { - if (sink.endPromise.globalObject()) |globalObject| { - const event_loop = globalObject.bunVM().eventLoop(); - event_loop.enter(); - defer event_loop.exit(); - switch (result) { - .success => { - sink.endPromise.resolve(globalObject, JSC.jsNumber(0)); - }, - .failure => |err| { - if (!sink.done) { - sink.abort(); - return; - } + pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.NetworkSink) void { + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + const event_loop = globalObject.bunVM().eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + switch (result) { + .success => { + sink.endPromise.resolve(globalObject, JSC.jsNumber(0)); + }, + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } - sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject)); - }, + sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject, sink.path())); + }, + } } } sink.finalize(); @@ -1649,7 +1989,7 @@ pub const AWSCredentials = struct { task.poll_ref.ref(task.vm); task.ref(); // + 1 for the stream - var response_stream = JSC.WebCore.FetchTaskletChunkedRequestSink.new(.{ + var response_stream = JSC.WebCore.NetworkSink.new(.{ .task = .{ .s3_upload = task }, .buffer = .{}, .globalThis = globalThis, @@ -1660,7 +2000,7 @@ pub const AWSCredentials = struct { task.callback_context = @ptrCast(response_stream); var signal = &response_stream.sink.signal; - signal.* = JSC.WebCore.FetchTaskletChunkedRequestSink.JSSink.SinkSignal.init(.zero); + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); // explicitly set it to a dead pointer // we use this memory address to disable signals being sent @@ -1686,6 +2026,7 @@ pub const MultiPartUpload = struct { ended: bool = false, options: MultiPartUploadOptions = .{}, + acl: ?ACL = null, credentials: *AWSCredentials, poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), vm: *JSC.VirtualMachine, @@ -1704,6 +2045,7 @@ pub const MultiPartUpload = struct { multipart_upload_list: bun.ByteList = .{}, state: enum { + wait_stream_check, not_started, multipart_started, multipart_completed, @@ -1756,7 +2098,7 @@ pub const MultiPartUpload = struct { } pub fn onPartResponse(result: AWS.S3PartResult, this: *@This()) void { - if (this.state == .canceled) { + if (this.state == .canceled or this.ctx.state == .finished) { log("onPartResponse {} canceled", .{this.partNumber}); if (this.owns_data) bun.default_allocator.free(this.data); this.ctx.deref(); @@ -1814,7 +2156,7 @@ pub const MultiPartUpload = struct { }, .{ .part = @ptrCast(&onPartResponse) }, this); } pub fn start(this: *@This()) void { - if (this.state != .pending or this.ctx.state != .multipart_completed) return; + if (this.state != .pending or this.ctx.state != .multipart_completed or this.ctx.state == .finished) return; this.ctx.ref(); this.state = .started; this.perform(); @@ -1860,11 +2202,14 @@ pub const MultiPartUpload = struct { } pub fn singleSendUploadResponse(result: AWS.S3UploadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; switch (result) { .failure => |err| { if (this.options.retry > 0) { log("singleSendUploadResponse {} retry", .{this.options.retry}); this.options.retry -= 1; + this.ref(); // retry failed this.credentials.executeSimpleS3Request(.{ .path = this.path, @@ -1872,6 +2217,7 @@ pub const MultiPartUpload = struct { .proxy_url = this.proxyUrl(), .body = this.buffered.items, .content_type = this.content_type, + .acl = this.acl, }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); return; @@ -1925,6 +2271,9 @@ pub const MultiPartUpload = struct { } fn drainEnqueuedParts(this: *@This()) void { + if (this.state == .finished) { + return; + } // check pending to start or transformed buffered ones into tasks if (this.state == .multipart_completed) { for (this.queue.items) |*part| { @@ -1946,13 +2295,16 @@ pub const MultiPartUpload = struct { } pub fn fail(this: *@This(), _err: AWS.S3Error) void { log("fail {s}:{s}", .{ _err.code, _err.message }); + this.ended = true; for (this.queue.items) |*task| { task.cancel(); } if (this.state != .finished) { - this.callback(.{ .failure = _err }, this.callback_context); + const old_state = this.state; this.state = .finished; - if (this.state == .multipart_completed) { + this.callback(.{ .failure = _err }, this.callback_context); + + if (old_state == .multipart_completed) { // will deref after rollback this.rollbackMultiPartRequest(); } else { @@ -1984,6 +2336,8 @@ pub const MultiPartUpload = struct { } } pub fn startMultiPartRequestResult(result: AWS.S3DownloadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; switch (result) { .failure => |err| { log("startMultiPartRequestResult {s} failed {s}: {s}", .{ this.path, err.message, err.message }); @@ -2021,6 +2375,7 @@ pub const MultiPartUpload = struct { pub fn onCommitMultiPartRequest(result: AWS.S3CommitResult, this: *@This()) void { log("onCommitMultiPartRequest {s}", .{this.upload_id}); + switch (result) { .failure => |err| { if (this.options.retry > 0) { @@ -2094,6 +2449,7 @@ pub const MultiPartUpload = struct { if (this.state == .not_started) { // will auto start later this.state = .multipart_started; + this.ref(); this.credentials.executeSimpleS3Request(.{ .path = this.path, .method = .POST, @@ -2101,6 +2457,7 @@ pub const MultiPartUpload = struct { .body = "", .search_params = "?uploads=", .content_type = this.content_type, + .acl = this.acl, }, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this); } else if (this.state == .multipart_completed) { part.start(); @@ -2138,6 +2495,7 @@ pub const MultiPartUpload = struct { if (this.ended and this.buffered.items.len < this.partSizeInBytes() and this.state == .not_started) { log("processBuffered {s} singlefile_started", .{this.path}); this.state = .singlefile_started; + this.ref(); // we can do only 1 request this.credentials.executeSimpleS3Request(.{ .path = this.path, @@ -2145,6 +2503,7 @@ pub const MultiPartUpload = struct { .proxy_url = this.proxyUrl(), .body = this.buffered.items, .content_type = this.content_type, + .acl = this.acl, }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); } else { // we need to split @@ -2156,9 +2515,22 @@ pub const MultiPartUpload = struct { return this.options.partSize * OneMiB; } + pub fn continueStream(this: *@This()) void { + if (this.state == .wait_stream_check) { + this.state = .not_started; + if (this.ended) { + this.processBuffered(this.partSizeInBytes()); + } + } + } + pub fn sendRequestData(this: *@This(), chunk: []const u8, is_last: bool) void { if (this.ended) return; - + if (this.state == .wait_stream_check and chunk.len == 0 and is_last) { + // we do this because stream will close if the file dont exists and we dont wanna to send an empty part in this case + this.ended = true; + return; + } if (is_last) { this.ended = true; if (chunk.len > 0) { diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index 7ac336fc6c..f265c4c95e 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -124,38 +124,40 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(result.status).toBe(200); expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); } - }, 10_000); + }, 20_000); }); }); describe("Bun.S3", () => { describe(bucketInName ? "bucket in path" : "bucket in options", () => { const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + const options = bucketInName ? null : { bucket: S3Bucket }; + + var bucket = S3(s3Options); beforeAll(async () => { - const file = new S3(tmp_filename, options); + const file = bucket(tmp_filename, options); await file.write("Hello Bun!"); }); afterAll(async () => { - const file = new S3(tmp_filename, options); + const file = bucket(tmp_filename, options); await file.unlink(); }); it("should download file via Bun.s3().text()", async () => { - const file = new S3(tmp_filename, options); + const file = bucket(tmp_filename, options); const text = await file.text(); expect(text).toBe("Hello Bun!"); }); it("should download range", async () => { - const file = new S3(tmp_filename, options); + const file = bucket(tmp_filename, options); const text = await file.slice(6, 10).text(); expect(text).toBe("Bun!"); }); it("should check if a key exists or content-length", async () => { - const file = new S3(tmp_filename, options); + const file = bucket(tmp_filename, options); const exists = await file.exists(); expect(exists).toBe(true); const contentLength = await file.size; @@ -163,27 +165,27 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { }); it("should check if a key does not exist", async () => { - const file = new S3(tmp_filename + "-does-not-exist", options); + const file = bucket(tmp_filename + "-does-not-exist", options); const exists = await file.exists(); expect(exists).toBe(false); }); it("should be able to set content-type", async () => { { - const s3file = new S3(tmp_filename, { ...options, type: "text/css" }); - await s3file.write("Hello Bun!"); + const s3file = bucket(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/css" }); const response = await fetch(s3file.presign()); expect(response.headers.get("content-type")).toStartWith("text/css"); } { - const s3file = new S3(tmp_filename, options); + const s3file = bucket(tmp_filename, options); await s3file.write("Hello Bun!", { type: "text/plain" }); const response = await fetch(s3file.presign()); expect(response.headers.get("content-type")).toStartWith("text/plain"); } { - const s3file = new S3(tmp_filename, options); + const s3file = bucket(tmp_filename, options); const writer = s3file.writer({ type: "application/json" }); writer.write("Hello Bun!"); await writer.end(); @@ -192,15 +194,15 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { } { - await S3.upload(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); - const response = await fetch(s3(tmp_filename, options).presign()); + await bucket.write(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); + const response = await fetch(bucket(tmp_filename, options).presign()); expect(response.headers.get("content-type")).toStartWith("application/xml"); } }); - it("should be able to upload large files using S3.upload + readable Request", async () => { + it("should be able to upload large files using bucket.write + readable Request", async () => { { - await S3.upload( + await bucket.write( tmp_filename, new Request("https://example.com", { method: "PUT", @@ -215,21 +217,21 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { }), options, ); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); } }, 10_000); - it("should be able to upload large files in one go using S3.upload", async () => { + it("should be able to upload large files in one go using bucket.write", async () => { { - await S3.upload(tmp_filename, bigPayload, options); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await new S3(tmp_filename, options).text()).toBe(bigPayload); + await bucket.write(tmp_filename, bigPayload, options); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await bucket(tmp_filename, options).text()).toBe(bigPayload); } }, 10_000); it("should be able to upload large files in one go using S3File.write", async () => { { - const s3File = new S3(tmp_filename, options); + const s3File = bucket(tmp_filename, options); await s3File.write(bigPayload); expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); expect(await s3File.text()).toBe(bigPayload); @@ -305,7 +307,7 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { it("should be able to upload large files in one go using Bun.write", async () => { { await Bun.write(file(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await s3(tmp_filename, options).size).toBe(Buffer.byteLength(bigPayload)); expect(await file(tmp_filename, options).text()).toEqual(bigPayload); } }, 15_000); @@ -392,18 +394,12 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { } }); - it("should be able to upload large files in one go using S3.upload", async () => { - { - await S3.upload(s3(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - } - }, 10_000); - it("should be able to upload large files in one go using Bun.write", async () => { { - await Bun.write(s3(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await s3(tmp_filename, options).text()).toBe(bigPayload); + const s3file = s3(tmp_filename, options); + await Bun.write(s3file, bigPayload); + expect(await s3file.size).toBe(Buffer.byteLength(bigPayload)); + expect(await s3file.text()).toBe(bigPayload); } }, 10_000); @@ -461,55 +457,315 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { }); }); } + describe("special characters", () => { + it("should allow special characters in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`🌈🦄${randomUUID()}.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow forward slashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}/test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow backslashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}\\test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow starting with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + { + const s3file = s3(`/${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + { + const s3file = s3(`\\${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + expect().pass(); + }); + it("should allow ending with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + { + const s3file = s3(`${randomUUID()}/`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + { + const s3file = s3(`${randomUUID()}\\`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); + } + expect().pass(); + }); + }); + describe("errors", () => { + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file("./do-not-exist.txt")); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ENOENT"); + expect(e?.path).toBe("./do-not-exist.txt"); + expect(e?.syscall).toBe("open"); + } + }); + + it("Bun.write(s3file, file) should work with empty file", async () => { + const dir = tempDirWithFiles("fsr", { + "hello.txt": "", + }); + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file(path.join(dir, "hello.txt"))); + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: S3Bucket }), + ); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("NoSuchKey"); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: "does-not-exists" }), + ); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("AccessDenied"); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("should error if bucket is missing", async () => { + try { + await Bun.write(s3("test.txt", s3Options), "Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error if bucket is missing on payload", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), s3("test2.txt", s3Options)); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.path).toBe("test2.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error when invalid method", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path)].map(async fn => { + const s3file = fn("method-test", { + ...s3Options, + bucket: S3Bucket, + }); + + try { + await s3file.presign({ method: "OPTIONS" }); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_METHOD"); + } + }), + ); + }); + + it("should error when path is too long", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path)].map(async fn => { + try { + const s3file = fn("test" + "a".repeat(4096), { + ...s3Options, + bucket: S3Bucket, + }); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ENAMETOOLONG"); + } + }), + ); + }); + }); describe("credentials", () => { it("should error with invalid access key id", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - accessKeyId: "invalid", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + accessKeyId: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("InvalidArgument"); + } + }), + ); }); it("should error with invalid secret key id", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - secretAccessKey: "invalid", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + secretAccessKey: "invalid", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("AccessDenied"); + } + }), + ); }); it("should error with invalid endpoint", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "🙂.🥯", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "🙂.🥯", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("InvalidAccessKeyId"); + } + }), + ); }); it("should error with invalid endpoint", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "..asd.@%&&&%%", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "..asd.@%&&&%%", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("InvalidAccessKeyId"); + } + }), + ); }); it("should error with invalid bucket", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://credentials-test", { - ...s3Options, - bucket: "invalid", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + ...s3Options, + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("AccessDenied"); + expect(e?.name).toBe("S3Error"); + } + }), + ); + }); + + it("should error when missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + it("should error when presign missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path)].map(async fn => { + const s3file = fn("method-test", { + bucket: S3Bucket, + }); + + try { + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + + it("should error when presign with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.endpoint = Buffer.alloc(1024, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_ENDPOINT"); + } + }), + ); + }); + it("should error when presign with invalid token", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.sessionToken = Buffer.alloc(4096, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_SESSION_TOKEN"); + } + }), + ); }); }); @@ -539,10 +795,24 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(url.includes("X-Amz-Algorithm")).toBe(true); expect(url.includes("X-Amz-SignedHeaders")).toBe(true); }); + it("should work with acl", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + acl: "public-read", + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Acl=public-read")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); - it("S3.presign should work", async () => { - const url = S3.presign("s3://bucket/credentials-test", { - ...s3Options, + it("s3().presign() should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ expiresIn: 10, }); expect(url).toBeDefined(); @@ -554,9 +824,8 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(url.includes("X-Amz-SignedHeaders")).toBe(true); }); - it("S3.presign endpoint should work", async () => { - const url = S3.presign("s3://bucket/credentials-test", { - ...s3Options, + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ expiresIn: 10, endpoint: "https://s3.bun.sh", }); @@ -570,9 +839,8 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(url.includes("X-Amz-SignedHeaders")).toBe(true); }); - it("S3.presign endpoint should work", async () => { - const url = S3.presign("s3://folder/credentials-test", { - ...s3Options, + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://folder/credentials-test", s3Options).presign({ expiresIn: 10, bucket: "my-bucket", }); @@ -587,16 +855,19 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { }); }); - it("exists, upload, size, unlink should work", async () => { - const filename = randomUUID(); - const fullPath = `s3://${S3Bucket}/${filename}`; - expect(await S3.exists(fullPath, s3Options)).toBe(false); + it("exists, write, size, unlink should work", async () => { + const fullPath = randomUUID(); + const bucket = S3({ + ...s3Options, + bucket: S3Bucket, + }); + expect(await bucket.exists(fullPath)).toBe(false); - await S3.upload(fullPath, "bun", s3Options); - expect(await S3.exists(fullPath, s3Options)).toBe(true); - expect(await S3.size(fullPath, s3Options)).toBe(3); - await S3.unlink(fullPath, s3Options); - expect(await S3.exists(fullPath, s3Options)).toBe(false); + await bucket.write(fullPath, "bun"); + expect(await bucket.exists(fullPath)).toBe(true); + expect(await bucket.size(fullPath)).toBe(3); + await bucket.unlink(fullPath); + expect(await bucket.exists(fullPath)).toBe(false); }); it("should be able to upload a slice", async () => { @@ -608,7 +879,7 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(await slice.text()).toBe("Bun!"); expect(await s3file.text()).toBe("Hello Bun!"); - await S3.upload(fullPath, slice, s3Options); + await s3file.write(slice); const text = await s3file.text(); expect(text).toBe("Bun!"); await s3file.unlink(); From cc5ee01752e4765fbabf43c260d675bb286c6550 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 3 Jan 2025 23:08:14 -0800 Subject: [PATCH 24/56] Initial S3 docs --- docs/api/s3.md | 549 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/nav.ts | 3 + 2 files changed, 552 insertions(+) create mode 100644 docs/api/s3.md diff --git a/docs/api/s3.md b/docs/api/s3.md new file mode 100644 index 0000000000..448baa0571 --- /dev/null +++ b/docs/api/s3.md @@ -0,0 +1,549 @@ +Production servers often read, upload, and write files to S3-compatible object storage services instead of the local filesystem. Historically, that means local filesystem APIs you use in development can't be used in production. When you use Bun, things are different. + +Bun provides fast, native bindings for interacting with S3-compatible object storage services. Bun's S3 API is designed to be simple and feel similar to fetch's `Response` and `Blob` APIs (like Bun's local filesystem APIs). + +```ts +import { s3, write, S3 } from "bun"; + +const metadata = await s3("123.json", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}); + +// Download from S3 as JSON +const data = await metadata.json(); + +// Upload to S3 +await write(metadata, JSON.stringify({ name: "John", age: 30 })); + +// Presign a URL (synchronous - no network request needed) +const url = metadata.presign({ + acl: "public-read", + expiresIn: 60 * 60 * 24, // 1 day +}); +``` + +S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. You can use Bun's S3 API with S3-compatible storage services like: + +- AWS S3 +- Cloudflare R2 +- DigitalOcean Spaces +- MinIO +- Backblaze B2 +- ...and any other S3-compatible storage service + +## Basic Usage + +There are several ways to interact with Bun's S3 API. + +### Using `Bun.s3()` + +The `s3()` helper function is used to create one-off `S3File` instances for a single file. + +```ts +import { s3 } from "bun"; + +// Using the s3() helper +const s3file = s3("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", // optional + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "https://.digitaloceanspaces.com", // DigitalOcean Spaces + // endpoint: "http://localhost:9000", // MinIO +}); +``` + +### Reading Files + +You can read files from S3 using similar methods to Bun's file system APIs: + +```ts +// Read an S3File as text +const text = await s3file.text(); + +// Read an S3File as JSON +const json = await s3file.json(); + +// Read an S3File as an ArrayBuffer +const buffer = await s3file.arrayBuffer(); + +// Get only the first 1024 bytes +const partial = await s3file.slice(0, 1024).text(); + +// Stream the file +const stream = s3file.stream(); +for await (const chunk of stream) { + console.log(chunk); +} +``` + +## Writing Files + +Writing to S3 is just as simple: + +```ts +// Write a string (replacing the file) +await s3file.write("Hello World!"); + +// Write with content type +await s3file.write(JSON.stringify({ name: "John", age: 30 }), { + type: "application/json", +}); + +// Write using a writer (streaming) +const writer = s3file.writer({ type: "application/json" }); +writer.write("Hello"); +writer.write(" World!"); +await writer.end(); + +// Write using Bun.write +await Bun.write(s3file, "Hello World!"); +``` + +### Working with large files (streams) + +Bun automatically handles multipart uploads for large files and provides streaming capabilities. The same API that works for local files also works for S3 files. + +```ts +// Write a large file +const bigFile = Buffer.alloc(10 * 1024 * 1024); // 10MB +const writer = s3file.writer({ + // Automatically retry on network errors up to 3 times + retry: 3, + + // Queue up to 10 requests at a time + queueSize: 10, + + // Upload in 5 MB chunks + partSize: 5 * 1024 * 1024, +}); +for (let i = 0; i < 10; i++) { + await writer.write(bigFile); +} +await writer.end(); +``` + +## Presigning URLs + +When your production service needs to let users upload files to your server, it's often more reliable for the user to upload directly to S3 instead of your server acting as an intermediary. + +To facilitate this, you can presign URLs for S3 files. This generates a URL with a signature that allows a user to securely upload that specific file to S3, without exposing your credentials or granting them unnecessary access to your bucket. + +```ts +// Generate a presigned URL that expires in 24 hours (default) +const url = s3file.presign(); + +// Custom expiration time (in seconds) +const url2 = s3file.presign({ expiresIn: 3600 }); // 1 hour + +// Using static method +const url3 = Bun.S3.presign("my-file.txt", { + bucket: "my-bucket", + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + expiresIn: 3600, +}); +``` + +### Setting ACLs + +To set an ACL (access control list) on a presigned URL, pass the `acl` option: + +```ts +const url = s3file.presign({ + acl: "public-read", + expiresIn: 3600, +}); +``` + +You can pass any of the following ACLs: + +| ACL | Explanation | +| ----------------------------- | ------------------------------------------------------------------- | +| `"public-read"` | The object is readable by the public. | +| `"private"` | The object is readable only by the bucket owner. | +| `"public-read-write"` | The object is readable and writable by the public. | +| `"authenticated-read"` | The object is readable by the bucket owner and authenticated users. | +| `"aws-exec-read"` | The object is readable by the AWS account that made the request. | +| `"bucket-owner-read"` | The object is readable by the bucket owner. | +| `"bucket-owner-full-control"` | The object is readable and writable by the bucket owner. | +| `"log-delivery-write"` | The object is writable by AWS services used for log delivery. | + +### Expiring URLs + +To set an expiration time for a presigned URL, pass the `expiresIn` option. + +```ts +const url = s3file.presign({ + // Seconds + expiresIn: 3600, // 1 hour +}); +``` + +### `method` + +To set the HTTP method for a presigned URL, pass the `method` option. + +```ts +const url = s3file.presign({ + method: "PUT", + // method: "DELETE", + // method: "GET", + // method: "HEAD", + // method: "POST", + // method: "PUT", +}); +``` + +### `new Response(S3File)` + +To quickly redirect users to a presigned URL for an S3 file, you can pass an `S3File` instance to a `Response` object as the body. + +```ts +const response = new Response(s3file); +console.log(response); +``` + +This will automatically redirect the user to the presigned URL for the S3 file, saving you the memory, time, and bandwidth cost of downloading the file to your server and sending it back to the user. + +```ts +Response (0 KB) { + ok: false, + url: "", + status: 302, + statusText: "", + headers: Headers { + "location": "https://.r2.cloudflarestorage.com/...", + }, + redirected: true, + bodyUsed: false +} +``` + +## Support for S3-Compatible Services + +Bun's S3 implementation works with any S3-compatible storage service. Just specify the appropriate endpoint: + +```ts +import { s3 } from "bun"; + +// CloudFlare R2 +const r2file = s3("my-file.txt", { + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://.r2.cloudflarestorage.com", +}); + +// DigitalOcean Spaces +const spacesFile = s3("my-file.txt", { + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://.digitaloceanspaces.com", +}); + +// MinIO +const minioFile = s3("my-file.txt", { + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "http://localhost:9000", +}); +``` + +## Credentials + +Credentials are one of the hardest parts of using S3, and we've tried to make it as easy as possible. By default, Bun reads the following environment variables for credentials. + +| Option name | Environment variable | +| ----------------- | ---------------------- | +| `accessKeyId` | `S3_ACCESS_KEY_ID` | +| `secretAccessKey` | `S3_SECRET_ACCESS_KEY` | +| `region` | `S3_REGION` | +| `endpoint` | `S3_ENDPOINT` | +| `bucket` | `S3_BUCKET` | +| `sessionToken` | `S3_SESSION_TOKEN` | + +If the `S3_*` environment variable is not set, Bun will also check for the `AWS_*` environment variable, for each of the above options. + +| Option name | Fallback environment variable | +| ----------------- | ----------------------------- | +| `accessKeyId` | `AWS_ACCESS_KEY_ID` | +| `secretAccessKey` | `AWS_SECRET_ACCESS_KEY` | +| `region` | `AWS_REGION` | +| `endpoint` | `AWS_ENDPOINT` | +| `bucket` | `AWS_BUCKET` | +| `sessionToken` | `AWS_SESSION_TOKEN` | + +These environment variables are read from [`.env` files](/docs/runtime/env) or from the process environment at initialization time (`process.env` is not used for this). + +These defaults are overriden by the options you pass to `s3(credentials)`, `new Bun.S3(credentials)`, or any of the methods that accept credentials. So if, for example, you use the same credentials for different buckets, you can set the credentials once in your `.env` file and then pass `bucket: "my-bucket"` to the `s3()` helper function without having to specify all the credentials again. + +### `S3` Buckets + +Passing around all of these credentials can be cumbersome. To make it easier, you can create a `S3` bucket instance. + +```ts +import { S3 } from "bun"; + +const bucket = new S3({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // sessionToken: "..." + endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "http://localhost:9000", // MinIO +}); + +// bucket is a function that creates `S3File` instances (lazy) +const file = bucket("my-file.txt"); + +// Write to S3 +await file.write("Hello World!"); + +// Read from S3 +const text = await file.text(); + +// Write using a Response +await file.write(new Response("Hello World!")); + +// Presign a URL +const url = file.presign({ + expiresIn: 60 * 60 * 24, // 1 day + acl: "public-read", +}); + +// Delete the file +await file.unlink(); +``` + +#### Read a file from an `S3` bucket + +The `S3` bucket instance is itself a function that creates `S3File` instances. It provides a more convenient API for interacting with S3. + +```ts +const s3file = bucket("my-file.txt"); +const text = await s3file.text(); +const json = await s3file.json(); +const bytes = await s3file.bytes(); +const arrayBuffer = await s3file.arrayBuffer(); +``` + +#### Write a file to S3 + +To write a file to the bucket, you can use the `write` method. + +```ts +const bucket = new Bun.S3({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + bucket: "my-bucket", +}); +await bucket.write("my-file.txt", "Hello World!"); +await bucket.write("my-file.txt", new Response("Hello World!")); +``` + +You can also call `.write` on the `S3File` instance created by the `S3` bucket instance. + +```ts +const s3file = bucket("my-file.txt"); +await s3file.write("Hello World!", { + type: "text/plain", +}); +await s3file.write(new Response("Hello World!")); +``` + +#### Delete a file from S3 + +To delete a file from the bucket, you can use the `delete` method. + +```ts +const bucket = new Bun.S3({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +await bucket.delete("my-file.txt"); +``` + +You can also use the `unlink` method, which is an alias for `delete`. + +```ts +// "delete" and "unlink" are aliases of each other. +await bucket.unlink("my-file.txt"); +``` + +## `S3File` + +`S3File` instances are created by calling the `S3` instance method or the `s3()` helper function. Like `Bun.file()`, `S3File` instances are lazy. They don't refer to something that necessarily exists at the time of creation. That's why all the methods that don't involve network requests are fully synchronous. + +```ts +interface S3File extends Blob { + slice(start: number, end?: number): S3File; + exists(): Promise; + unlink(): Promise; + presign(options: S3Options): string; + text(): Promise; + json(): Promise; + bytes(): Promise; + arrayBuffer(): Promise; + stream(options: S3Options): ReadableStream; + write( + data: + | string + | Uint8Array + | ArrayBuffer + | Blob + | ReadableStream + | Response + | Request, + options?: BlobPropertyBag, + ): Promise; + + readonly size: Promise; + + // ... more omitted for brevity +} +``` + +Like `Bun.file()`, `S3File` extends [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob), so all the methods that are available on `Blob` are also available on `S3File`. The same API for reading data from a local file is also available for reading data from S3. + +| Method | Output | +| ---------------------------- | ---------------- | +| `await s3File.text()` | `string` | +| `await s3File.bytes()` | `Uint8Array` | +| `await s3File.json()` | `JSON` | +| `await s3File.stream()` | `ReadableStream` | +| `await s3File.arrayBuffer()` | `ArrayBuffer` | + +That means using `S3File` instances with `fetch()`, `Response`, and other web APIs that accept `Blob` instances just works. + +### Partial reads + +To read a partial range of a file, you can use the `slice` method. + +```ts +const partial = s3file.slice(0, 1024); + +// Read the partial range as a Uint8Array +const bytes = await partial.bytes(); + +// Read the partial range as a string +const text = await partial.text(); +``` + +Internally, this works by using the HTTP `Range` header to request only the bytes you want. This `slice` method is the same as [`Blob.prototype.slice`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice). + +## Error codes + +When Bun's S3 API throws an error, it will have a `code` property that matches one of the following values: + +- `ERR_S3_MISSING_CREDENTIALS` +- `ERR_S3_INVALID_METHOD` +- `ERR_S3_INVALID_PATH` +- `ERR_S3_INVALID_ENDPOINT` +- `ERR_S3_INVALID_SIGNATURE` +- `ERR_S3_INVALID_SESSION_TOKEN` + +When the S3 Object Storage service returns an error (that is, not Bun), it will be an `S3Error` instance (an `Error` instance with the name `"S3Error"`). + +## `S3` static methods + +The `S3` class provides several static methods for interacting with S3. + +### `S3.presign` + +To generate a presigned URL for an S3 file, you can use the `S3.presign` method. + +```ts +import { S3 } from "bun"; + +const url = S3.presign("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + expiresIn: 3600, + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 +}); +``` + +This is the same as `S3File.prototype.presign` and `new S3(credentials).presign`, as a static method on the `S3` class. + +### `S3.exists` + +To check if an S3 file exists, you can use the `S3.exists` method. + +```ts +import { S3 } from "bun"; + +const exists = await S3.exists("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}); +``` + +The same method also works on `S3File` instances. + +```ts +const s3file = Bun.s3("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); +const exists = await s3file.exists(); +``` + +### `S3.size` + +To get the size of an S3 file, you can use the `S3.size` method. + +```ts +import { S3 } from "bun"; +const size = await S3.size("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}); +``` + +### `S3.unlink` + +To delete an S3 file, you can use the `S3.unlink` method. + +```ts +import { S3 } from "bun"; + +await S3.unlink("my-file.txt", { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}); +``` + +## s3:// protocol + +To make it easier to use the same code for local files and S3 files, the `s3://` protocol is supported in `fetch` and `Bun.file()`. + +```ts +const response = await fetch("s3://my-bucket/my-file.txt"); +const file = Bun.file("s3://my-bucket/my-file.txt"); +``` + +This is the equivalent of calling `Bun.s3("my-file.txt", { bucket: "my-bucket" })`. + +This `s3://` protocol exists to make it easier to use the same code for local files and S3 files. diff --git a/docs/nav.ts b/docs/nav.ts index 900cfdcb24..124ff0febc 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -311,6 +311,9 @@ export default { page("api/streams", "Streams", { description: `Reading, writing, and manipulating streams of data in Bun.`, }), // "`Bun.serve`"), + page("api/s3", "S3 Object Storage", { + description: `Bun provides fast, native bindings for interacting with S3-compatible object storage services.`, + }), page("api/file-io", "File I/O", { description: `Read and write files fast with Bun's heavily optimized file system API.`, }), // "`Bun.write`"), From debd8a0ebaa2007d67935dad5e430266d97be182 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 00:05:14 -0800 Subject: [PATCH 25/56] Support `BUN_CONFIG_VERBOSE_FETCH` in S3 --- src/s3.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/s3.zig b/src/s3.zig index 396ee291c9..6ccec287fc 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -1459,7 +1459,7 @@ pub const AWSCredentials = struct { .follow, .{ .http_proxy = if (proxy.len > 0) bun.URL.parse(proxy) else null, - .verbose = .none, + .verbose = task.vm.getVerboseFetch(), .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), }, ); @@ -1579,7 +1579,7 @@ pub const AWSCredentials = struct { .follow, .{ .http_proxy = if (owned_proxy.len > 0) bun.URL.parse(owned_proxy) else null, - .verbose = .none, + .verbose = task.vm.getVerboseFetch(), .signals = task.signals, .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), }, From ed0b4e1a6e3f76a9063cb246f5a6574476da36e0 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Sat, 4 Jan 2025 03:23:51 -0500 Subject: [PATCH 26/56] fix(build/html): handle relative paths in script src (#16153) --- src/HTMLScanner.zig | 13 +++++++++++-- src/resolver/resolve_path.zig | 13 ++++++++++--- test/bundler/bundler_html.test.ts | 32 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/HTMLScanner.zig b/src/HTMLScanner.zig index dd69942977..b029e3dd46 100644 --- a/src/HTMLScanner.zig +++ b/src/HTMLScanner.zig @@ -33,8 +33,17 @@ fn createImportRecord(this: *HTMLScanner, input_path: []const u8, kind: ImportKi // In that case, we don't want to use the absolute filesystem path, we want to use the path relative to the project root const path_to_use = if (input_path.len > 1 and input_path[0] == '/') bun.path.joinAbsString(bun.fs.FileSystem.instance.top_level_dir, &[_][]const u8{input_path[1..]}, .auto) - else - input_path; + + // Check if imports to (e.g) "App.tsx" are actually relative imoprts w/o the "./" + else if (input_path.len > 2 and input_path[0] != '.' and input_path[1] != '/') blk: { + const index_of_dot = std.mem.lastIndexOfScalar(u8, input_path, '.') orelse break :blk input_path; + const ext = input_path[index_of_dot..]; + if (ext.len > 4) break :blk input_path; + // /foo/bar/index.html -> /foo/bar + const dirname: []const u8 = std.fs.path.dirname(this.source.path.text) orelse break :blk input_path; + const resolved = bun.path.joinAbsString(dirname, &[_][]const u8{input_path}, .auto); + break :blk if (bun.sys.exists(resolved)) resolved else input_path; + } else input_path; const record = ImportRecord{ .path = fs.Path.init(try this.allocator.dupeZ(u8, path_to_use)), diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index bf7a341467..46f2dcd03a 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1252,9 +1252,11 @@ pub fn joinAbs(cwd: []const u8, comptime _platform: Platform, part: []const u8) return joinAbsString(cwd, &.{part}, _platform); } -// Convert parts of potentially invalid file paths into a single valid filpeath -// without querying the filesystem -// This is the equivalent of path.resolve +/// Convert parts of potentially invalid file paths into a single valid filpeath +/// without querying the filesystem +/// This is the equivalent of path.resolve +/// +/// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsString(_cwd: []const u8, parts: anytype, comptime _platform: Platform) []const u8 { return joinAbsStringBuf( _cwd, @@ -1264,6 +1266,11 @@ pub fn joinAbsString(_cwd: []const u8, parts: anytype, comptime _platform: Platf ); } +/// Convert parts of potentially invalid file paths into a single valid filpeath +/// without querying the filesystem +/// This is the equivalent of path.resolve +/// +/// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsStringZ(_cwd: []const u8, parts: anytype, comptime _platform: Platform) [:0]const u8 { return joinAbsStringBufZ( _cwd, diff --git a/test/bundler/bundler_html.test.ts b/test/bundler/bundler_html.test.ts index 9778ee55ea..e748bddcdb 100644 --- a/test/bundler/bundler_html.test.ts +++ b/test/bundler/bundler_html.test.ts @@ -33,6 +33,38 @@ describe("bundler", () => { }, }); + // Test relative paths without "./" in script src + itBundled("html/implicit-relative-paths", { + outdir: "out/", + files: { + "/src/index.html": ` + + + + + + + +

Hello World

+ +`, + "/src/styles.css": "body { background-color: red; }", + "/src/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + root: "/src", + entryPoints: ["/src/index.html"], + + onAfterBundle(api) { + // Check that output HTML references hashed filenames + api.expectFile("out/index.html").not.toContain("styles.css"); + api.expectFile("out/index.html").not.toContain("script.js"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + }, + }); + // Test multiple script and style bundling itBundled("html/multiple-assets", { outdir: "out/", From 33233b16070a3af5fe3dd2b5ef3f9310381af47a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 00:32:17 -0800 Subject: [PATCH 27/56] Don't include //# sourcemap comments in .html or .css files (#16159) --- src/bundler/bundle_v2.zig | 20 ++++-- test/bundler/bundler_html.test.ts | 103 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index a9d2345bf1..e7cccd04b3 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -12961,7 +12961,7 @@ pub const LinkerContext = struct { chunk, chunks, &display_size, - c.options.source_maps != .none, + chunk.content.sourcemap(c.options.source_maps) != .none, ); var code_result = _code_result catch @panic("Failed to allocate memory for output file"); @@ -12974,7 +12974,7 @@ pub const LinkerContext = struct { chunk.final_rel_path, ); - switch (c.options.source_maps) { + switch (chunk.content.sourcemap(c.options.source_maps)) { .external, .linked => |tag| { const output_source_map = chunk.output_source_map.finalize(bun.default_allocator, code_result.shifts) catch @panic("Failed to allocate memory for external source map"); var source_map_final_rel_path = default_allocator.alloc(u8, chunk.final_rel_path.len + ".map".len) catch unreachable; @@ -13280,7 +13280,7 @@ pub const LinkerContext = struct { chunk, chunks, &display_size, - c.options.source_maps != .none, + chunk.content.sourcemap(c.options.source_maps) != .none, ) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)}); var source_map_output_file: ?options.OutputFile = null; @@ -13293,7 +13293,7 @@ pub const LinkerContext = struct { chunk.final_rel_path, ); - switch (c.options.source_maps) { + switch (chunk.content.sourcemap(c.options.source_maps)) { .external, .linked => |tag| { const output_source_map = chunk.output_source_map.finalize(source_map_allocator, code_result.shifts) catch @panic("Failed to allocate memory for external source map"); const source_map_final_rel_path = strings.concat(default_allocator, &.{ @@ -15463,6 +15463,18 @@ pub const Chunk = struct { javascript: JavaScriptChunk, css: CssChunk, html: HtmlChunk, + + pub fn sourcemap(this: *const Content, default: options.SourceMapOption) options.SourceMapOption { + return switch (this.*) { + .javascript => default, + // TODO: + .css => options.SourceMapOption.none, + + // probably never + .html => options.SourceMapOption.none, + }; + } + pub fn loader(this: *const Content) Loader { return switch (this.*) { .javascript => .js, diff --git a/test/bundler/bundler_html.test.ts b/test/bundler/bundler_html.test.ts index e748bddcdb..6314971925 100644 --- a/test/bundler/bundler_html.test.ts +++ b/test/bundler/bundler_html.test.ts @@ -809,4 +809,107 @@ body { expect(jsBundle).toContain("App loaded"); }, }); + + // Test that sourcemap comments are not included in HTML and CSS files + itBundled("html/no-sourcemap-comments", { + outdir: "out/", + sourceMap: "linked", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "linked", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); + + // Also test with inline sourcemaps + itBundled("html/no-sourcemap-comments-inline", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "inline", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); }); From 4454ebb152fef8c40a2982156e0fbd512566a08c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 00:54:44 -0800 Subject: [PATCH 28/56] Allow `http://` endpoints in Bun.S3 --- .vscode/launch.json | 28 ++++++++++++++++++++++++++++ src/s3.zig | 22 +++++++++++++--------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 817b7533d3..872ff1e7e1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -31,6 +32,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "--only", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "1", @@ -53,6 +55,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -69,6 +72,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "0", "BUN_DEBUG_jest": "1", @@ -85,6 +89,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "--watch", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -101,6 +106,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "--hot", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -117,6 +123,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -139,6 +146,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${file}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -162,6 +170,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "FORCE_COLOR": "0", "BUN_DEBUG_QUIET_LOGS": "1", @@ -178,6 +187,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", @@ -197,6 +207,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "0", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -212,6 +223,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "--watch", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { // "BUN_DEBUG_DEBUGGER": "1", // "BUN_DEBUG_INTERNAL_DEBUGGER": "1", @@ -230,6 +242,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "--hot", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -245,6 +258,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "FORCE_COLOR": "0", "BUN_DEBUG_QUIET_LOGS": "1", @@ -267,6 +281,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["run", "${fileBasename}"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "FORCE_COLOR": "0", "BUN_DEBUG_QUIET_LOGS": "1", @@ -290,6 +305,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -306,6 +322,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -322,6 +339,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -338,6 +356,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "--watch", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -354,6 +373,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "--hot", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -370,6 +390,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -392,6 +413,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_DEBUG_jest": "1", @@ -415,6 +437,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["exec", "${input:testName}"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -431,6 +454,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -446,6 +470,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "0", @@ -461,6 +486,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["test"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -482,6 +508,7 @@ "program": "${workspaceFolder}/build/debug/bun-debug", "args": ["install"], "cwd": "${fileDirname}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", @@ -497,6 +524,7 @@ "program": "node", "args": ["test/runner.node.mjs"], "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env", "env": { "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", diff --git a/src/s3.zig b/src/s3.zig index 6ccec287fc..15db08a8df 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -639,6 +639,10 @@ pub const AWSCredentials = struct { } } }; + + // Default to https. Only use http if they explicit pass "http://" as the endpoint. + const protocol = if (strings.hasPrefixComptime(this.endpoint, "http://")) "http" else "https"; + // detect service name and host from region or endpoint var encoded_host_buffer: [512]u8 = undefined; var encoded_host: []const u8 = ""; @@ -708,28 +712,28 @@ pub const AWSCredentials = struct { if (encoded_session_token) |token| { break :brk try std.fmt.allocPrint( bun.default_allocator, - "https://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, ); } else { break :brk try std.fmt.allocPrint( bun.default_allocator, - "https://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, ); } } else { if (encoded_session_token) |token| { break :brk try std.fmt.allocPrint( bun.default_allocator, - "https://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, ); } else { break :brk try std.fmt.allocPrint( bun.default_allocator, - "https://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, ); } } @@ -801,7 +805,7 @@ pub const AWSCredentials = struct { .host = host, .authorization = authorization, .acl = signOptions.acl, - .url = try std.fmt.allocPrint(bun.default_allocator, "https://{s}{s}{s}", .{ host, normalizedPath, if (search_params) |s| s else "" }), + .url = try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}{s}", .{ protocol, host, normalizedPath, if (search_params) |s| s else "" }), ._headers = [_]picohttp.Header{ .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, .{ .name = "x-amz-date", .value = amz_date }, From 79aa5d16dfe63c986dd86c65d171cf6a446d5740 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Sat, 4 Jan 2025 01:22:24 -0800 Subject: [PATCH 29/56] skip root scripts if root is filtered out with `--filter` (#16152) Co-authored-by: Jarred Sumner --- src/install/install.zig | 58 +++++++++++++++++++++++-- src/install/lockfile.zig | 56 ++++++++++-------------- test/cli/install/bun-workspaces.test.ts | 56 +++++++++++++++++++++++- 3 files changed, 132 insertions(+), 38 deletions(-) diff --git a/src/install/install.zig b/src/install/install.zig index 3e1f3e89d9..d16c20ce83 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -13903,13 +13903,14 @@ pub const PackageManager = struct { pub fn installPackages( this: *PackageManager, ctx: Command.Context, - original_cwd: string, + workspace_filters: []const WorkspaceFilter, + install_root_dependencies: bool, comptime log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { const original_trees = this.lockfile.buffers.trees; const original_tree_dep_ids = this.lockfile.buffers.hoisted_dependencies; - try this.lockfile.filter(this.log, this, original_cwd); + try this.lockfile.filter(this.log, this, install_root_dependencies, workspace_filters); defer { this.lockfile.buffers.trees = original_trees; @@ -15027,11 +15028,60 @@ pub const PackageManager = struct { return; } + var path_buf: bun.PathBuffer = undefined; + var workspace_filters: std.ArrayListUnmanaged(WorkspaceFilter) = .{}; + // only populated when subcommand is `.install` + if (manager.subcommand == .install and manager.options.filter_patterns.len > 0) { + try workspace_filters.ensureUnusedCapacity(manager.allocator, manager.options.filter_patterns.len); + for (manager.options.filter_patterns) |pattern| { + try workspace_filters.append(manager.allocator, try WorkspaceFilter.init(manager.allocator, pattern, original_cwd, &path_buf)); + } + } + defer workspace_filters.deinit(manager.allocator); + + var install_root_dependencies = workspace_filters.items.len == 0; + if (!install_root_dependencies) { + const pkg_names = manager.lockfile.packages.items(.name); + + const abs_root_path = abs_root_path: { + if (comptime !Environment.isWindows) { + break :abs_root_path strings.withoutTrailingSlash(FileSystem.instance.top_level_dir); + } + + var abs_path = Path.pathToPosixBuf(u8, FileSystem.instance.top_level_dir, &path_buf); + break :abs_root_path strings.withoutTrailingSlash(abs_path[Path.windowsVolumeNameLen(abs_path)[0]..]); + }; + + for (workspace_filters.items) |filter| { + const pattern, const path_or_name = switch (filter) { + .name => |pattern| .{ pattern, pkg_names[0].slice(manager.lockfile.buffers.string_bytes.items) }, + .path => |pattern| .{ pattern, abs_root_path }, + .all => { + install_root_dependencies = true; + continue; + }, + }; + + switch (bun.glob.walk.matchImpl(pattern, path_or_name)) { + .match, .negate_match => install_root_dependencies = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + install_root_dependencies = false; + break; + }, + + .no_match => {}, + } + } + } + var install_summary = PackageInstall.Summary{}; if (manager.options.do.install_packages) { install_summary = try manager.installPackages( ctx, - original_cwd, + workspace_filters.items, + install_root_dependencies, log_level, ); } @@ -15091,7 +15141,7 @@ pub const PackageManager = struct { } } - if (manager.options.do.run_scripts) { + if (manager.options.do.run_scripts and install_root_dependencies) { if (manager.root_lifecycle_scripts) |scripts| { if (comptime Environment.allow_assert) { bun.assert(scripts.total > 0); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index d57a66ae93..47f4c6d8ac 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -690,6 +690,7 @@ pub const Tree = struct { manager: if (method == .filter) *const PackageManager else void, sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{} else {}, + install_root_dependencies: if (method == .filter) bool else void, path_buf: []u8, pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { @@ -747,13 +748,6 @@ pub const Tree = struct { this.queue.deinit(); this.sort_buf.deinit(this.allocator); - if (comptime method == .filter) { - for (this.workspace_filters) |workspace_filter| { - workspace_filter.deinit(this.lockfile.allocator); - } - this.lockfile.allocator.free(this.workspace_filters); - } - // take over the `builder.list` pointer for only trees if (@intFromPtr(trees.ptr) != @intFromPtr(list_ptr)) { var new: [*]Tree = @ptrCast(list_ptr); @@ -870,27 +864,32 @@ pub const Tree = struct { continue; } - if (builder.manager.subcommand == .install) { + if (builder.manager.subcommand == .install) dont_skip: { // only do this when parent is root. workspaces are always dependencies of the root // package, and the root package is always called with `processSubtree` if (parent_pkg_id == 0 and builder.workspace_filters.len > 0) { + if (!builder.dependencies[dep_id].behavior.isWorkspaceOnly()) { + if (builder.install_root_dependencies) { + break :dont_skip; + } + + continue; + } + var match = false; for (builder.workspace_filters) |workspace_filter| { const res_id = builder.resolutions[dep_id]; const pattern, const path_or_name = switch (workspace_filter) { - .name => |pattern| .{ pattern, if (builder.dependencies[dep_id].behavior.isWorkspaceOnly()) - pkg_names[res_id].slice(builder.buf()) - else - pkg_names[0].slice(builder.buf()) }, + .name => |pattern| .{ pattern, pkg_names[res_id].slice(builder.buf()) }, .path => |pattern| path: { - const res_path = if (builder.dependencies[dep_id].behavior.isWorkspaceOnly() and pkg_resolutions[res_id].tag == .workspace) - pkg_resolutions[res_id].value.workspace.slice(builder.buf()) - else - // dependnecy of the root package.json. use top level dir - FileSystem.instance.top_level_dir; + const res = &pkg_resolutions[res_id]; + if (res.tag != .workspace) { + break :dont_skip; + } + const res_path = res.value.workspace.slice(builder.buf()); // occupy `builder.path_buf` var abs_res_path = strings.withoutTrailingSlash(bun.path.joinAbsStringBuf( @@ -1570,16 +1569,17 @@ pub fn resolve( lockfile: *Lockfile, log: *logger.Log, ) Tree.SubtreeError!void { - return lockfile.hoist(log, .resolvable, {}, {}); + return lockfile.hoist(log, .resolvable, {}, {}, {}); } pub fn filter( lockfile: *Lockfile, log: *logger.Log, manager: *PackageManager, - cwd: string, + install_root_dependencies: bool, + workspace_filters: []const WorkspaceFilter, ) Tree.SubtreeError!void { - return lockfile.hoist(log, .filter, manager, cwd); + return lockfile.hoist(log, .filter, manager, install_root_dependencies, workspace_filters); } /// Sets `buffers.trees` and `buffers.hoisted_dependencies` @@ -1588,7 +1588,8 @@ pub fn hoist( log: *logger.Log, comptime method: Tree.BuilderMethod, manager: if (method == .filter) *PackageManager else void, - cwd: if (method == .filter) string else void, + install_root_dependencies: if (method == .filter) bool else void, + workspace_filters: if (method == .filter) []const WorkspaceFilter else void, ) Tree.SubtreeError!void { const allocator = lockfile.allocator; var slice = lockfile.packages.slice(); @@ -1606,19 +1607,10 @@ pub fn hoist( .lockfile = lockfile, .manager = manager, .path_buf = &path_buf, + .install_root_dependencies = install_root_dependencies, + .workspace_filters = workspace_filters, }; - if (comptime method == .filter) { - if (manager.options.filter_patterns.len > 0) { - var filters = try std.ArrayListUnmanaged(WorkspaceFilter).initCapacity(allocator, manager.options.filter_patterns.len); - for (manager.options.filter_patterns) |pattern| { - try filters.append(allocator, try WorkspaceFilter.init(allocator, pattern, cwd, &path_buf)); - } - - builder.workspace_filters = filters.items; - } - } - try (Tree{}).processSubtree( Tree.root_dep_id, Tree.invalid_id, diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index b282e6c1be..a72ece2280 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -381,8 +381,6 @@ describe("workspace aliases", async () => { ), ]); - console.log({ packageDir }); - await runBunInstall(env, packageDir); const files = await Promise.all( ["a0", "a1", "a2", "a3", "a4", "a5"].map(name => @@ -1284,6 +1282,60 @@ for (const version of versions) { } describe("install --filter", () => { + test("does not run root scripts if root is filtered out", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + scripts: { + postinstall: `${bunExe()} root.js`, + }, + }), + ), + write(join(packageDir, "root.js"), `require("fs").writeFileSync("root.txt", "")`), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + scripts: { + postinstall: `${bunExe()} pkg1.js`, + }, + }), + ), + write(join(packageDir, "packages", "pkg1", "pkg1.js"), `require("fs").writeFileSync("pkg1.txt", "")`), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "pkg1.txt"))).toBeTrue(); + + await rm(join(packageDir, "packages", "pkg1", "pkg1.txt")); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "root"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1.txt"))).toBeFalse(); + }); + test("basic", async () => { await Promise.all([ write( From a53f2e6aaa0e979c5acaaf87dd21a9d3c45cf944 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 4 Jan 2025 01:22:48 -0800 Subject: [PATCH 30/56] fix test on windows (#16151) --- test/js/bun/s3/s3.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index f265c4c95e..88a17f85a0 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -607,7 +607,7 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { await s3file.write("Hello Bun!"); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBe("ENAMETOOLONG"); + expect(["ENAMETOOLONG", "ERR_S3_INVALID_PATH"]).toContain(e?.code); } }), ); From 5fe9b6f4268d800cd3ae58720b86cc5c1127bc83 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 02:44:17 -0800 Subject: [PATCH 31/56] Improve MinIO support in Bun.S3 --- src/s3.zig | 15 +++++++++++-- test/js/bun/s3/s3-insecure.test.ts | 35 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 test/js/bun/s3/s3-insecure.test.ts diff --git a/src/s3.zig b/src/s3.zig index 15db08a8df..b92fbe1913 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -57,6 +57,9 @@ pub const AWSCredentials = struct { bucket: []const u8, sessionToken: []const u8, + /// Important for MinIO support. + insecure_http: bool = false, + ref_count: u32 = 1, pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); @@ -168,9 +171,15 @@ pub const AWSCredentials = struct { defer str.deref(); if (str.tag != .Empty and str.tag != .Dead) { new_credentials._endpointSlice = str.toUTF8(bun.default_allocator); - const normalized_endpoint = bun.URL.parse(new_credentials._endpointSlice.?.slice()).host; + const url = bun.URL.parse(new_credentials._endpointSlice.?.slice()); + const normalized_endpoint = url.host; if (normalized_endpoint.len > 0) { new_credentials.credentials.endpoint = normalized_endpoint; + + // Default to https:// + // Only use http:// if the endpoint specifically starts with 'http://' + new_credentials.credentials.insecure_http = url.isHTTP(); + new_credentials.changed_credentials = true; } } @@ -283,6 +292,8 @@ pub const AWSCredentials = struct { bun.default_allocator.dupe(u8, this.sessionToken) catch bun.outOfMemory() else "", + + .insecure_http = this.insecure_http, }); } pub fn deinit(this: *@This()) void { @@ -641,7 +652,7 @@ pub const AWSCredentials = struct { }; // Default to https. Only use http if they explicit pass "http://" as the endpoint. - const protocol = if (strings.hasPrefixComptime(this.endpoint, "http://")) "http" else "https"; + const protocol = if (this.insecure_http) "http" else "https"; // detect service name and host from region or endpoint var encoded_host_buffer: [512]u8 = undefined; diff --git a/test/js/bun/s3/s3-insecure.test.ts b/test/js/bun/s3/s3-insecure.test.ts new file mode 100644 index 0000000000..ee62e37b68 --- /dev/null +++ b/test/js/bun/s3/s3-insecure.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "bun:test"; +import { S3, s3, file } from "bun"; + +describe("s3", async () => { + it("should not fail to connect when endpoint is http and not https", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response("<>lol!", { + headers: { + "Content-Type": "text/plain", + }, + status: 400, + }); + }, + }); + + const s3 = new S3({ + accessKeyId: "test", + secretAccessKey: "test", + endpoint: server.url.href, + bucket: "test", + }); + + const file = s3("hello.txt"); + let err; + try { + await file.text(); + } catch (e) { + err = e; + } + // Test we don't get ConnectionRefused + expect(err.code!).toBe("UnknownError"); + }); +}); From cc52828d5402ca19ea7e9b050cf93631aaa7b15a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 04:17:03 -0800 Subject: [PATCH 32/56] Remove rejectOnNextTick (#16161) --- src/bun.js/api/bun/socket.zig | 5 +++-- src/bun.js/bindings/bindings.zig | 21 +-------------------- src/bun.js/bindings/headers.zig | 2 -- src/bun.js/javascript.zig | 5 +++++ src/bun.js/webcore/S3File.zig | 4 ++-- src/bun.js/webcore/blob.zig | 23 ++++++++++++++--------- src/bun.js/webcore/response.zig | 2 -- src/s3.zig | 14 ++++++-------- 8 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 9971f84d16..7a2fef50eb 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1626,6 +1626,7 @@ fn NewSocket(comptime ssl: bool) type { if (callback == .zero) { if (handlers.promise.trySwap()) |promise| { + handlers.promise.deinit(); if (this.this_value != .zero) { this.this_value = .zero; } @@ -1633,7 +1634,7 @@ fn NewSocket(comptime ssl: bool) type { // reject the promise on connect() error const err_value = err.toErrorInstance(globalObject); - promise.asPromise().?.rejectOnNextTick(globalObject, err_value); + promise.asPromise().?.reject(globalObject, err_value); } return; @@ -1657,7 +1658,7 @@ fn NewSocket(comptime ssl: bool) type { // The error is effectively handled, but we should still reject the promise. var promise = val.asPromise().?; const err_ = err.toErrorInstance(globalObject); - promise.rejectOnNextTickAsHandled(globalObject, err_); + promise.rejectAsHandled(globalObject, err_); } } pub fn onConnectError(this: *This, _: Socket, errno: c_int) void { diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 964eab3dee..da43353ea4 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2344,10 +2344,6 @@ pub const JSPromise = extern struct { this.reject(globalThis, val); } - pub fn rejectOnNextTick(this: *WeakType, globalThis: *JSC.JSGlobalObject, val: JSC.JSValue) void { - this.swap().rejectOnNextTick(globalThis, val); - } - pub fn resolve(this: *WeakType, globalThis: *JSC.JSGlobalObject, val: JSC.JSValue) void { this.swap().resolve(globalThis, val); } @@ -2425,9 +2421,7 @@ pub const JSPromise = extern struct { this.reject(globalThis, val); } - pub fn rejectOnNextTick(this: *Strong, globalThis: *JSC.JSGlobalObject, val: JSC.JSValue) void { - this.swap().rejectOnNextTick(globalThis, val); - } + pub const rejectOnNextTick = @compileError("Either use an event loop task, or you're draining microtasks when you shouldn't be."); pub fn resolve(this: *Strong, globalThis: *JSC.JSGlobalObject, val: JSC.JSValue) void { this.swap().resolve(globalThis, val); @@ -2544,18 +2538,6 @@ pub const JSPromise = extern struct { return cppFn("resolveOnNextTick", .{ promise, globalThis, value }); } - pub fn rejectOnNextTick(promise: *JSC.JSPromise, globalThis: *JSGlobalObject, value: JSC.JSValue) void { - return rejectOnNextTickWithHandled(promise, globalThis, value, false); - } - - pub fn rejectOnNextTickAsHandled(promise: *JSC.JSPromise, globalThis: *JSGlobalObject, value: JSC.JSValue) void { - return rejectOnNextTickWithHandled(promise, globalThis, value, true); - } - - pub fn rejectOnNextTickWithHandled(promise: *JSC.JSPromise, globalThis: *JSGlobalObject, value: JSC.JSValue, handled: bool) void { - return cppFn("rejectOnNextTickWithHandled", .{ promise, globalThis, value, handled }); - } - /// Create a new promise with an already fulfilled value /// This is the faster function for doing that. pub fn resolvedPromiseValue(globalThis: *JSGlobalObject, value: JSValue) JSValue { @@ -2621,7 +2603,6 @@ pub const JSPromise = extern struct { "reject", "rejectAsHandled", "rejectAsHandledException", - "rejectOnNextTickWithHandled", "rejectedPromise", "rejectedPromiseValue", "resolve", diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 91264230dc..642d54cf3b 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -155,11 +155,9 @@ pub extern fn JSC__JSPromise__rejectAsHandled(arg0: ?*bindings.JSPromise, arg1: pub extern fn JSC__JSPromise__rejectAsHandledException(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, arg2: [*c]bindings.Exception) void; pub extern fn JSC__JSPromise__rejectedPromise(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*bindings.JSPromise; pub extern fn JSC__JSPromise__rejectedPromiseValue(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) JSC__JSValue; -pub extern fn JSC__JSPromise__rejectOnNextTickWithHandled(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue, arg3: bool) void; pub extern fn JSC__JSPromise__resolve(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__resolvedPromise(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*bindings.JSPromise; pub extern fn JSC__JSPromise__resolvedPromiseValue(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) JSC__JSValue; -pub extern fn JSC__JSPromise__resolveOnNextTick(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__result(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) JSC__JSValue; pub extern fn JSC__JSPromise__setHandled(arg0: ?*bindings.JSPromise, arg1: *bindings.VM) void; pub extern fn JSC__JSPromise__status(arg0: [*c]const JSC__JSPromise, arg1: *bindings.VM) u32; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 6b55cf2f21..404558d102 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2816,6 +2816,11 @@ pub const VirtualMachine = struct { pub const main_file_name: string = "bun:main"; pub fn drainMicrotasks(this: *VirtualMachine) void { + if (comptime Environment.isDebug) { + if (this.eventLoop().debug.is_inside_tick_queue) { + @panic("Calling drainMicrotasks from inside the event loop tick queue is a bug in your code. Please fix your bug."); + } + } this.eventLoop().drainMicrotasks(); } diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index a7a4a272a5..dc208cf92d 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -341,7 +341,7 @@ pub const S3BlobStatTask = struct { this.promise.resolve(globalThis, .true); }, .failure => |err| { - this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); }, } } @@ -355,7 +355,7 @@ pub const S3BlobStatTask = struct { this.promise.resolve(globalThis, JSValue.jsNumber(stat.size)); }, inline .not_found, .failure => |err| { - this.promise.rejectOnNextTick(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); }, } } diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 23e9f3b03b..21adaf0176 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -905,7 +905,7 @@ pub const Blob = struct { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(0)), .failure => |err| { - this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, this.store.getPath())); + this.promise.reject(globalObject, err.toJS(globalObject, this.store.getPath())); }, } } @@ -1058,7 +1058,7 @@ pub const Blob = struct { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(this.store.data.bytes.len)), .failure => |err| { - this.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, this.store.getPath())); + this.promise.reject(globalObject, err.toJS(globalObject, this.store.getPath())); }, } } @@ -3494,7 +3494,7 @@ pub const Blob = struct { self.promise.resolve(globalObject, .true); }, inline .not_found, .failure => |err| { - self.promise.rejectOnNextTick(globalObject, err.toJS(globalObject, self.store.getPath())); + self.promise.reject(globalObject, err.toJS(globalObject, self.store.getPath())); }, } } @@ -3845,7 +3845,7 @@ pub const Blob = struct { JSC.AnyPromise.wrap(.{ .normal = this.promise.get() }, this.globalThis, S3BlobDownloadTask.callHandler, .{ this, bytes }); }, inline .not_found, .failure => |err| { - this.promise.rejectOnNextTick(this.globalThis, err.toJS(this.globalThis, this.blob.store.?.getPath())); + this.promise.reject(this.globalThis, err.toJS(this.globalThis, this.blob.store.?.getPath())); }, } } @@ -3985,10 +3985,12 @@ pub const Blob = struct { var args = callframe.arguments_old(2); var this = args.ptr[args.len - 1].asPromisePtr(FileStreamWrapper); defer this.deinit(); - if (this.readable_stream_ref.get()) |stream| { + var strong = this.readable_stream_ref; + defer strong.deinit(); + this.readable_stream_ref = .{}; + if (strong.get()) |stream| { stream.done(globalThis); } - this.readable_stream_ref.deinit(); this.promise.resolve(globalThis, JSC.JSValue.jsNumber(0)); return .undefined; } @@ -3999,11 +4001,14 @@ pub const Blob = struct { defer this.sink.deinit(); const err = args.ptr[0]; - this.promise.rejectOnNextTick(globalThis, err); + var strong = this.readable_stream_ref; + defer strong.deinit(); + this.readable_stream_ref = .{}; - if (this.readable_stream_ref.get()) |stream| { + this.promise.reject(globalThis, err); + + if (strong.get()) |stream| { stream.cancel(globalThis); - this.readable_stream_ref.deinit(); } return .undefined; } diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index b0e7ff6056..8d3dccd503 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -1173,8 +1173,6 @@ pub const Fetch = struct { } if (!assignment_result.isEmptyOrUndefinedOrNull()) { - this.javascript_vm.drainMicrotasks(); - assignment_result.ensureStillAlive(); // it returns a Promise when it goes through ReadableStreamDefaultReader if (assignment_result.asAnyPromise()) |promise| { diff --git a/src/s3.zig b/src/s3.zig index b92fbe1913..ca17d6906e 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -1723,7 +1723,7 @@ pub const AWSCredentials = struct { sink.abort(); return; } - sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject, self.path)); + sink.endPromise.reject(globalObject, err.toJS(globalObject, self.path)); }, } } @@ -1762,7 +1762,7 @@ pub const AWSCredentials = struct { const err = args.ptr[0]; if (this.sink.endPromise.hasValue()) { - this.sink.endPromise.rejectOnNextTick(globalThis, err); + this.sink.endPromise.reject(globalThis, err); } if (this.readable_stream_ref.get()) |stream| { @@ -1888,7 +1888,7 @@ pub const AWSCredentials = struct { if (assignment_result.toError()) |err| { if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.rejectOnNextTick(globalThis, err); + response_stream.sink.endPromise.reject(globalThis, err); } task.fail(.{ @@ -1900,8 +1900,6 @@ pub const AWSCredentials = struct { } if (!assignment_result.isEmptyOrUndefinedOrNull()) { - task.vm.drainMicrotasks(); - assignment_result.ensureStillAlive(); // it returns a Promise when it goes through ReadableStreamDefaultReader if (assignment_result.asAnyPromise()) |promise| { @@ -1935,7 +1933,7 @@ pub const AWSCredentials = struct { }, .rejected => { if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.rejectOnNextTick(globalThis, promise.result(globalThis.vm())); + response_stream.sink.endPromise.reject(globalThis, promise.result(globalThis.vm())); } task.fail(.{ @@ -1947,7 +1945,7 @@ pub const AWSCredentials = struct { } } else { if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.rejectOnNextTick(globalThis, assignment_result); + response_stream.sink.endPromise.reject(globalThis, assignment_result); } task.fail(.{ @@ -1978,7 +1976,7 @@ pub const AWSCredentials = struct { return; } - sink.endPromise.rejectOnNextTick(globalObject, err.toJS(globalObject, sink.path())); + sink.endPromise.reject(globalObject, err.toJS(globalObject, sink.path())); }, } } From e532456cfe8f27a6cc162625df74f65d0b014688 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 05:24:57 -0800 Subject: [PATCH 33/56] Update Bun.S3 type definitions --- packages/bun-types/bun.d.ts | 748 +++++++++++++++++++++++++++++++----- 1 file changed, 642 insertions(+), 106 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d4a8d30c26..edf6a27e5a 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1243,80 +1243,299 @@ declare module "bun" { end(error?: Error): number | Promise; } + type S3 = { + /** + * Create a new instance of an S3 bucket so that credentials can be managed + * from a single instance instead of being passed to every method. + * + * @param options The default options to use for the S3 client. Can be + * overriden by passing options to the methods. + * + * ## Keep S3 credentials in a single instance + * + * @example + * const bucket = new Bun.S3({ + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key", + * bucket: "my-bucket", + * endpoint: "https://s3.us-east-1.amazonaws.com", + * sessionToken: "your-session-token", + * }); + * + * // S3Bucket is callable, so you can do this: + * const file = bucket("my-file.txt"); + * + * // or this: + * await file.write("Hello Bun!"); + * await file.text(); + * + * // To delete the file: + * await bucket.delete("my-file.txt"); + * + * // To write a file without returning the instance: + * await bucket.write("my-file.txt", "Hello Bun!"); + * + */ + new (options?: S3Options): S3Bucket; + + /** + * Delete a file from an S3-compatible object storage service. + * + * @param path The path to the file. + * @param options The options to use for the S3 client. + * + * For an instance method version, {@link S3File.unlink}. You can also use {@link S3Bucket.unlink}. + * + * @example + * import { S3 } from "bun"; + * await S3.unlink("s3://my-bucket/my-file.txt", { + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key", + * }); + * + * @example + * await S3.unlink("key", { + * bucket: "my-bucket", + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key", + * }); + */ + delete(path: string, options?: S3Options): Promise; + /** + * unlink is an alias for {@link S3.delete} + */ + unlink: S3["delete"]; + + /** + * Writes data to an S3-compatible storage service. + * Supports various input types and handles large files with multipart uploads. + * + * @param path The path or key where the file will be written + * @param data The data to write + * @param options S3 configuration and upload options + * @returns promise that resolves with the number of bytes written + * + * @example + * // Writing a string + * await S3.write("hello.txt", "Hello World!", { + * bucket: "my-bucket", + * type: "text/plain" + * }); + * + * @example + * // Writing JSON + * await S3.write( + * "data.json", + * JSON.stringify({ hello: "world" }), + * { type: "application/json" } + * ); + * + * @example + * // Writing a large file with multipart upload + * await S3.write("large-file.dat", largeBuffer, { + * partSize: 10 * 1024 * 1024, // 10MB parts + * queueSize: 4, // Upload 4 parts in parallel + * retry: 3 // Retry failed parts up to 3 times + * }); + */ + write( + path: string, + data: string | ArrayBufferView | ArrayBufferLike | Response | Request | ReadableStream | Blob | File, + options?: S3Options, + ): Promise; + }; + var S3: S3; + + /** + * Creates a new S3File instance for working with a single file. + * + * @param path The path or key of the file + * @param options S3 configuration options + * @returns `S3File` instance for the specified path + * + * @example + * import { s3 } from "bun"; + * const file = s3("my-file.txt", { + * bucket: "my-bucket", + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key" + * }); + * + * // Read the file + * const content = await file.text(); + * + * @example + * // Using s3:// protocol + * const file = s3("s3://my-bucket/my-file.txt", { + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key" + * }); + */ + function s3(path: string | URL, options?: S3Options): S3File; + + /** + * Configuration options for S3 operations + */ interface S3Options extends BlobPropertyBag { /** - * The ACL to used to write the file to S3. by default will omit the ACL header/parameter. + * The Access Control List (ACL) policy for the file. + * Controls who can access the file and what permissions they have. + * + * @example + * // Setting public read access + * const file = s3("public-file.txt", { + * acl: "public-read", + * bucket: "my-bucket" + * }); + * + * @example + * // Using with presigned URLs + * const url = file.presign({ + * acl: "public-read", + * expiresIn: 3600 + * }); */ - acl?: /** - * Owner gets FULL_CONTROL. No one else has access rights (default). - */ - | "private" - /** - * Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. - */ + acl?: + | "private" | "public-read" - /** - * Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. - */ | "public-read-write" - /** - * Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. - */ | "aws-exec-read" - /** - * Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. - */ | "authenticated-read" - /** - * Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. - */ | "bucket-owner-read" - /** - * Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. - */ | "bucket-owner-full-control" | "log-delivery-write"; + /** - * The bucket to use for the S3 client. by default will use the `S3_BUCKET` and `AWS_BUCKET` environment variable, or deduce as first part of the path. + * The S3 bucket name. Can be set via `S3_BUCKET` or `AWS_BUCKET` environment variables. + * + * @example + * // Using explicit bucket + * const file = s3("my-file.txt", { bucket: "my-bucket" }); + * + * @example + * // Using environment variables + * // With S3_BUCKET=my-bucket in .env + * const file = s3("my-file.txt"); */ bucket?: string; + /** - * The region to use for the S3 client. By default, it will use the `S3_REGION` and `AWS_REGION` environment variable. + * The AWS region. Can be set via `S3_REGION` or `AWS_REGION` environment variables. + * + * @example + * const file = s3("my-file.txt", { + * bucket: "my-bucket", + * region: "us-west-2" + * }); */ region?: string; + /** - * The access key ID to use for the S3 client. By default, it will use the `S3_ACCESS_KEY_ID` and `AWS_ACCESS_KEY_ID` environment variable. + * The access key ID for authentication. + * Can be set via `S3_ACCESS_KEY_ID` or `AWS_ACCESS_KEY_ID` environment variables. */ accessKeyId?: string; + /** - * The secret access key to use for the S3 client. By default, it will use the `S3_SECRET_ACCESS_KEY and `AWS_SECRET_ACCESS_KEY` environment variable. + * The secret access key for authentication. + * Can be set via `S3_SECRET_ACCESS_KEY` or `AWS_SECRET_ACCESS_KEY` environment variables. */ secretAccessKey?: string; + /** - * The session token to use for the S3 client. By default, it will use the `S3_SESSION_TOKEN` and `AWS_SESSION_TOKEN` environment variable. + * Optional session token for temporary credentials. + * Can be set via `S3_SESSION_TOKEN` or `AWS_SESSION_TOKEN` environment variables. + * + * @example + * // Using temporary credentials + * const file = s3("my-file.txt", { + * accessKeyId: tempAccessKey, + * secretAccessKey: tempSecretKey, + * sessionToken: tempSessionToken + * }); */ sessionToken?: string; /** - * The endpoint to use for the S3 client. Defaults to `https://s3.{region}.amazonaws.com`, it will also use the `S3_ENDPOINT` and `AWS_ENDPOINT` environment variable. + * The S3-compatible service endpoint URL. + * Can be set via `S3_ENDPOINT` or `AWS_ENDPOINT` environment variables. + * + * @example + * // AWS S3 + * const file = s3("my-file.txt", { + * endpoint: "https://s3.us-east-1.amazonaws.com" + * }); + * + * @example + * // Cloudflare R2 + * const file = s3("my-file.txt", { + * endpoint: "https://.r2.cloudflarestorage.com" + * }); + * + * @example + * // DigitalOcean Spaces + * const file = s3("my-file.txt", { + * endpoint: "https://.digitaloceanspaces.com" + * }); + * + * @example + * // MinIO (local development) + * const file = s3("my-file.txt", { + * endpoint: "http://localhost:9000" + * }); */ endpoint?: string; /** - * The size of each part in MiB. Minimum and Default is 5 MiB and maximum is 5120 MiB. + * The size of each part in multipart uploads (in MiB). + * - Minimum: 5 MiB + * - Maximum: 5120 MiB + * - Default: 5 MiB + * + * @example + * // Configuring multipart uploads + * const file = s3("large-file.dat", { + * partSize: 10, // 10 MiB parts + * queueSize: 4 // Upload 4 parts in parallel + * }); + * + * const writer = file.writer(); + * // ... write large file in chunks */ partSize?: number; + /** - * The number of parts to upload in parallel. Default is 5 and maximum is 255. This can speed up the upload of large files but will also use more memory. + * Number of parts to upload in parallel for multipart uploads. + * - Default: 5 + * - Maximum: 255 + * + * Increasing this value can improve upload speeds for large files + * but will use more memory. */ queueSize?: number; + /** - * The number of times to retry the upload if it fails. Default is 3 and maximum is 255. + * Number of retry attempts for failed uploads. + * - Default: 3 + * - Maximum: 255 + * + * @example + * // Setting retry attempts + * const file = s3("my-file.txt", { + * retry: 5 // Retry failed uploads up to 5 times + * }); */ retry?: number; /** - * The Content-Type of the file. If not provided, it is automatically set based on the file extension when possible. + * The Content-Type of the file. + * Automatically set based on file extension when possible. + * + * @example + * // Setting explicit content type + * const file = s3("data.bin", { + * type: "application/octet-stream" + * }); */ type?: string; @@ -1326,93 +1545,253 @@ declare module "bun" { highWaterMark?: number; } + /** + * Options for generating presigned URLs + */ interface S3FilePresignOptions extends S3Options { /** - * The number of seconds the presigned URL will be valid for. Defaults to 86400 (1 day). + * Number of seconds until the presigned URL expires. + * - Default: 86400 (1 day) + * + * @example + * // Short-lived URL + * const url = file.presign({ + * expiresIn: 3600 // 1 hour + * }); + * + * @example + * // Long-lived public URL + * const url = file.presign({ + * expiresIn: 7 * 24 * 60 * 60, // 7 days + * acl: "public-read" + * }); */ expiresIn?: number; + /** - * The HTTP method to use for the presigned URL. Defaults to GET. + * The HTTP method allowed for the presigned URL. + * + * @example + * // GET URL for downloads + * const downloadUrl = file.presign({ + * method: "GET", + * expiresIn: 3600 + * }); + * + * @example + * // PUT URL for uploads + * const uploadUrl = file.presign({ + * method: "PUT", + * expiresIn: 3600, + * type: "application/json" + * }); */ - method?: string; + method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD"; } - interface S3File extends BunFile { - /** - * @param path - The path to the file. If bucket options is not provided or set in the path, it will be deduced from the path. - * @param options - The options to use for the S3 client. - */ - new (path: string | URL, options?: S3Options): S3File; + /** + * Represents a file in an S3-compatible storage service. + * Extends the Blob interface for compatibility with web APIs. + */ + interface S3File extends Blob { /** * The size of the file in bytes. + * This is a Promise because it requires a network request to determine the size. + * + * @example + * // Getting file size + * const size = await file.size; + * console.log(`File size: ${size} bytes`); + * + * @example + * // Check if file is larger than 1MB + * if (await file.size > 1024 * 1024) { + * console.log("Large file detected"); + * } */ - size: Promise; /** - * Offset any operation on the file starting at `begin` and ending at `end`. `end` is relative to 0 + * TODO: figure out how to get the typescript types to not error for this property. + */ + // size: Promise; + + /** + * Creates a new S3File representing a slice of the original file. + * Uses HTTP Range headers for efficient partial downloads. * - * Similar to [`TypedArray.subarray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray). Does not copy the file, open the file, or modify the file. + * @param begin - Starting byte offset + * @param end - Ending byte offset (exclusive) + * @param contentType - Optional MIME type for the slice + * @returns A new S3File representing the specified range * - * It will use [`range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) to download only the bytes you need. + * @example + * // Reading file header + * const header = file.slice(0, 1024); + * const headerText = await header.text(); * - * @param begin - start offset in bytes - * @param end - absolute offset in bytes (relative to 0) - * @param contentType - MIME type for the new S3File + * @example + * // Reading with content type + * const jsonSlice = file.slice(1024, 2048, "application/json"); + * const data = await jsonSlice.json(); + * + * @example + * // Reading from offset to end + * const remainder = file.slice(1024); + * const content = await remainder.text(); */ slice(begin?: number, end?: number, contentType?: string): S3File; - - /** */ - /** - * Offset any operation on the file starting at `begin` - * - * Similar to [`TypedArray.subarray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray). Does not copy the file, open the file, or modify the file. - * - * It will use [`range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) to download only the bytes you need. - * - * @param begin - start offset in bytes - * @param contentType - MIME type for the new S3File - */ slice(begin?: number, contentType?: string): S3File; - - /** - * @param contentType - MIME type for the new S3File - */ slice(contentType?: string): S3File; /** - * Incremental writer to stream writes to the network, this is equivalent of using MultipartUpload and is suitable for large files. + * Creates a writable stream for uploading data. + * Suitable for large files as it uses multipart upload. + * + * @param options - Configuration for the upload + * @returns A NetworkSink for writing data + * + * @example + * // Basic streaming write + * const writer = file.writer({ + * type: "application/json" + * }); + * writer.write('{"hello": '); + * writer.write('"world"}'); + * await writer.end(); + * + * @example + * // Optimized large file upload + * const writer = file.writer({ + * partSize: 10 * 1024 * 1024, // 10MB parts + * queueSize: 4, // Upload 4 parts in parallel + * retry: 3 // Retry failed parts + * }); + * + * // Write large chunks of data efficiently + * for (const chunk of largeDataChunks) { + * await writer.write(chunk); + * } + * await writer.end(); + * + * @example + * // Error handling + * const writer = file.writer(); + * try { + * await writer.write(data); + * await writer.end(); + * } catch (err) { + * console.error('Upload failed:', err); + * // Writer will automatically abort multipart upload on error + * } */ writer(options?: S3Options): NetworkSink; /** - * The readable stream of the file. + * Gets a readable stream of the file's content. + * Useful for processing large files without loading them entirely into memory. + * + * @returns A ReadableStream for the file content + * + * @example + * // Basic streaming read + * const stream = file.stream(); + * for await (const chunk of stream) { + * console.log('Received chunk:', chunk); + * } + * + * @example + * // Piping to response + * const stream = file.stream(); + * return new Response(stream, { + * headers: { 'Content-Type': file.type } + * }); + * + * @example + * // Processing large files + * const stream = file.stream(); + * const textDecoder = new TextDecoder(); + * for await (const chunk of stream) { + * const text = textDecoder.decode(chunk); + * // Process text chunk by chunk + * } */ readonly readable: ReadableStream; - - /** - * Get a readable stream of the file. - */ stream(): ReadableStream; /** - * The name or path of the file, as specified in the constructor. + * The name or path of the file in the bucket. + * + * @example + * const file = s3("folder/image.jpg"); + * console.log(file.name); // "folder/image.jpg" */ readonly name?: string; /** - * The bucket name of the file. + * The bucket name containing the file. + * + * @example + * const file = s3("s3://my-bucket/file.txt"); + * console.log(file.bucket); // "my-bucket" */ readonly bucket?: string; /** - * Does the file exist? - * It will use [`head`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) to check if the file exists. + * Checks if the file exists in S3. + * Uses HTTP HEAD request to efficiently check existence without downloading. + * + * @returns Promise resolving to true if file exists, false otherwise + * + * @example + * // Basic existence check + * if (await file.exists()) { + * console.log("File exists in S3"); + * } + * + * @example + * // With error handling + * try { + * const exists = await file.exists(); + * if (!exists) { + * console.log("File not found"); + * } + * } catch (err) { + * console.error("Error checking file:", err); + * } */ exists(): Promise; /** - * Uploads the data to S3. This is equivalent of using {@link S3File.upload} with a {@link S3File}. - * @param data - The data to write. - * @param options - The options to use for the S3 client. + * Uploads data to S3. + * Supports various input types and automatically handles large files. + * + * @param data - The data to upload + * @param options - Upload configuration options + * @returns Promise resolving to number of bytes written + * + * @example + * // Writing string data + * await file.write("Hello World", { + * type: "text/plain" + * }); + * + * @example + * // Writing JSON + * const data = { hello: "world" }; + * await file.write(JSON.stringify(data), { + * type: "application/json" + * }); + * + * @example + * // Writing from Response + * const response = await fetch("https://example.com/data"); + * await file.write(response); + * + * @example + * // Writing with ACL + * await file.write(data, { + * acl: "public-read", + * type: "application/octet-stream" + * }); */ write( data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob, @@ -1420,55 +1799,224 @@ declare module "bun" { ): Promise; /** - * Returns a presigned URL for the file. - * @param options - The options to use for the presigned URL. + * Generates a presigned URL for the file. + * Allows temporary access to the file without exposing credentials. + * + * @param options - Configuration for the presigned URL + * @returns Presigned URL string + * + * @example + * // Basic download URL + * const url = file.presign({ + * expiresIn: 3600 // 1 hour + * }); + * + * @example + * // Upload URL with specific content type + * const uploadUrl = file.presign({ + * method: "PUT", + * expiresIn: 3600, + * type: "image/jpeg", + * acl: "public-read" + * }); + * + * @example + * // URL with custom permissions + * const url = file.presign({ + * method: "GET", + * expiresIn: 7 * 24 * 60 * 60, // 7 days + * acl: "public-read" + * }); */ presign(options?: S3FilePresignOptions): string; /** * Deletes the file from S3. + * + * @returns Promise that resolves when deletion is complete + * + * @example + * // Basic deletion + * await file.delete(); + * + * @example + * // With error handling + * try { + * await file.delete(); + * console.log("File deleted successfully"); + * } catch (err) { + * console.error("Failed to delete file:", err); + * } */ - unlink(): Promise; + delete(): Promise; + + /** + * Alias for delete() method. + * Provided for compatibility with Node.js fs API naming. + * + * @example + * await file.unlink(); + */ + unlink: S3File["delete"]; } - interface S3Bucket { + /** + * A configured S3 bucket instance for managing files. + * The instance is callable to create S3File instances and provides methods + * for common operations. + * + * @example + * // Basic bucket setup + * const bucket = new S3({ + * bucket: "my-bucket", + * accessKeyId: "key", + * secretAccessKey: "secret" + * }); + * + * // Get file instance + * const file = bucket("image.jpg"); + * + * // Common operations + * await bucket.write("data.json", JSON.stringify({hello: "world"})); + * const url = bucket.presign("file.pdf"); + * await bucket.unlink("old.txt"); + */ + type S3Bucket = { /** - * Get a file from the bucket. - * @param path - The path to the file. + * Creates an S3File instance for the given path. + * + * @example + * const file = bucket("image.jpg"); + * await file.write(imageData); + * const configFile = bucket("config.json", { + * type: "application/json", + * acl: "private" + * }); */ (path: string, options?: S3Options): S3File; + /** - * Uploads the data to S3. This will overwrite the file if it already exists. - * @param data - The data to write. - * @param options - The options to use for the S3 client. + * Writes data directly to a path in the bucket. + * Supports strings, buffers, streams, and web API types. + * + * @example + * // Write string + * await bucket.write("hello.txt", "Hello World"); + * + * // Write JSON with type + * await bucket.write( + * "data.json", + * JSON.stringify({hello: "world"}), + * {type: "application/json"} + * ); + * + * // Write from fetch + * const res = await fetch("https://example.com/data"); + * await bucket.write("data.bin", res); + * + * // Write with ACL + * await bucket.write("public.html", html, { + * acl: "public-read", + * type: "text/html" + * }); */ write( path: string, - data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File, + data: + | string + | ArrayBufferView + | ArrayBuffer + | SharedArrayBuffer + | Request + | Response + | BunFile + | S3File + | Blob + | File, options?: S3Options, ): Promise; /** - * Returns a presigned URL for the file. - * @param options - The options to use for the presigned URL. + * Generate a presigned URL for temporary access to a file. + * Useful for generating upload/download URLs without exposing credentials. + * + * @example + * // Download URL + * const downloadUrl = bucket.presign("file.pdf", { + * expiresIn: 3600 // 1 hour + * }); + * + * // Upload URL + * const uploadUrl = bucket.presign("uploads/image.jpg", { + * method: "PUT", + * expiresIn: 3600, + * type: "image/jpeg", + * acl: "public-read" + * }); + * + * // Long-lived public URL + * const publicUrl = bucket.presign("public/doc.pdf", { + * expiresIn: 7 * 24 * 60 * 60, // 7 days + * acl: "public-read" + * }); */ presign(path: string, options?: S3FilePresignOptions): string; /** - * Deletes the file from S3. + * Delete a file from the bucket. + * + * @example + * // Simple delete + * await bucket.unlink("old-file.txt"); + * + * // With error handling + * try { + * await bucket.unlink("file.dat"); + * console.log("File deleted"); + * } catch (err) { + * console.error("Delete failed:", err); + * } */ unlink(path: string, options?: S3Options): Promise; /** - * The size of the file in bytes. + * Get the size of a file in bytes. + * Uses HEAD request to efficiently get size. + * + * @example + * // Get size + * const bytes = await bucket.size("video.mp4"); + * console.log(`Size: ${bytes} bytes`); + * + * // Check if file is large + * if (await bucket.size("data.zip") > 100 * 1024 * 1024) { + * console.log("File is larger than 100MB"); + * } */ size(path: string, options?: S3Options): Promise; /** - * Does the file exist? + * Check if a file exists in the bucket. + * Uses HEAD request to check existence. + * + * @example + * // Check existence + * if (await bucket.exists("config.json")) { + * const file = bucket("config.json"); + * const config = await file.json(); + * } + * + * // With error handling + * try { + * if (!await bucket.exists("required.txt")) { + * throw new Error("Required file missing"); + * } + * } catch (err) { + * console.error("Check failed:", err); + * } */ exists(path: string, options?: S3Options): Promise; - } + }; /** * This lets you use macros as regular imports @@ -3320,18 +3868,6 @@ declare module "bun" { // tslint:disable-next-line:unified-signatures function file(fileDescriptor: number, options?: BlobPropertyBag): BunFile; - /** - * Lazily load/upload a file from S3. - * @param path - The path to the file. If bucket options is not provided or set in the path, it will be deduced from the path. - * @param options - The options to use for the S3 client. - */ - function s3(path: string | URL, options?: S3Options): S3File; - /** - * Create a configured S3 bucket reference. - * @param options - The options to use for the S3 client. - */ - function S3(options?: S3Options): S3Bucket; - /** * Allocate a new [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) without zeroing the bytes. * From 8a469cce7eabe17fab58a1a84013945053a3310e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Jan 2025 06:21:32 -0800 Subject: [PATCH 34/56] Default to "auto" instead of "us-east-1" --- src/s3.zig | 3 ++- test/js/bun/s3/s3.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/s3.zig b/src/s3.zig index ca17d6906e..e54a0aae82 100644 --- a/src/s3.zig +++ b/src/s3.zig @@ -446,7 +446,8 @@ pub const AWSCredentials = struct { } } } - return "us-east-1"; + + return "auto"; } fn toHexChar(value: u8) !u8 { return switch (value) { diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index 88a17f85a0..3fb3670cb1 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -659,7 +659,7 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { await s3file.write("Hello Bun!"); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBe("InvalidAccessKeyId"); + expect(e?.code).toBeOneOf(["FailedToOpenSocket", "ConnectionRefused"]); } }), ); @@ -676,7 +676,7 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { await s3file.write("Hello Bun!"); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBe("InvalidAccessKeyId"); + expect(e?.code).toBeOneOf(["FailedToOpenSocket", "ConnectionRefused"]); } }), ); From 034f7760477f7d06f88c80155c31e9486fb9158d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 4 Jan 2025 19:57:35 -0800 Subject: [PATCH 35/56] WIP: S3 improvements (#16167) --- src/bun.js/api/server.zig | 6 +- src/bun.js/bindings/JSS3Bucket.cpp | 1 + src/bun.js/event_loop.zig | 6 +- src/bun.js/webcore/S3Bucket.zig | 24 +- src/bun.js/webcore/S3File.zig | 33 +- src/bun.js/webcore/blob.zig | 169 +- src/bun.js/webcore/response.classes.ts | 1 + src/bun.js/webcore/response.zig | 19 +- src/bun.js/webcore/streams.zig | 5 +- src/bun.zig | 3 +- src/env_loader.zig | 6 +- src/s3.zig | 2568 ------------------------ src/s3/acl.zig | 43 + src/s3/client.zig | 629 ++++++ src/s3/credentials.zig | 775 +++++++ src/s3/download_stream.zig | 242 +++ src/s3/error.zig | 86 + src/s3/multipart.zig | 538 +++++ src/s3/multipart_options.zig | 22 + src/s3/simple_request.zig | 404 ++++ test/js/bun/s3/s3.test.ts | 67 +- 21 files changed, 2974 insertions(+), 2673 deletions(-) delete mode 100644 src/s3.zig create mode 100644 src/s3/acl.zig create mode 100644 src/s3/client.zig create mode 100644 src/s3/credentials.zig create mode 100644 src/s3/download_stream.zig create mode 100644 src/s3/error.zig create mode 100644 src/s3/multipart.zig create mode 100644 src/s3/multipart_options.zig create mode 100644 src/s3/simple_request.zig diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index f162020626..46e96b7f71 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -90,7 +90,7 @@ const linux = std.os.linux; const Async = bun.Async; const httplog = Output.scoped(.Server, false); const ctxLog = Output.scoped(.RequestContext, false); -const AWS = @import("../../s3.zig").AWSCredentials; +const S3 = bun.S3; const BlobFileContentResult = struct { data: [:0]const u8, @@ -3182,7 +3182,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.endWithoutBody(this.shouldCloseConnection()); this.deref(); } - pub fn onS3SizeResolved(result: AWS.S3StatResult, this: *RequestContext) void { + pub fn onS3SizeResolved(result: S3.S3StatResult, this: *RequestContext) void { defer { this.deref(); } @@ -3254,7 +3254,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp const path = blob.store.?.data.s3.path(); const env = globalThis.bunVM().transpiler.env; - credentials.s3Stat(path, @ptrCast(&onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.stat(credentials, path, @ptrCast(&onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); return; } diff --git a/src/bun.js/bindings/JSS3Bucket.cpp b/src/bun.js/bindings/JSS3Bucket.cpp index f9880a3415..b85a41029a 100644 --- a/src/bun.js/bindings/JSS3Bucket.cpp +++ b/src/bun.js/bindings/JSS3Bucket.cpp @@ -37,6 +37,7 @@ JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_size); static const HashTableValue JSS3BucketPrototypeTableValues[] = { { "unlink"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, + { "delete"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_write, 1 } }, { "presign"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_presign, 1 } }, { "exists"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_exists, 1 } }, diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 21e6393b89..d31753a19e 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -18,9 +18,9 @@ const ReadFileTask = WebCore.Blob.ReadFile.ReadFileTask; const WriteFileTask = WebCore.Blob.WriteFile.WriteFileTask; const napi_async_work = JSC.napi.napi_async_work; const FetchTasklet = Fetch.FetchTasklet; -const AWS = @import("../s3.zig").AWSCredentials; -const S3HttpSimpleTask = AWS.S3HttpSimpleTask; -const S3HttpDownloadStreamingTask = AWS.S3HttpDownloadStreamingTask; +const S3 = bun.S3; +const S3HttpSimpleTask = S3.S3HttpSimpleTask; +const S3HttpDownloadStreamingTask = S3.S3HttpDownloadStreamingTask; const JSValue = JSC.JSValue; const js = JSC.C; diff --git a/src/bun.js/webcore/S3Bucket.zig b/src/bun.js/webcore/S3Bucket.zig index 7c8eed5392..4d3b95ff16 100644 --- a/src/bun.js/webcore/S3Bucket.zig +++ b/src/bun.js/webcore/S3Bucket.zig @@ -6,11 +6,11 @@ const PathOrBlob = JSC.Node.PathOrBlob; const ZigString = JSC.ZigString; const Method = bun.http.Method; const S3File = @import("./S3File.zig"); -const AWSCredentials = bun.AWSCredentials; +const S3Credentials = bun.S3.S3Credentials; const S3BucketOptions = struct { - credentials: *AWSCredentials, - options: bun.S3.MultiPartUpload.MultiPartUploadOptions = .{}, + credentials: *S3Credentials, + options: bun.S3.MultiPartUploadOptions = .{}, acl: ?bun.S3.ACL = null, pub usingnamespace bun.New(@This()); @@ -20,7 +20,7 @@ const S3BucketOptions = struct { } }; -pub fn writeFormatCredentials(credentials: *AWSCredentials, options: bun.S3.MultiPartUpload.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { +pub fn writeFormatCredentials(credentials: *S3Credentials, options: bun.S3.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { try writer.writeAll("\n"); { @@ -37,7 +37,7 @@ pub fn writeFormatCredentials(credentials: *AWSCredentials, options: bun.S3.Mult formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); try writer.writeAll("\n"); - const region = if (credentials.region.len > 0) credentials.region else AWSCredentials.guessRegion(credentials.endpoint); + const region = if (credentials.region.len > 0) credentials.region else S3Credentials.guessRegion(credentials.endpoint); try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime bun.Output.prettyFmt("region: \"", enable_ansi_colors)); try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{region}); @@ -133,7 +133,7 @@ pub fn call(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: * }; errdefer path.deinit(); const options = args.nextEat(); - var blob = Blob.new(try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); + var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -151,7 +151,7 @@ pub fn presign(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); defer blob.detach(); return S3File.getPresignUrlFrom(&blob, globalThis, options); } @@ -168,7 +168,7 @@ pub fn exists(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); defer blob.detach(); return S3File.S3BlobStatTask.exists(globalThis, &blob); } @@ -185,7 +185,7 @@ pub fn size(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: * }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); defer blob.detach(); return S3File.S3BlobStatTask.size(globalThis, &blob); } @@ -203,7 +203,7 @@ pub fn write(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: }; const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); defer blob.detach(); var blob_internal: PathOrBlob = .{ .blob = blob }; return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ @@ -221,7 +221,7 @@ pub fn unlink(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithAWSCredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); defer blob.detach(); return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); } @@ -235,7 +235,7 @@ pub fn construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) cal if (options.isEmptyOrUndefinedOrNull() or !options.isObject()) { globalThis.throwInvalidArguments("Expected S3 options to be passed", .{}) catch return null; } - var aws_options = AWSCredentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getAWSCredentials(), .{}, options, null, globalThis) catch return null; + var aws_options = S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, options, null, globalThis) catch return null; defer aws_options.deinit(); return S3BucketOptions.new(.{ .credentials = aws_options.credentials.dupe(), diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index dc208cf92d..f73f99b890 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -8,6 +8,7 @@ const Method = bun.http.Method; const strings = bun.strings; const Output = bun.Output; const S3Bucket = @import("./S3Bucket.zig"); +const S3 = bun.S3; pub fn writeFormat(s3: *Blob.S3Store, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { try writer.writeAll(comptime Output.prettyFmt("S3Ref", enable_ansi_colors)); @@ -214,19 +215,19 @@ fn constructS3FileInternalStore( options: ?JSC.JSValue, ) bun.JSError!Blob { // get credentials from env - const existing_credentials = globalObject.bunVM().transpiler.env.getAWSCredentials(); - return constructS3FileWithAWSCredentials(globalObject, path, options, existing_credentials); + const existing_credentials = globalObject.bunVM().transpiler.env.getS3Credentials(); + return constructS3FileWithS3Credentials(globalObject, path, options, existing_credentials); } /// if the credentials have changed, we need to clone it, if not we can just ref/deref it -pub fn constructS3FileWithAWSCredentialsAndOptions( +pub fn constructS3FileWithS3CredentialsAndOptions( globalObject: *JSC.JSGlobalObject, path: JSC.Node.PathLike, options: ?JSC.JSValue, - default_credentials: *AWS, - default_options: bun.S3.MultiPartUpload.MultiPartUploadOptions, + default_credentials: *S3.S3Credentials, + default_options: bun.S3.MultiPartUploadOptions, default_acl: ?bun.S3.ACL, ) bun.JSError!Blob { - var aws_options = try AWS.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, globalObject); + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, globalObject); defer aws_options.deinit(); const store = brk: { @@ -268,13 +269,13 @@ pub fn constructS3FileWithAWSCredentialsAndOptions( return blob; } -pub fn constructS3FileWithAWSCredentials( +pub fn constructS3FileWithS3Credentials( globalObject: *JSC.JSGlobalObject, path: JSC.Node.PathLike, options: ?JSC.JSValue, - existing_credentials: AWS, + existing_credentials: S3.S3Credentials, ) bun.JSError!Blob { - var aws_options = try AWS.getCredentialsWithOptions(existing_credentials, .{}, options, null, globalObject); + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(existing_credentials, .{}, options, null, globalObject); defer aws_options.deinit(); const store = Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator) catch bun.outOfMemory(); errdefer store.deinit(); @@ -318,14 +319,12 @@ fn constructS3FileInternal( return ptr; } -const AWS = bun.S3.AWSCredentials; - pub const S3BlobStatTask = struct { promise: JSC.JSPromise.Strong, store: *Blob.Store, usingnamespace bun.New(S3BlobStatTask); - pub fn onS3ExistsResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { + pub fn onS3ExistsResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { defer this.deinit(); const globalThis = this.promise.globalObject().?; switch (result) { @@ -346,7 +345,7 @@ pub const S3BlobStatTask = struct { } } - pub fn onS3SizeResolved(result: AWS.S3StatResult, this: *S3BlobStatTask) void { + pub fn onS3SizeResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { defer this.deinit(); const globalThis = this.promise.globalObject().?; @@ -371,7 +370,7 @@ pub const S3BlobStatTask = struct { const path = blob.store.?.data.s3.path(); const env = globalThis.bunVM().transpiler.env; - credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); return promise; } @@ -386,7 +385,7 @@ pub const S3BlobStatTask = struct { const path = blob.store.?.data.s3.path(); const env = globalThis.bunVM().transpiler.env; - credentials.s3Stat(path, @ptrCast(&S3BlobStatTask.onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3SizeResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); return promise; } @@ -405,7 +404,7 @@ pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_opt var method: bun.http.Method = .GET; var expires: usize = 86400; // 1 day default - var credentialsWithOptions: AWS.AWSCredentialsWithOptions = .{ + var credentialsWithOptions: S3.S3CredentialsWithOptions = .{ .credentials = this.store.?.data.s3.getCredentials().*, }; defer { @@ -434,7 +433,7 @@ pub fn getPresignUrlFrom(this: *Blob, globalThis: *JSC.JSGlobalObject, extra_opt .method = method, .acl = credentialsWithOptions.acl, }, .{ .expires = expires }) catch |sign_err| { - return AWS.throwSignError(sign_err, globalThis); + return S3.throwSignError(sign_err, globalThis); }; defer result.deinit(); var str = bun.String.fromUTF8(result.url); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 21adaf0176..37b1b04942 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -43,10 +43,8 @@ const Request = JSC.WebCore.Request; const libuv = bun.windows.libuv; -const S3 = @import("../../s3.zig"); -const AWSCredentials = S3.AWSCredentials; -const S3MultiPartUpload = S3.MultiPartUpload; -const AWS = AWSCredentials; +const S3 = bun.S3; +const S3Credentials = S3.S3Credentials; const PathOrBlob = JSC.Node.PathOrBlob; const WriteFilePromise = @import("./blob/WriteFile.zig").WriteFilePromise; const WriteFileWaitFromLockedValueTask = @import("./blob/WriteFile.zig").WriteFileWaitFromLockedValueTask; @@ -275,7 +273,7 @@ pub const Blob = struct { switch (store.data) { .s3 => |_| { // TODO: s3 - // we need to make this async and use s3Download/s3DownloadSlice + // we need to make this async and use download/downloadSlice }, .file => |file| { @@ -900,7 +898,7 @@ pub const Blob = struct { store: *Store, pub usingnamespace bun.New(@This()); - pub fn resolve(result: AWS.S3UploadResult, this: *@This()) void { + pub fn resolve(result: S3.S3UploadResult, this: *@This()) void { if (this.promise.globalObject()) |globalObject| { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(0)), @@ -924,10 +922,19 @@ pub const Blob = struct { const proxy = ctx.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; destination_blob.store.?.ref(); - aws_options.credentials.s3Upload(s3.path(), "", destination_blob.contentTypeOrMimeType(), aws_options.acl, proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ - .promise = promise, - .store = destination_blob.store.?, - })); + S3.upload( + &aws_options.credentials, + s3.path(), + "", + destination_blob.contentTypeOrMimeType(), + aws_options.acl, + proxy_url, + @ptrCast(&Wrapper.resolve), + Wrapper.new(.{ + .promise = promise, + .store = destination_blob.store.?, + }), + ); return promise_value; } @@ -1003,7 +1010,7 @@ pub const Blob = struct { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3MultiPartUpload.OneMiB), + @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), ), ctx)) |stream| { return destination_blob.pipeReadableStreamToBlob(ctx, stream, options.extra_options); } else { @@ -1037,13 +1044,24 @@ pub const Blob = struct { const proxy_url = if (proxy) |p| p.href else null; switch (store.data) { .bytes => |bytes| { - if (bytes.len > S3MultiPartUpload.MAX_SINGLE_UPLOAD_SIZE) { + if (bytes.len > S3.MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE) { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3MultiPartUpload.OneMiB), + @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), ), ctx)) |stream| { - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + stream, + ctx, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); } else { return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); } @@ -1053,7 +1071,7 @@ pub const Blob = struct { promise: JSC.JSPromise.Strong, pub usingnamespace bun.New(@This()); - pub fn resolve(result: AWS.S3UploadResult, this: *@This()) void { + pub fn resolve(result: S3.S3UploadResult, this: *@This()) void { if (this.promise.globalObject()) |globalObject| { switch (result) { .success => this.promise.resolve(globalObject, JSC.jsNumber(this.store.data.bytes.len)), @@ -1074,10 +1092,19 @@ pub const Blob = struct { const promise = JSC.JSPromise.Strong.init(ctx); const promise_value = promise.value(); - aws_options.credentials.s3Upload(s3.path(), bytes.slice(), destination_blob.contentTypeOrMimeType(), aws_options.acl, proxy_url, @ptrCast(&Wrapper.resolve), Wrapper.new(.{ - .store = store, - .promise = promise, - })); + S3.upload( + &aws_options.credentials, + s3.path(), + bytes.slice(), + destination_blob.contentTypeOrMimeType(), + aws_options.acl, + proxy_url, + @ptrCast(&Wrapper.resolve), + Wrapper.new(.{ + .store = store, + .promise = promise, + }), + ); return promise_value; } }, @@ -1086,9 +1113,20 @@ pub const Blob = struct { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3MultiPartUpload.OneMiB), + @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), ), ctx)) |stream| { - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), stream, ctx, s3.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + stream, + ctx, + s3.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); } else { return JSC.JSPromise.rejectedPromiseValue(ctx, ctx.createErrorInstance("Failed to stream bytes to s3 bucket", .{})); } @@ -1266,7 +1304,18 @@ pub const Blob = struct { const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + readable, + globalThis, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); } destination_blob.detach(); return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); @@ -1314,7 +1363,18 @@ pub const Blob = struct { } const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(s3.path(), readable, globalThis, aws_options.options, aws_options.acl, destination_blob.contentTypeOrMimeType(), proxy_url, null, undefined); + return S3.uploadStream( + (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + s3.path(), + readable, + globalThis, + aws_options.options, + aws_options.acl, + destination_blob.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); } destination_blob.detach(); return globalThis.throwInvalidArguments("ReadableStream has already been used", .{}); @@ -1768,7 +1828,7 @@ pub const Blob = struct { if (check_s3) { if (path_or_fd.* == .path) { if (strings.startsWith(path_or_fd.path.slice(), "s3://")) { - const credentials = globalThis.bunVM().transpiler.env.getAWSCredentials(); + const credentials = globalThis.bunVM().transpiler.env.getS3Credentials(); const copy = path_or_fd.*; path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; return Blob.initWithStore(Blob.Store.initS3(copy.path, null, credentials, allocator) catch bun.outOfMemory(), globalThis); @@ -1898,7 +1958,7 @@ pub const Blob = struct { var this = bun.cast(*Store, ptr); this.deref(); } - pub fn initS3WithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS, allocator: std.mem.Allocator) !*Store { + pub fn initS3WithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; // this actually protects/refs the pathlike path.toThreadSafe(); @@ -1926,7 +1986,7 @@ pub const Blob = struct { }); return store; } - pub fn initS3(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials, allocator: std.mem.Allocator) !*Store { + pub fn initS3(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; // this actually protects/refs the pathlike path.toThreadSafe(); @@ -3447,20 +3507,20 @@ pub const Blob = struct { pub const S3Store = struct { pathlike: JSC.Node.PathLike, mime_type: http.MimeType = http.MimeType.other, - credentials: ?*AWSCredentials, - options: S3MultiPartUpload.MultiPartUploadOptions = .{}, + credentials: ?*S3Credentials, + options: bun.S3.MultiPartUploadOptions = .{}, acl: ?S3.ACL = null, pub fn isSeekable(_: *const @This()) ?bool { return true; } - pub fn getCredentials(this: *const @This()) *AWSCredentials { + pub fn getCredentials(this: *const @This()) *S3Credentials { bun.assert(this.credentials != null); return this.credentials.?; } - pub fn getCredentialsWithOptions(this: *const @This(), options: ?JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!AWS.AWSCredentialsWithOptions { - return AWS.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, globalObject); + pub fn getCredentialsWithOptions(this: *const @This(), options: ?JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!S3.S3CredentialsWithOptions { + return S3Credentials.getCredentialsWithOptions(this.getCredentials().*, this.options, options, this.acl, globalObject); } pub fn path(this: *@This()) []const u8 { @@ -3486,7 +3546,7 @@ pub const Blob = struct { pub usingnamespace bun.New(@This()); - pub fn resolve(result: AWS.S3DeleteResult, self: *@This()) void { + pub fn resolve(result: S3.S3DeleteResult, self: *@This()) void { defer self.deinit(); const globalObject = self.promise.globalObject().?; switch (result) { @@ -3511,7 +3571,7 @@ pub const Blob = struct { const proxy = if (proxy_url) |url| url.href else null; var aws_options = try this.getCredentialsWithOptions(extra_options, globalThis); defer aws_options.deinit(); - aws_options.credentials.s3Delete(this.path(), @ptrCast(&Wrapper.resolve), Wrapper.new(.{ + S3.delete(&aws_options.credentials, this.path(), @ptrCast(&Wrapper.resolve), Wrapper.new(.{ .promise = promise, .store = store, // store is needed in case of not found error }), proxy); @@ -3519,7 +3579,7 @@ pub const Blob = struct { return value; } - pub fn initWithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *AWS) S3Store { + pub fn initWithReferencedCredentials(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: *S3Credentials) S3Store { credentials.ref(); return .{ .credentials = credentials, @@ -3527,7 +3587,7 @@ pub const Blob = struct { .mime_type = mime_type orelse http.MimeType.other, }; } - pub fn init(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: AWSCredentials) S3Store { + pub fn init(pathlike: JSC.Node.PathLike, mime_type: ?http.MimeType, credentials: S3Credentials) S3Store { return .{ .credentials = credentials.dupe(), .pathlike = pathlike, @@ -3834,7 +3894,7 @@ pub const Blob = struct { pub fn callHandler(this: *S3BlobDownloadTask, raw_bytes: []u8) JSValue { return this.handler(&this.blob, this.globalThis, raw_bytes); } - pub fn onS3DownloadResolved(result: AWS.S3DownloadResult, this: *S3BlobDownloadTask) void { + pub fn onS3DownloadResolved(result: S3.S3DownloadResult, this: *S3BlobDownloadTask) void { defer this.deinit(); switch (result) { .success => |response| { @@ -3868,13 +3928,13 @@ pub const Blob = struct { if (blob.offset > 0) { const len: ?usize = if (blob.size != Blob.max_size) @intCast(blob.size) else null; const offset: usize = @intCast(blob.offset); - credentials.s3DownloadSlice(path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.downloadSlice(credentials, path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); } else if (blob.size == Blob.max_size) { - credentials.s3Download(path, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.download(credentials, path, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); } else { const len: usize = @intCast(blob.size); const offset: usize = @intCast(blob.offset); - credentials.s3DownloadSlice(path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + S3.downloadSlice(credentials, path, offset, len, @ptrCast(&S3BlobDownloadTask.onS3DownloadResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); } return promise; } @@ -4038,7 +4098,18 @@ pub const Blob = struct { const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return (if (extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()).s3UploadStream(path, readable_stream, globalThis, aws_options.options, aws_options.acl, this.contentTypeOrMimeType(), proxy_url, null, undefined); + return S3.uploadStream( + (if (extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), + path, + readable_stream, + globalThis, + aws_options.options, + aws_options.acl, + this.contentTypeOrMimeType(), + proxy_url, + null, + undefined, + ); } if (store.data != .file) { @@ -4264,10 +4335,24 @@ pub const Blob = struct { } } const credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis); - return try credentialsWithOptions.credentials.dupe().s3WritableStream(path, globalThis, credentialsWithOptions.options, this.contentTypeOrMimeType(), proxy_url); + return try S3.writableStream( + credentialsWithOptions.credentials.dupe(), + path, + globalThis, + credentialsWithOptions.options, + this.contentTypeOrMimeType(), + proxy_url, + ); } } - return try s3.getCredentials().s3WritableStream(path, globalThis, .{}, this.contentTypeOrMimeType(), proxy_url); + return try S3.writableStream( + s3.getCredentials(), + path, + globalThis, + .{}, + this.contentTypeOrMimeType(), + proxy_url, + ); } if (store.data != .file) { return globalThis.throwInvalidArguments("Blob is read-only", .{}); diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index d09c7a0c1d..97a471db0e 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -168,6 +168,7 @@ export default [ // Non-standard, s3 + BunFile support unlink: { fn: "doUnlink", length: 0 }, + delete: { fn: "doUnlink", length: 0 }, write: { fn: "doWrite", length: 2 }, size: { getter: "getSize", diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 8d3dccd503..047012427f 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -55,7 +55,7 @@ const Async = bun.Async; const BoringSSL = bun.BoringSSL; const X509 = @import("../api/bun/x509.zig"); const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; -const s3 = @import("../../s3.zig"); +const s3 = bun.S3; pub const Response = struct { const ResponseMixin = BodyMixin(@This()); @@ -533,7 +533,7 @@ pub const Response = struct { .path = blob.store.?.data.s3.path(), .method = .GET, }, .{ .expires = 15 * 60 }) catch |sign_err| { - return s3.AWSCredentials.throwSignError(sign_err, globalThis); + return s3.throwSignError(sign_err, globalThis); }; defer result.deinit(); response.init.headers = response.getOrCreateHeaders(globalThis); @@ -3141,7 +3141,7 @@ pub const Fetch = struct { prepare_body: { // is a S3 file we can use chunked here - if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob(globalThis, &body.AnyBlob.Blob, s3.MultiPartUpload.DefaultPartSize), globalThis)) |stream| { + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob(globalThis, &body.AnyBlob.Blob, s3.MultiPartUploadOptions.DefaultPartSize), globalThis)) |stream| { var old = body; defer old.detach(); body = .{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis) }; @@ -3250,8 +3250,8 @@ pub const Fetch = struct { if (url.isS3()) { // get ENV config - var credentialsWithOptions: s3.AWSCredentials.AWSCredentialsWithOptions = .{ - .credentials = globalThis.bunVM().transpiler.env.getAWSCredentials(), + var credentialsWithOptions: s3.S3CredentialsWithOptions = .{ + .credentials = globalThis.bunVM().transpiler.env.getS3Credentials(), .options = .{}, .acl = null, }; @@ -3263,7 +3263,7 @@ pub const Fetch = struct { if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| { if (s3_options.isObject()) { s3_options.ensureStillAlive(); - credentialsWithOptions = try s3.AWSCredentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, globalThis); + credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, globalThis); } } } @@ -3277,7 +3277,7 @@ pub const Fetch = struct { url_proxy_buffer: []const u8, pub usingnamespace bun.New(@This()); - pub fn resolve(result: s3.AWSCredentials.S3UploadResult, self: *@This()) void { + pub fn resolve(result: s3.S3UploadResult, self: *@This()) void { if (self.promise.globalObject()) |global| { switch (result) { .success => { @@ -3332,7 +3332,8 @@ pub const Fetch = struct { const promise_value = promise.value(); const proxy_url = if (proxy) |p| p.href else ""; - _ = credentialsWithOptions.credentials.dupe().s3UploadStream( + _ = bun.S3.uploadStream( + credentialsWithOptions.credentials.dupe(), url.s3Path(), body.ReadableStream.get().?, globalThis, @@ -3356,7 +3357,7 @@ pub const Fetch = struct { .method = method, }, null) catch |sign_err| { is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, s3.AWSCredentials.getJSSignError(sign_err, globalThis)); + return JSPromise.rejectedPromiseValue(globalThis, s3.getJSSignError(sign_err, globalThis)); }; defer result.deinit(); if (proxy) |proxy_| { diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 8516f12158..9cfa4dc51e 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -43,7 +43,6 @@ const Request = JSC.WebCore.Request; const assert = bun.assert; const Syscall = bun.sys; const uv = bun.windows.libuv; -const S3MultiPartUpload = @import("../../s3.zig").MultiPartUpload; const AnyBlob = JSC.WebCore.AnyBlob; pub const ReadableStream = struct { @@ -379,7 +378,7 @@ pub const ReadableStream = struct { const proxy = globalThis.bunVM().transpiler.env.getHttpProxy(true, null); const proxy_url = if (proxy) |p| p.href else null; - return credentials.s3ReadableStream(path, blob.offset, if (blob.size != Blob.max_size) blob.size else null, proxy_url, globalThis); + return bun.S3.readableStream(credentials, path, blob.offset, if (blob.size != Blob.max_size) blob.size else null, proxy_url, globalThis); }, } } @@ -2668,7 +2667,7 @@ pub const NetworkSink = struct { pub usingnamespace bun.New(NetworkSink); const HTTPWritableStream = union(enum) { fetch: *JSC.WebCore.Fetch.FetchTasklet, - s3_upload: *S3MultiPartUpload, + s3_upload: *bun.S3.MultiPartUpload, }; fn getHighWaterMark(this: *@This()) Blob.SizeType { diff --git a/src/bun.zig b/src/bun.zig index db49641f88..bd611f9276 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -4222,5 +4222,4 @@ pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPath }; pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; -pub const S3 = @import("./s3.zig"); -pub const AWSCredentials = S3.AWSCredentials; +pub const S3 = @import("./s3/client.zig"); diff --git a/src/env_loader.zig b/src/env_loader.zig index 29cfcb7c08..6364684527 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -17,7 +17,7 @@ const Fs = @import("./fs.zig"); const URL = @import("./url.zig").URL; const Api = @import("./api/schema.zig").Api; const which = @import("./which.zig").which; -const s3 = @import("./s3.zig"); +const s3 = bun.S3; const DotEnvFileSuffix = enum { development, @@ -46,7 +46,7 @@ pub const Loader = struct { did_load_process: bool = false, reject_unauthorized: ?bool = null, - aws_credentials: ?s3.AWSCredentials = null, + aws_credentials: ?s3.S3Credentials = null, pub fn iterator(this: *const Loader) Map.HashTable.Iterator { return this.map.iterator(); @@ -115,7 +115,7 @@ pub const Loader = struct { } } - pub fn getAWSCredentials(this: *Loader) s3.AWSCredentials { + pub fn getS3Credentials(this: *Loader) s3.S3Credentials { if (this.aws_credentials) |credentials| { return credentials; } diff --git a/src/s3.zig b/src/s3.zig deleted file mode 100644 index e54a0aae82..0000000000 --- a/src/s3.zig +++ /dev/null @@ -1,2568 +0,0 @@ -const bun = @import("root").bun; -const picohttp = bun.picohttp; -const std = @import("std"); -const DotEnv = @import("./env_loader.zig"); -pub const RareData = @import("./bun.js/rare_data.zig"); - -const JSC = bun.JSC; -const strings = bun.strings; - -pub const ACL = enum { - /// Owner gets FULL_CONTROL. No one else has access rights (default). - private, - /// Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. - public_read, - /// Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. - public_read_write, - /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. - aws_exec_read, - /// Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. - authenticated_read, - /// Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. - bucket_owner_read, - /// Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. - bucket_owner_full_control, - log_delivery_write, - - pub fn toString(this: @This()) []const u8 { - return switch (this) { - .private => "private", - .public_read => "public-read", - .public_read_write => "public-read-write", - .aws_exec_read => "aws-exec-read", - .authenticated_read => "authenticated-read", - .bucket_owner_read => "bucket-owner-read", - .bucket_owner_full_control => "bucket-owner-full-control", - .log_delivery_write => "log-delivery-write", - }; - } - - pub const Map = bun.ComptimeStringMap(ACL, .{ - .{ "private", .private }, - .{ "public-read", .public_read }, - .{ "public-read-write", .public_read_write }, - .{ "aws-exec-read", .aws_exec_read }, - .{ "authenticated-read", .authenticated_read }, - .{ "bucket-owner-read", .bucket_owner_read }, - .{ "bucket-owner-full-control", .bucket_owner_full_control }, - .{ "log-delivery-write", .log_delivery_write }, - }); -}; - -pub const AWSCredentials = struct { - accessKeyId: []const u8, - secretAccessKey: []const u8, - region: []const u8, - endpoint: []const u8, - bucket: []const u8, - sessionToken: []const u8, - - /// Important for MinIO support. - insecure_http: bool = false, - - ref_count: u32 = 1, - pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); - - pub fn estimatedSize(this: *const @This()) usize { - return @sizeOf(AWSCredentials) + this.accessKeyId.len + this.region.len + this.secretAccessKey.len + this.endpoint.len + this.bucket.len; - } - - pub const AWSCredentialsWithOptions = struct { - credentials: AWSCredentials, - options: MultiPartUpload.MultiPartUploadOptions = .{}, - acl: ?ACL = null, - /// indicates if the credentials have changed - changed_credentials: bool = false, - - _accessKeyIdSlice: ?JSC.ZigString.Slice = null, - _secretAccessKeySlice: ?JSC.ZigString.Slice = null, - _regionSlice: ?JSC.ZigString.Slice = null, - _endpointSlice: ?JSC.ZigString.Slice = null, - _bucketSlice: ?JSC.ZigString.Slice = null, - _sessionTokenSlice: ?JSC.ZigString.Slice = null, - - pub fn deinit(this: *@This()) void { - if (this._accessKeyIdSlice) |slice| slice.deinit(); - if (this._secretAccessKeySlice) |slice| slice.deinit(); - if (this._regionSlice) |slice| slice.deinit(); - if (this._endpointSlice) |slice| slice.deinit(); - if (this._bucketSlice) |slice| slice.deinit(); - if (this._sessionTokenSlice) |slice| slice.deinit(); - } - }; - - fn hashConst(acl: []const u8) u64 { - var hasher = std.hash.Wyhash.init(0); - var remain = acl; - - var buf: [@sizeOf(@TypeOf(hasher.buf))]u8 = undefined; - - while (remain.len > 0) { - const end = @min(hasher.buf.len, remain.len); - - hasher.update(strings.copyLowercaseIfNeeded(remain[0..end], &buf)); - remain = remain[end..]; - } - - return hasher.final(); - } - pub fn getCredentialsWithOptions(this: AWSCredentials, default_options: MultiPartUpload.MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, globalObject: *JSC.JSGlobalObject) bun.JSError!AWSCredentialsWithOptions { - // get ENV config - var new_credentials = AWSCredentialsWithOptions{ - .credentials = this, - .options = default_options, - .acl = default_acl, - }; - errdefer { - new_credentials.deinit(); - } - - if (options) |opts| { - if (opts.isObject()) { - if (try opts.getTruthyComptime(globalObject, "accessKeyId")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._accessKeyIdSlice = str.toUTF8(bun.default_allocator); - new_credentials.credentials.accessKeyId = new_credentials._accessKeyIdSlice.?.slice(); - new_credentials.changed_credentials = true; - } - } else { - return globalObject.throwInvalidArgumentTypeValue("accessKeyId", "string", js_value); - } - } - } - if (try opts.getTruthyComptime(globalObject, "secretAccessKey")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._secretAccessKeySlice = str.toUTF8(bun.default_allocator); - new_credentials.credentials.secretAccessKey = new_credentials._secretAccessKeySlice.?.slice(); - new_credentials.changed_credentials = true; - } - } else { - return globalObject.throwInvalidArgumentTypeValue("secretAccessKey", "string", js_value); - } - } - } - if (try opts.getTruthyComptime(globalObject, "region")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._regionSlice = str.toUTF8(bun.default_allocator); - new_credentials.credentials.region = new_credentials._regionSlice.?.slice(); - new_credentials.changed_credentials = true; - } - } else { - return globalObject.throwInvalidArgumentTypeValue("region", "string", js_value); - } - } - } - if (try opts.getTruthyComptime(globalObject, "endpoint")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._endpointSlice = str.toUTF8(bun.default_allocator); - const url = bun.URL.parse(new_credentials._endpointSlice.?.slice()); - const normalized_endpoint = url.host; - if (normalized_endpoint.len > 0) { - new_credentials.credentials.endpoint = normalized_endpoint; - - // Default to https:// - // Only use http:// if the endpoint specifically starts with 'http://' - new_credentials.credentials.insecure_http = url.isHTTP(); - - new_credentials.changed_credentials = true; - } - } - } else { - return globalObject.throwInvalidArgumentTypeValue("endpoint", "string", js_value); - } - } - } - if (try opts.getTruthyComptime(globalObject, "bucket")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._bucketSlice = str.toUTF8(bun.default_allocator); - new_credentials.credentials.bucket = new_credentials._bucketSlice.?.slice(); - new_credentials.changed_credentials = true; - } - } else { - return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); - } - } - } - - if (try opts.getTruthyComptime(globalObject, "sessionToken")) |js_value| { - if (!js_value.isEmptyOrUndefinedOrNull()) { - if (js_value.isString()) { - const str = bun.String.fromJS(js_value, globalObject); - defer str.deref(); - if (str.tag != .Empty and str.tag != .Dead) { - new_credentials._sessionTokenSlice = str.toUTF8(bun.default_allocator); - new_credentials.credentials.sessionToken = new_credentials._sessionTokenSlice.?.slice(); - new_credentials.changed_credentials = true; - } - } else { - return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); - } - } - } - - if (try opts.getOptional(globalObject, "pageSize", i32)) |pageSize| { - if (pageSize < MultiPartUpload.MIN_SINGLE_UPLOAD_SIZE_IN_MiB and pageSize > MultiPartUpload.MAX_SINGLE_UPLOAD_SIZE_IN_MiB) { - return globalObject.throwRangeError(pageSize, .{ - .min = @intCast(MultiPartUpload.MIN_SINGLE_UPLOAD_SIZE_IN_MiB), - .max = @intCast(MultiPartUpload.MAX_SINGLE_UPLOAD_SIZE_IN_MiB), - .field_name = "pageSize", - }); - } else { - new_credentials.options.partSize = @intCast(pageSize); - } - } - - if (try opts.getOptional(globalObject, "queueSize", i32)) |queueSize| { - if (queueSize < 1) { - return globalObject.throwRangeError(queueSize, .{ - .min = 1, - .field_name = "queueSize", - }); - } else { - new_credentials.options.queueSize = @intCast(@max(queueSize, std.math.maxInt(u8))); - } - } - - if (try opts.getOptional(globalObject, "retry", i32)) |retry| { - if (retry < 0 and retry > 255) { - return globalObject.throwRangeError(retry, .{ - .min = 0, - .max = 255, - .field_name = "retry", - }); - } else { - new_credentials.options.retry = @intCast(retry); - } - } - if (try opts.getOptionalEnum(globalObject, "acl", ACL)) |acl| { - new_credentials.acl = acl; - } - } - } - return new_credentials; - } - pub fn dupe(this: *const @This()) *AWSCredentials { - return AWSCredentials.new(.{ - .accessKeyId = if (this.accessKeyId.len > 0) - bun.default_allocator.dupe(u8, this.accessKeyId) catch bun.outOfMemory() - else - "", - - .secretAccessKey = if (this.secretAccessKey.len > 0) - bun.default_allocator.dupe(u8, this.secretAccessKey) catch bun.outOfMemory() - else - "", - - .region = if (this.region.len > 0) - bun.default_allocator.dupe(u8, this.region) catch bun.outOfMemory() - else - "", - - .endpoint = if (this.endpoint.len > 0) - bun.default_allocator.dupe(u8, this.endpoint) catch bun.outOfMemory() - else - "", - - .bucket = if (this.bucket.len > 0) - bun.default_allocator.dupe(u8, this.bucket) catch bun.outOfMemory() - else - "", - - .sessionToken = if (this.sessionToken.len > 0) - bun.default_allocator.dupe(u8, this.sessionToken) catch bun.outOfMemory() - else - "", - - .insecure_http = this.insecure_http, - }); - } - pub fn deinit(this: *@This()) void { - if (this.accessKeyId.len > 0) { - bun.default_allocator.free(this.accessKeyId); - } - if (this.secretAccessKey.len > 0) { - bun.default_allocator.free(this.secretAccessKey); - } - if (this.region.len > 0) { - bun.default_allocator.free(this.region); - } - if (this.endpoint.len > 0) { - bun.default_allocator.free(this.endpoint); - } - if (this.bucket.len > 0) { - bun.default_allocator.free(this.bucket); - } - if (this.sessionToken.len > 0) { - bun.default_allocator.free(this.sessionToken); - } - this.destroy(); - } - - const log = bun.Output.scoped(.AWS, false); - - const DateResult = struct { - // numeric representation of year, month and day (excluding time components) - numeric_day: u64, - date: []const u8, - }; - - fn getAMZDate(allocator: std.mem.Allocator) DateResult { - // We can also use Date.now() but would be slower and would add JSC dependency - // var buffer: [28]u8 = undefined; - // the code bellow is the same as new Date(Date.now()).toISOString() - // JSC.JSValue.getDateNowISOString(globalObject, &buffer); - - // Create UTC timestamp - const secs: u64 = @intCast(@divFloor(std.time.milliTimestamp(), 1000)); - const utc_seconds = std.time.epoch.EpochSeconds{ .secs = secs }; - const utc_day = utc_seconds.getEpochDay(); - const year_and_day = utc_day.calculateYearDay(); - const month_and_day = year_and_day.calculateMonthDay(); - // Get UTC date components - const year = year_and_day.year; - const day = @as(u32, month_and_day.day_index) + 1; // this starts in 0 - const month = month_and_day.month.numeric(); // starts in 1 - - // Get UTC time components - const time = utc_seconds.getDaySeconds(); - const hours = time.getHoursIntoDay(); - const minutes = time.getMinutesIntoHour(); - const seconds = time.getSecondsIntoMinute(); - - // Format the date - return .{ - .numeric_day = secs - time.secs, - .date = std.fmt.allocPrint(allocator, "{d:0>4}{d:0>2}{d:0>2}T{d:0>2}{d:0>2}{d:0>2}Z", .{ - year, - month, - day, - hours, - minutes, - seconds, - }) catch bun.outOfMemory(), - }; - } - - const DIGESTED_HMAC_256_LEN = 32; - pub const SignResult = struct { - amz_date: []const u8, - host: []const u8, - authorization: []const u8, - url: []const u8, - - content_disposition: []const u8 = "", - session_token: []const u8 = "", - acl: ?ACL = null, - _headers: [7]picohttp.Header = .{ - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - }, - _headers_len: u8 = 0, - - pub fn headers(this: *const @This()) []const picohttp.Header { - return this._headers[0..this._headers_len]; - } - - pub fn mixWithHeader(this: *const @This(), headers_buffer: []picohttp.Header, header: picohttp.Header) []const picohttp.Header { - // copy the headers to buffer - const len = this._headers_len; - for (this._headers[0..len], 0..len) |existing_header, i| { - headers_buffer[i] = existing_header; - } - headers_buffer[len] = header; - return headers_buffer[0 .. len + 1]; - } - - pub fn deinit(this: *const @This()) void { - if (this.amz_date.len > 0) { - bun.default_allocator.free(this.amz_date); - } - - if (this.session_token.len > 0) { - bun.default_allocator.free(this.session_token); - } - - if (this.content_disposition.len > 0) { - bun.default_allocator.free(this.content_disposition); - } - - if (this.host.len > 0) { - bun.default_allocator.free(this.host); - } - - if (this.authorization.len > 0) { - bun.default_allocator.free(this.authorization); - } - - if (this.url.len > 0) { - bun.default_allocator.free(this.url); - } - } - }; - - pub const SignQueryOptions = struct { - expires: usize = 86400, - }; - pub const SignOptions = struct { - path: []const u8, - method: bun.http.Method, - content_hash: ?[]const u8 = null, - search_params: ?[]const u8 = null, - content_disposition: ?[]const u8 = null, - acl: ?ACL = null, - }; - - pub fn guessRegion(endpoint: []const u8) []const u8 { - if (endpoint.len > 0) { - if (strings.endsWith(endpoint, ".r2.cloudflarestorage.com")) return "auto"; - if (strings.indexOf(endpoint, ".amazonaws.com")) |end| { - if (strings.indexOf(endpoint, "s3.")) |start| { - return endpoint[start + 3 .. end]; - } - } - } - - return "auto"; - } - fn toHexChar(value: u8) !u8 { - return switch (value) { - 0...9 => value + '0', - 10...15 => (value - 10) + 'A', - else => error.InvalidHexChar, - }; - } - fn encodeURIComponent(input: []const u8, buffer: []u8, comptime encode_slash: bool) ![]const u8 { - var written: usize = 0; - - for (input) |c| { - switch (c) { - // RFC 3986 Unreserved Characters (do not encode) - 'A'...'Z', 'a'...'z', '0'...'9', '-', '_', '.', '~' => { - if (written >= buffer.len) return error.BufferTooSmall; - buffer[written] = c; - written += 1; - }, - // All other characters need to be percent-encoded - else => { - if (!encode_slash and (c == '/' or c == '\\')) { - if (written >= buffer.len) return error.BufferTooSmall; - buffer[written] = if (c == '\\') '/' else c; - written += 1; - continue; - } - if (written + 3 > buffer.len) return error.BufferTooSmall; - buffer[written] = '%'; - // Convert byte to hex - const high_nibble: u8 = (c >> 4) & 0xF; - const low_nibble: u8 = c & 0xF; - buffer[written + 1] = try toHexChar(high_nibble); - buffer[written + 2] = try toHexChar(low_nibble); - written += 3; - }, - } - } - - return buffer[0..written]; - } - - const ErrorCodeAndMessage = struct { - code: []const u8, - message: []const u8, - }; - fn getSignErrorMessage(comptime err: anyerror) [:0]const u8 { - return switch (err) { - error.MissingCredentials => return "Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required", - error.InvalidMethod => return "Method must be GET, PUT, DELETE or HEAD when using s3:// protocol", - error.InvalidPath => return "Invalid S3 bucket, key combination", - error.InvalidEndpoint => return "Invalid S3 endpoint", - error.InvalidSessionToken => return "Invalid session token", - else => return "Failed to retrieve S3 content. Are the credentials correct?", - }; - } - pub fn getJSSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return switch (err) { - error.MissingCredentials => return globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).toJS(), - error.InvalidMethod => return globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).toJS(), - error.InvalidPath => return globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).toJS(), - error.InvalidEndpoint => return globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).toJS(), - error.InvalidSessionToken => return globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).toJS(), - else => return globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).toJS(), - }; - } - pub fn throwSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) bun.JSError { - return switch (err) { - error.MissingCredentials => globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).throw(), - error.InvalidMethod => globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).throw(), - error.InvalidPath => globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).throw(), - error.InvalidEndpoint => globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).throw(), - error.InvalidSessionToken => globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).throw(), - else => globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).throw(), - }; - } - pub fn getSignErrorCodeAndMessage(err: anyerror) ErrorCodeAndMessage { - // keep error codes consistent for internal errors - return switch (err) { - error.MissingCredentials => .{ .code = "ERR_S3_MISSING_CREDENTIALS", .message = getSignErrorMessage(error.MissingCredentials) }, - error.InvalidMethod => .{ .code = "ERR_S3_INVALID_METHOD", .message = getSignErrorMessage(error.InvalidMethod) }, - error.InvalidPath => .{ .code = "ERR_S3_INVALID_PATH", .message = getSignErrorMessage(error.InvalidPath) }, - error.InvalidEndpoint => .{ .code = "ERR_S3_INVALID_ENDPOINT", .message = getSignErrorMessage(error.InvalidEndpoint) }, - error.InvalidSessionToken => .{ .code = "ERR_S3_INVALID_SESSION_TOKEN", .message = getSignErrorMessage(error.InvalidSessionToken) }, - else => .{ .code = "ERR_S3_INVALID_SIGNATURE", .message = getSignErrorMessage(error.SignError) }, - }; - } - - pub fn signRequest(this: *const @This(), signOptions: SignOptions, signQueryOption: ?SignQueryOptions) !SignResult { - const method = signOptions.method; - const request_path = signOptions.path; - const content_hash = signOptions.content_hash; - const search_params = signOptions.search_params; - - var content_disposition = signOptions.content_disposition; - if (content_disposition != null and content_disposition.?.len == 0) { - content_disposition = null; - } - const session_token: ?[]const u8 = if (this.sessionToken.len == 0) null else this.sessionToken; - - const acl: ?[]const u8 = if (signOptions.acl) |acl_value| acl_value.toString() else null; - - if (this.accessKeyId.len == 0 or this.secretAccessKey.len == 0) return error.MissingCredentials; - const signQuery = signQueryOption != null; - const expires = if (signQueryOption) |options| options.expires else 0; - const method_name = switch (method) { - .GET => "GET", - .POST => "POST", - .PUT => "PUT", - .DELETE => "DELETE", - .HEAD => "HEAD", - else => return error.InvalidMethod, - }; - - const region = if (this.region.len > 0) this.region else guessRegion(this.endpoint); - var full_path = request_path; - // handle \\ on bucket name - if (strings.startsWith(full_path, "/")) { - full_path = full_path[1..]; - } else if (strings.startsWith(full_path, "\\")) { - full_path = full_path[1..]; - } - - var path: []const u8 = full_path; - var bucket: []const u8 = this.bucket; - - if (bucket.len == 0) { - //TODO: r2 supports bucket in the endpoint - - // guess bucket using path - if (strings.indexOf(full_path, "/")) |end| { - if (strings.indexOf(full_path, "\\")) |backslash_index| { - if (backslash_index < end) { - bucket = full_path[0..backslash_index]; - path = full_path[backslash_index + 1 ..]; - } - } - bucket = full_path[0..end]; - path = full_path[end + 1 ..]; - } else if (strings.indexOf(full_path, "\\")) |backslash_index| { - bucket = full_path[0..backslash_index]; - path = full_path[backslash_index + 1 ..]; - } else { - return error.InvalidPath; - } - } - if (strings.endsWith(path, "/")) { - path = path[0..path.len]; - } else if (strings.endsWith(path, "\\")) { - path = path[0 .. path.len - 1]; - } - if (strings.startsWith(path, "/")) { - path = path[1..]; - } else if (strings.startsWith(path, "\\")) { - path = path[1..]; - } - - // if we allow path.len == 0 it will list the bucket for now we disallow - if (path.len == 0) return error.InvalidPath; - - var normalized_path_buffer: [1024 + 63 + 2]u8 = undefined; // 1024 max key size and 63 max bucket name - var path_buffer: [1024]u8 = undefined; - var bucket_buffer: [63]u8 = undefined; - bucket = encodeURIComponent(bucket, &bucket_buffer, false) catch return error.InvalidPath; - path = encodeURIComponent(path, &path_buffer, false) catch return error.InvalidPath; - const normalizedPath = std.fmt.bufPrint(&normalized_path_buffer, "/{s}/{s}", .{ bucket, path }) catch return error.InvalidPath; - - const date_result = getAMZDate(bun.default_allocator); - const amz_date = date_result.date; - errdefer bun.default_allocator.free(amz_date); - - const amz_day = amz_date[0..8]; - const signed_headers = if (signQuery) "host" else brk: { - if (acl != null) { - if (content_disposition != null) { - if (session_token != null) { - break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token"; - } else { - break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date"; - } - } else { - if (session_token != null) { - break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; - } else { - break :brk "host;x-amz-content-sha256;x-amz-date"; - } - } - } else { - if (content_disposition != null) { - if (session_token != null) { - break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; - } else { - break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date"; - } - } else { - if (session_token != null) { - break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; - } else { - break :brk "host;x-amz-content-sha256;x-amz-date"; - } - } - } - }; - - // Default to https. Only use http if they explicit pass "http://" as the endpoint. - const protocol = if (this.insecure_http) "http" else "https"; - - // detect service name and host from region or endpoint - var encoded_host_buffer: [512]u8 = undefined; - var encoded_host: []const u8 = ""; - const host = brk_host: { - if (this.endpoint.len > 0) { - encoded_host = encodeURIComponent(this.endpoint, &encoded_host_buffer, true) catch return error.InvalidEndpoint; - break :brk_host try bun.default_allocator.dupe(u8, this.endpoint); - } else { - break :brk_host try std.fmt.allocPrint(bun.default_allocator, "s3.{s}.amazonaws.com", .{region}); - } - }; - const service_name = "s3"; - - errdefer bun.default_allocator.free(host); - - const aws_content_hash = if (content_hash) |hash| hash else ("UNSIGNED-PAYLOAD"); - var tmp_buffer: [4096]u8 = undefined; - - const authorization = brk: { - // we hash the hash so we need 2 buffers - var hmac_sig_service: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; - var hmac_sig_service2: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; - - const sigDateRegionServiceReq = brk_sign: { - const key = try std.fmt.bufPrint(&tmp_buffer, "{s}{s}{s}", .{ region, service_name, this.secretAccessKey }); - var cache = (JSC.VirtualMachine.getMainThreadVM() orelse JSC.VirtualMachine.get()).rareData().awsCache(); - if (cache.get(date_result.numeric_day, key)) |cached| { - break :brk_sign cached; - } - // not cached yet lets generate a new one - const sigDate = bun.hmac.generate(try std.fmt.bufPrint(&tmp_buffer, "AWS4{s}", .{this.secretAccessKey}), amz_day, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; - const sigDateRegion = bun.hmac.generate(sigDate, region, .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; - const sigDateRegionService = bun.hmac.generate(sigDateRegion, service_name, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; - const result = bun.hmac.generate(sigDateRegionService, "aws4_request", .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; - - cache.set(date_result.numeric_day, key, hmac_sig_service2[0..DIGESTED_HMAC_256_LEN].*); - break :brk_sign result; - }; - if (signQuery) { - var token_encoded_buffer: [2048]u8 = undefined; // token is normaly like 600-700 but can be up to 2k - var encoded_session_token: ?[]const u8 = null; - if (session_token) |token| { - encoded_session_token = encodeURIComponent(token, &token_encoded_buffer, true) catch return error.InvalidSessionToken; - } - const canonical = brk_canonical: { - if (acl) |acl_value| { - if (encoded_session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); - } - } else { - if (encoded_session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); - } - } - }; - var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); - bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); - - const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); - - const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; - if (acl) |acl_value| { - if (encoded_session_token) |token| { - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); - } else { - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); - } - } else { - if (encoded_session_token) |token| { - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); - } else { - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", - .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); - } - } - } else { - var encoded_content_disposition_buffer: [255]u8 = undefined; - const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer, true) catch return error.ContentTypeIsTooLong else ""; - const canonical = brk_canonical: { - if (acl) |acl_value| { - if (content_disposition != null) { - if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); - } - } else { - if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); - } - } - } else { - if (content_disposition != null) { - if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); - } - } else { - if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); - } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); - } - } - } - }; - var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); - bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); - - const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); - - const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; - - break :brk try std.fmt.allocPrint( - bun.default_allocator, - "AWS4-HMAC-SHA256 Credential={s}/{s}/{s}/{s}/aws4_request, SignedHeaders={s}, Signature={s}", - .{ this.accessKeyId, amz_day, region, service_name, signed_headers, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, - ); - } - }; - errdefer bun.default_allocator.free(authorization); - - if (signQuery) { - defer bun.default_allocator.free(host); - defer bun.default_allocator.free(amz_date); - - return SignResult{ - .amz_date = "", - .host = "", - .authorization = "", - .acl = signOptions.acl, - .url = authorization, - }; - } - - var result = SignResult{ - .amz_date = amz_date, - .host = host, - .authorization = authorization, - .acl = signOptions.acl, - .url = try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}{s}", .{ protocol, host, normalizedPath, if (search_params) |s| s else "" }), - ._headers = [_]picohttp.Header{ - .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, - .{ .name = "x-amz-date", .value = amz_date }, - .{ .name = "Authorization", .value = authorization[0..] }, - .{ .name = "Host", .value = host }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - .{ .name = "", .value = "" }, - }, - ._headers_len = 4, - }; - - if (acl) |acl_value| { - result._headers[result._headers_len] = .{ .name = "x-amz-acl", .value = acl_value }; - result._headers_len += 1; - } - - if (session_token) |token| { - const session_token_value = bun.default_allocator.dupe(u8, token) catch bun.outOfMemory(); - result.session_token = session_token_value; - result._headers[result._headers_len] = .{ .name = "x-amz-security-token", .value = session_token_value }; - result._headers_len += 1; - } - - if (content_disposition) |cd| { - const content_disposition_value = bun.default_allocator.dupe(u8, cd) catch bun.outOfMemory(); - result.content_disposition = content_disposition_value; - result._headers[result._headers_len] = .{ .name = "Content-Disposition", .value = content_disposition_value }; - result._headers_len += 1; - } - - return result; - } - const JSS3Error = extern struct { - code: bun.String = bun.String.empty, - message: bun.String = bun.String.empty, - path: bun.String = bun.String.empty, - - pub fn init(code: []const u8, message: []const u8, path: ?[]const u8) @This() { - return .{ - // lets make sure we can reuse code and message and keep it service independent - .code = bun.String.createAtomIfPossible(code), - .message = bun.String.createAtomIfPossible(message), - .path = if (path) |p| bun.String.init(p) else bun.String.empty, - }; - } - - pub fn deinit(this: *const @This()) void { - this.path.deref(); - this.code.deref(); - this.message.deref(); - } - - pub fn toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) JSC.JSValue { - defer this.deinit(); - - return S3Error__toErrorInstance(this, global); - } - extern fn S3Error__toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue; - }; - - pub const S3Error = struct { - code: []const u8, - message: []const u8, - - pub fn toJS(err: *const @This(), globalObject: *JSC.JSGlobalObject, path: ?[]const u8) JSC.JSValue { - const value = JSS3Error.init(err.code, err.message, path).toErrorInstance(globalObject); - bun.assert(!globalObject.hasException()); - return value; - } - }; - pub const S3StatResult = union(enum) { - success: struct { - size: usize = 0, - /// etag is not owned and need to be copied if used after this callback - etag: []const u8 = "", - }, - not_found: S3Error, - - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - pub const S3DownloadResult = union(enum) { - success: struct { - /// etag is not owned and need to be copied if used after this callback - etag: []const u8 = "", - /// body is owned and dont need to be copied, but dont forget to free it - body: bun.MutableString, - }, - not_found: S3Error, - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - pub const S3UploadResult = union(enum) { - success: void, - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - pub const S3DeleteResult = union(enum) { - success: void, - not_found: S3Error, - - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - // commit result also fails if status 200 but with body containing an Error - pub const S3CommitResult = union(enum) { - success: void, - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - // commit result also fails if status 200 but with body containing an Error - pub const S3PartResult = union(enum) { - etag: []const u8, - /// failure error is not owned and need to be copied if used after this callback - failure: S3Error, - }; - pub const S3HttpSimpleTask = struct { - http: bun.http.AsyncHTTP, - vm: *JSC.VirtualMachine, - sign_result: SignResult, - headers: JSC.WebCore.Headers, - callback_context: *anyopaque, - callback: Callback, - response_buffer: bun.MutableString = .{ - .allocator = bun.default_allocator, - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }, - result: bun.http.HTTPClientResult = .{}, - concurrent_task: JSC.ConcurrentTask = .{}, - range: ?[]const u8, - poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), - - usingnamespace bun.New(@This()); - pub const Callback = union(enum) { - stat: *const fn (S3StatResult, *anyopaque) void, - download: *const fn (S3DownloadResult, *anyopaque) void, - upload: *const fn (S3UploadResult, *anyopaque) void, - delete: *const fn (S3DeleteResult, *anyopaque) void, - commit: *const fn (S3CommitResult, *anyopaque) void, - part: *const fn (S3PartResult, *anyopaque) void, - - pub fn fail(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { - switch (this) { - inline .upload, - .download, - .stat, - .delete, - .commit, - .part, - => |callback| callback(.{ - .failure = .{ - .code = code, - .message = message, - }, - }, context), - } - } - pub fn notFound(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { - switch (this) { - inline .download, - .stat, - .delete, - => |callback| callback(.{ - .not_found = .{ - .code = code, - .message = message, - }, - }, context), - else => this.fail(code, message, context), - } - } - }; - pub fn deinit(this: *@This()) void { - if (this.result.certificate_info) |*certificate| { - certificate.deinit(bun.default_allocator); - } - this.poll_ref.unref(this.vm); - this.response_buffer.deinit(); - this.headers.deinit(); - this.sign_result.deinit(); - this.http.clearData(); - if (this.range) |range| { - bun.default_allocator.free(range); - } - if (this.result.metadata) |*metadata| { - metadata.deinit(bun.default_allocator); - } - this.destroy(); - } - - const ErrorType = enum { - not_found, - failure, - }; - fn errorWithBody(this: @This(), comptime error_type: ErrorType) void { - var code: []const u8 = "UnknownError"; - var message: []const u8 = "an unexpected error has occurred"; - var has_error_code = false; - if (this.result.fail) |err| { - code = @errorName(err); - has_error_code = true; - } else if (this.result.body) |body| { - const bytes = body.list.items; - if (bytes.len > 0) { - message = bytes[0..]; - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - code = bytes[start + "".len .. end]; - has_error_code = true; - } - } - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - message = bytes[start + "".len .. end]; - } - } - } - } - - if (error_type == .not_found) { - if (!has_error_code) { - code = "NoSuchKey"; - message = "The specified key does not exist."; - } - this.callback.notFound(code, message, this.callback_context); - } else { - this.callback.fail(code, message, this.callback_context); - } - } - - fn failIfContainsError(this: *@This(), status: u32) bool { - var code: []const u8 = "UnknownError"; - var message: []const u8 = "an unexpected error has occurred"; - - if (this.result.fail) |err| { - code = @errorName(err); - } else if (this.result.body) |body| { - const bytes = body.list.items; - var has_error = false; - if (bytes.len > 0) { - message = bytes[0..]; - if (strings.indexOf(bytes, "") != null) { - has_error = true; - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - code = bytes[start + "".len .. end]; - } - } - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - message = bytes[start + "".len .. end]; - } - } - } - } - if (!has_error and status == 200 or status == 206) { - return false; - } - } else if (status == 200 or status == 206) { - return false; - } - this.callback.fail(code, message, this.callback_context); - return true; - } - - pub fn onResponse(this: *@This()) void { - defer this.deinit(); - if (!this.result.isSuccess()) { - this.errorWithBody(.failure); - return; - } - bun.assert(this.result.metadata != null); - const response = this.result.metadata.?.response; - switch (this.callback) { - .stat => |callback| { - switch (response.status_code) { - 200 => { - callback(.{ - .success = .{ - .etag = response.headers.get("etag") orelse "", - .size = if (response.headers.get("content-length")) |content_len| (std.fmt.parseInt(usize, content_len, 10) catch 0) else 0, - }, - }, this.callback_context); - }, - 404 => { - this.errorWithBody(.not_found); - }, - else => { - this.errorWithBody(.failure); - }, - } - }, - .delete => |callback| { - switch (response.status_code) { - 200, 204 => { - callback(.{ .success = {} }, this.callback_context); - }, - 404 => { - this.errorWithBody(.not_found); - }, - else => { - this.errorWithBody(.failure); - }, - } - }, - .upload => |callback| { - switch (response.status_code) { - 200 => { - callback(.{ .success = {} }, this.callback_context); - }, - else => { - this.errorWithBody(.failure); - }, - } - }, - .download => |callback| { - switch (response.status_code) { - 200, 204, 206 => { - const body = this.response_buffer; - this.response_buffer = .{ - .allocator = bun.default_allocator, - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }; - callback(.{ - .success = .{ - .etag = response.headers.get("etag") orelse "", - .body = body, - }, - }, this.callback_context); - }, - 404 => { - this.errorWithBody(.not_found); - }, - else => { - //error - this.errorWithBody(.failure); - }, - } - }, - .commit => |callback| { - // commit multipart upload can fail with status 200 - if (!this.failIfContainsError(response.status_code)) { - callback(.{ .success = {} }, this.callback_context); - } - }, - .part => |callback| { - if (!this.failIfContainsError(response.status_code)) { - if (response.headers.get("etag")) |etag| { - callback(.{ .etag = etag }, this.callback_context); - } else { - this.errorWithBody(.failure); - } - } - }, - } - } - - pub fn http_callback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { - const is_done = !result.has_more; - this.result = result; - this.http = async_http.*; - this.response_buffer = async_http.response_buffer.*; - if (is_done) { - this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } - } - }; - - pub const S3HttpDownloadStreamingTask = struct { - http: bun.http.AsyncHTTP, - vm: *JSC.VirtualMachine, - sign_result: SignResult, - headers: JSC.WebCore.Headers, - callback_context: *anyopaque, - // this transfers ownership from the chunk - callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?S3Error, *anyopaque) void, - has_schedule_callback: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), - signal_store: bun.http.Signals.Store = .{}, - signals: bun.http.Signals = .{}, - poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), - - response_buffer: bun.MutableString = .{ - .allocator = bun.default_allocator, - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }, - reported_response_lock: bun.Lock = .{}, - reported_response_buffer: bun.MutableString = .{ - .allocator = bun.default_allocator, - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }, - state: State.AtomicType = State.AtomicType.init(0), - - concurrent_task: JSC.ConcurrentTask = .{}, - range: ?[]const u8, - proxy_url: []const u8, - - usingnamespace bun.New(@This()); - pub const State = packed struct(u64) { - pub const AtomicType = std.atomic.Value(u64); - status_code: u32 = 0, - request_error: u16 = 0, - has_more: bool = false, - _reserved: u15 = 0, - }; - - pub fn getState(this: @This()) State { - const state: State = @bitCast(this.state.load(.acquire)); - return state; - } - - pub fn setState(this: *@This(), state: State) void { - this.state.store(@bitCast(state), .monotonic); - } - - pub fn deinit(this: *@This()) void { - this.poll_ref.unref(this.vm); - this.response_buffer.deinit(); - this.reported_response_buffer.deinit(); - this.headers.deinit(); - this.sign_result.deinit(); - this.http.clearData(); - if (this.range) |range| { - bun.default_allocator.free(range); - } - if (this.proxy_url.len > 0) { - bun.default_allocator.free(this.proxy_url); - } - - this.destroy(); - } - - fn reportProgress(this: *@This()) bool { - var has_more = true; - var err: ?S3Error = null; - var failed = false; - this.reported_response_lock.lock(); - defer this.reported_response_lock.unlock(); - const chunk = brk: { - const state = this.getState(); - has_more = state.has_more; - switch (state.status_code) { - 200, 204, 206 => { - failed = state.request_error != 0; - }, - else => { - failed = true; - }, - } - if (failed) { - if (!has_more) { - var has_body_code = false; - var has_body_message = false; - - var code: []const u8 = "UnknownError"; - var message: []const u8 = "an unexpected error has occurred"; - if (state.request_error != 0) { - const req_err = @errorFromInt(state.request_error); - code = @errorName(req_err); - has_body_code = true; - } else { - const bytes = this.reported_response_buffer.list.items; - if (bytes.len > 0) { - message = bytes[0..]; - - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - code = bytes[start + "".len .. end]; - has_body_code = true; - } - } - if (strings.indexOf(bytes, "")) |start| { - if (strings.indexOf(bytes, "")) |end| { - message = bytes[start + "".len .. end]; - has_body_message = true; - } - } - } - } - - err = .{ - .code = code, - .message = message, - }; - } - break :brk bun.MutableString{ .allocator = bun.default_allocator, .list = .{} }; - } else { - const buffer = this.reported_response_buffer; - break :brk buffer; - } - }; - log("reportProgres failed: {} has_more: {} len: {d}", .{ failed, has_more, chunk.list.items.len }); - if (failed) { - if (!has_more) { - this.callback(chunk, false, err, this.callback_context); - } - } else { - // dont report empty chunks if we have more data to read - if (!has_more or chunk.list.items.len > 0) { - this.callback(chunk, has_more, null, this.callback_context); - this.reported_response_buffer.reset(); - } - } - - return has_more; - } - - pub fn onResponse(this: *@This()) void { - this.has_schedule_callback.store(false, .monotonic); - const has_more = this.reportProgress(); - if (!has_more) this.deinit(); - } - - pub fn http_callback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { - const is_done = !result.has_more; - var state = this.getState(); - - var wait_until_done = false; - { - state.has_more = !is_done; - - state.request_error = if (result.fail) |err| @intFromError(err) else 0; - if (state.status_code == 0) { - if (result.certificate_info) |*certificate| { - certificate.deinit(bun.default_allocator); - } - if (result.metadata) |m| { - var metadata = m; - state.status_code = metadata.response.status_code; - metadata.deinit(bun.default_allocator); - } - } - switch (state.status_code) { - 200, 204, 206 => wait_until_done = state.request_error != 0, - else => wait_until_done = true, - } - this.setState(state); - this.http = async_http.*; - } - // if we got a error or fail wait until we are done buffering the response body to report - const should_enqueue = !wait_until_done or is_done; - log("state err: {} status_code: {} has_more: {} should_enqueue: {}", .{ state.request_error, state.status_code, state.has_more, should_enqueue }); - if (should_enqueue) { - if (result.body) |body| { - this.reported_response_lock.lock(); - defer this.reported_response_lock.unlock(); - this.response_buffer = body.*; - if (body.list.items.len > 0) { - _ = this.reported_response_buffer.write(body.list.items) catch bun.outOfMemory(); - } - this.response_buffer.reset(); - if (this.reported_response_buffer.list.items.len == 0 and !is_done) { - return; - } - } else if (!is_done) { - return; - } - if (this.has_schedule_callback.cmpxchgStrong(false, true, .acquire, .monotonic)) |has_schedule_callback| { - if (has_schedule_callback) { - return; - } - } - this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } - } - }; - - pub const S3SimpleRequestOptions = struct { - // signing options - path: []const u8, - method: bun.http.Method, - search_params: ?[]const u8 = null, - content_type: ?[]const u8 = null, - content_disposition: ?[]const u8 = null, - - // http request options - body: []const u8, - proxy_url: ?[]const u8 = null, - range: ?[]const u8 = null, - acl: ?ACL = null, - }; - - pub fn executeSimpleS3Request( - this: *const @This(), - options: S3SimpleRequestOptions, - callback: S3HttpSimpleTask.Callback, - callback_context: *anyopaque, - ) void { - var result = this.signRequest(.{ - .path = options.path, - .method = options.method, - .search_params = options.search_params, - .content_disposition = options.content_disposition, - .acl = options.acl, - }, null) catch |sign_err| { - if (options.range) |range_| bun.default_allocator.free(range_); - const error_code_and_message = getSignErrorCodeAndMessage(sign_err); - callback.fail(error_code_and_message.code, error_code_and_message.message, callback_context); - return; - }; - - const headers = brk: { - var header_buffer: [10]picohttp.Header = undefined; - if (options.range) |range_| { - const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); - } else { - if (options.content_type) |content_type| { - if (content_type.len > 0) { - const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = content_type }); - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); - } - } - - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); - } - }; - const task = S3HttpSimpleTask.new(.{ - .http = undefined, - .sign_result = result, - .callback_context = callback_context, - .callback = callback, - .range = options.range, - .headers = headers, - .vm = JSC.VirtualMachine.get(), - }); - task.poll_ref.ref(task.vm); - - const url = bun.URL.parse(result.url); - const proxy = options.proxy_url orelse ""; - task.http = bun.http.AsyncHTTP.init( - bun.default_allocator, - options.method, - url, - task.headers.entries, - task.headers.buf.items, - &task.response_buffer, - options.body, - bun.http.HTTPClientResult.Callback.New( - *S3HttpSimpleTask, - S3HttpSimpleTask.http_callback, - ).init(task), - .follow, - .{ - .http_proxy = if (proxy.len > 0) bun.URL.parse(proxy) else null, - .verbose = task.vm.getVerboseFetch(), - .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), - }, - ); - // queue http request - bun.http.HTTPThread.init(&.{}); - var batch = bun.ThreadPool.Batch{}; - task.http.schedule(bun.default_allocator, &batch); - bun.http.http_thread.schedule(batch); - } - - pub fn s3Stat(this: *const @This(), path: []const u8, callback: *const fn (S3StatResult, *anyopaque) void, callback_context: *anyopaque, proxy_url: ?[]const u8) void { - this.executeSimpleS3Request(.{ - .path = path, - .method = .HEAD, - .proxy_url = proxy_url, - .body = "", - }, .{ .stat = callback }, callback_context); - } - - pub fn s3Download(this: *const @This(), path: []const u8, callback: *const fn (S3DownloadResult, *anyopaque) void, callback_context: *anyopaque, proxy_url: ?[]const u8) void { - this.executeSimpleS3Request(.{ - .path = path, - .method = .GET, - .proxy_url = proxy_url, - .body = "", - }, .{ .download = callback }, callback_context); - } - - pub fn s3DownloadSlice(this: *const @This(), path: []const u8, offset: usize, size: ?usize, callback: *const fn (S3DownloadResult, *anyopaque) void, callback_context: *anyopaque, proxy_url: ?[]const u8) void { - const range = brk: { - if (size) |size_| { - if (offset == 0) break :brk null; - - var end = (offset + size_); - if (size_ > 0) { - end -= 1; - } - break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); - } - if (offset == 0) break :brk null; - break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); - }; - - this.executeSimpleS3Request(.{ - .path = path, - .method = .GET, - .proxy_url = proxy_url, - .body = "", - .range = range, - }, .{ .download = callback }, callback_context); - } - - pub fn s3StreamDownload(this: *@This(), path: []const u8, offset: usize, size: ?usize, proxy_url: ?[]const u8, callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?S3Error, *anyopaque) void, callback_context: *anyopaque) void { - const range = brk: { - if (size) |size_| { - if (offset == 0) break :brk null; - - var end = (offset + size_); - if (size_ > 0) { - end -= 1; - } - break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); - } - if (offset == 0) break :brk null; - break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); - }; - - var result = this.signRequest(.{ - .path = path, - .method = .GET, - }, null) catch |sign_err| { - if (range) |range_| bun.default_allocator.free(range_); - const error_code_and_message = getSignErrorCodeAndMessage(sign_err); - callback(.{ .allocator = bun.default_allocator, .list = .{} }, false, .{ .code = error_code_and_message.code, .message = error_code_and_message.message }, callback_context); - return; - }; - - var header_buffer: [10]picohttp.Header = undefined; - const headers = brk: { - if (range) |range_| { - const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); - } else { - break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); - } - }; - const proxy = proxy_url orelse ""; - const owned_proxy = if (proxy.len > 0) bun.default_allocator.dupe(u8, proxy) catch bun.outOfMemory() else ""; - const task = S3HttpDownloadStreamingTask.new(.{ - .http = undefined, - .sign_result = result, - .proxy_url = owned_proxy, - .callback_context = callback_context, - .callback = callback, - .range = range, - .headers = headers, - .vm = JSC.VirtualMachine.get(), - }); - task.poll_ref.ref(task.vm); - - const url = bun.URL.parse(result.url); - - task.signals = task.signal_store.to(); - - task.http = bun.http.AsyncHTTP.init( - bun.default_allocator, - .GET, - url, - task.headers.entries, - task.headers.buf.items, - &task.response_buffer, - "", - bun.http.HTTPClientResult.Callback.New( - *S3HttpDownloadStreamingTask, - S3HttpDownloadStreamingTask.http_callback, - ).init(task), - .follow, - .{ - .http_proxy = if (owned_proxy.len > 0) bun.URL.parse(owned_proxy) else null, - .verbose = task.vm.getVerboseFetch(), - .signals = task.signals, - .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), - }, - ); - // enable streaming - task.http.enableBodyStreaming(); - // queue http request - bun.http.HTTPThread.init(&.{}); - var batch = bun.ThreadPool.Batch{}; - task.http.schedule(bun.default_allocator, &batch); - bun.http.http_thread.schedule(batch); - } - - pub fn s3ReadableStream(this: *@This(), path: []const u8, offset: usize, size: ?usize, proxy_url: ?[]const u8, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - var reader = JSC.WebCore.ByteStream.Source.new(.{ - .context = undefined, - .globalThis = globalThis, - }); - - reader.context.setup(); - const readable_value = reader.toReadableStream(globalThis); - - this.s3StreamDownload(path, offset, size, proxy_url, @ptrCast(&S3DownloadStreamWrapper.callback), S3DownloadStreamWrapper.new(.{ - .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(.{ - .ptr = .{ .Bytes = &reader.context }, - .value = readable_value, - }, globalThis), - .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), - })); - return readable_value; - } - - const S3DownloadStreamWrapper = struct { - readable_stream_ref: JSC.WebCore.ReadableStream.Strong, - path: []const u8, - pub usingnamespace bun.New(@This()); - - pub fn callback(chunk: bun.MutableString, has_more: bool, request_err: ?S3Error, this: *@This()) void { - defer if (!has_more) this.deinit(); - - if (this.readable_stream_ref.get()) |readable| { - if (readable.ptr == .Bytes) { - const globalThis = this.readable_stream_ref.globalThis().?; - - if (request_err) |err| { - log("S3DownloadStreamWrapper.callback .temporary", .{}); - - readable.ptr.Bytes.onData( - .{ - .err = .{ .JSValue = err.toJS(globalThis, this.path) }, - }, - bun.default_allocator, - ); - return; - } - if (has_more) { - log("S3DownloadStreamWrapper.callback .temporary", .{}); - - readable.ptr.Bytes.onData( - .{ - .temporary = bun.ByteList.initConst(chunk.list.items), - }, - bun.default_allocator, - ); - return; - } - log("S3DownloadStreamWrapper.callback .temporary_and_done", .{}); - - readable.ptr.Bytes.onData( - .{ - .temporary_and_done = bun.ByteList.initConst(chunk.list.items), - }, - bun.default_allocator, - ); - return; - } - } - log("S3DownloadStreamWrapper.callback invalid readable stream", .{}); - } - - pub fn deinit(this: *@This()) void { - this.readable_stream_ref.deinit(); - bun.default_allocator.free(this.path); - this.destroy(); - } - }; - - pub fn s3Delete(this: *const @This(), path: []const u8, callback: *const fn (S3DeleteResult, *anyopaque) void, callback_context: *anyopaque, proxy_url: ?[]const u8) void { - this.executeSimpleS3Request(.{ - .path = path, - .method = .DELETE, - .proxy_url = proxy_url, - .body = "", - }, .{ .delete = callback }, callback_context); - } - - pub fn s3Upload(this: *const @This(), path: []const u8, content: []const u8, content_type: ?[]const u8, acl: ?ACL, proxy_url: ?[]const u8, callback: *const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) void { - this.executeSimpleS3Request(.{ - .path = path, - .method = .PUT, - .proxy_url = proxy_url, - .body = content, - .content_type = content_type, - .acl = acl, - }, .{ .upload = callback }, callback_context); - } - - const S3UploadStreamWrapper = struct { - readable_stream_ref: JSC.WebCore.ReadableStream.Strong, - sink: *JSC.WebCore.NetworkSink, - task: *MultiPartUpload, - callback: ?*const fn (S3UploadResult, *anyopaque) void, - callback_context: *anyopaque, - ref_count: u32 = 1, - path: []const u8, // this is owned by the task not by the wrapper - pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); - pub fn resolve(result: S3UploadResult, self: *@This()) void { - const sink = self.sink; - defer self.deref(); - if (sink.endPromise.hasValue()) { - if (sink.endPromise.globalObject()) |globalObject| { - switch (result) { - .success => sink.endPromise.resolve(globalObject, JSC.jsNumber(0)), - .failure => |err| { - if (!sink.done) { - sink.abort(); - return; - } - sink.endPromise.reject(globalObject, err.toJS(globalObject, self.path)); - }, - } - } - } - if (self.callback) |callback| { - callback(result, self.callback_context); - } - } - - pub fn deinit(self: *@This()) void { - self.readable_stream_ref.deinit(); - self.sink.finalize(); - self.sink.destroy(); - self.task.deref(); - self.destroy(); - } - }; - pub fn onUploadStreamResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var args = callframe.arguments_old(2); - var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); - defer this.deref(); - - if (this.readable_stream_ref.get()) |stream| { - stream.done(globalThis); - } - this.readable_stream_ref.deinit(); - this.task.continueStream(); - - return .undefined; - } - - pub fn onUploadStreamRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const args = callframe.arguments_old(2); - var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); - defer this.deref(); - - const err = args.ptr[0]; - if (this.sink.endPromise.hasValue()) { - this.sink.endPromise.reject(globalThis, err); - } - - if (this.readable_stream_ref.get()) |stream| { - stream.cancel(globalThis); - this.readable_stream_ref.deinit(); - } - if (this.sink.task) |task| { - if (task == .s3_upload) { - task.s3_upload.fail(.{ - .code = "UnknownError", - .message = "ReadableStream ended with an error", - }); - } - } - this.task.continueStream(); - - return .undefined; - } - pub const shim = JSC.Shimmer("Bun", "S3UploadStream", @This()); - - pub const Export = shim.exportFunctions(.{ - .onResolveRequestStream = onUploadStreamResolveRequestStream, - .onRejectRequestStream = onUploadStreamRejectRequestStream, - }); - comptime { - const jsonResolveRequestStream = JSC.toJSHostFunction(onUploadStreamResolveRequestStream); - @export(jsonResolveRequestStream, .{ .name = Export[0].symbol_name }); - const jsonRejectRequestStream = JSC.toJSHostFunction(onUploadStreamRejectRequestStream); - @export(jsonRejectRequestStream, .{ .name = Export[1].symbol_name }); - } - - /// consumes the readable stream and upload to s3 - pub fn s3UploadStream(this: *@This(), path: []const u8, readable_stream: JSC.WebCore.ReadableStream, globalThis: *JSC.JSGlobalObject, options: MultiPartUpload.MultiPartUploadOptions, acl: ?ACL, content_type: ?[]const u8, proxy: ?[]const u8, callback: ?*const fn (S3UploadResult, *anyopaque) void, callback_context: *anyopaque) JSC.JSValue { - this.ref(); // ref the credentials - const proxy_url = (proxy orelse ""); - - if (readable_stream.isDisturbed(globalThis)) { - return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is already disturbed").toErrorInstance(globalThis)); - } - - switch (readable_stream.ptr) { - .Invalid => { - return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is invalid").toErrorInstance(globalThis)); - }, - inline .File, .Bytes => |stream| { - if (stream.pending.result == .err) { - // we got an error, fail early - const err = stream.pending.result.err; - stream.pending = .{ .result = .{ .done = {} } }; - const js_err, const was_strong = err.toJSWeak(globalThis); - if (was_strong == .Strong) { - js_err.unprotect(); - } - js_err.ensureStillAlive(); - return JSC.JSPromise.rejectedPromise(globalThis, js_err).asValue(globalThis); - } - }, - else => {}, - } - - const task = MultiPartUpload.new(.{ - .credentials = this, - .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), - .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", - .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, - .callback = @ptrCast(&S3UploadStreamWrapper.resolve), - .callback_context = undefined, - .globalThis = globalThis, - .state = .wait_stream_check, - .options = options, - .acl = acl, - .vm = JSC.VirtualMachine.get(), - }); - - task.poll_ref.ref(task.vm); - - task.ref(); // + 1 for the stream sink - - var response_stream = JSC.WebCore.NetworkSink.new(.{ - .task = .{ .s3_upload = task }, - .buffer = .{}, - .globalThis = globalThis, - .encoded = false, - .endPromise = JSC.JSPromise.Strong.init(globalThis), - }).toSink(); - task.ref(); // + 1 for the stream wrapper - - const endPromise = response_stream.sink.endPromise.value(); - const ctx = S3UploadStreamWrapper.new(.{ - .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable_stream, globalThis), - .sink = &response_stream.sink, - .callback = callback, - .callback_context = callback_context, - .path = task.path, - .task = task, - }); - task.callback_context = @ptrCast(ctx); - // keep the task alive until we are done configuring the signal - task.ref(); - defer task.deref(); - - var signal = &response_stream.sink.signal; - - signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); - - // explicitly set it to a dead pointer - // we use this memory address to disable signals being sent - signal.clear(); - bun.assert(signal.isDead()); - - // We are already corked! - const assignment_result: JSC.JSValue = JSC.WebCore.NetworkSink.JSSink.assignToStream( - globalThis, - readable_stream.value, - response_stream, - @as(**anyopaque, @ptrCast(&signal.ptr)), - ); - - assignment_result.ensureStillAlive(); - - // assert that it was updated - bun.assert(!signal.isDead()); - - if (assignment_result.toError()) |err| { - if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.reject(globalThis, err); - } - - task.fail(.{ - .code = "UnknownError", - .message = "ReadableStream ended with an error", - }); - readable_stream.cancel(globalThis); - return endPromise; - } - - if (!assignment_result.isEmptyOrUndefinedOrNull()) { - assignment_result.ensureStillAlive(); - // it returns a Promise when it goes through ReadableStreamDefaultReader - if (assignment_result.asAnyPromise()) |promise| { - switch (promise.status(globalThis.vm())) { - .pending => { - // if we eended and its not canceled the promise is the endPromise - // because assignToStream can return the sink.end() promise - // we set the endPromise in the NetworkSink so we need to resolve it - if (response_stream.sink.ended and !response_stream.sink.cancel) { - task.continueStream(); - - readable_stream.done(globalThis); - return endPromise; - } - ctx.ref(); - - assignment_result.then( - globalThis, - task.callback_context, - onUploadStreamResolveRequestStream, - onUploadStreamRejectRequestStream, - ); - // we need to wait the promise to resolve because can be an error/cancel here - if (!task.ended) - task.continueStream(); - }, - .fulfilled => { - task.continueStream(); - - readable_stream.done(globalThis); - }, - .rejected => { - if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.reject(globalThis, promise.result(globalThis.vm())); - } - - task.fail(.{ - .code = "UnknownError", - .message = "ReadableStream ended with an error", - }); - readable_stream.cancel(globalThis); - }, - } - } else { - if (response_stream.sink.endPromise.hasValue()) { - response_stream.sink.endPromise.reject(globalThis, assignment_result); - } - - task.fail(.{ - .code = "UnknownError", - .message = "ReadableStream ended with an error", - }); - readable_stream.cancel(globalThis); - } - } - return endPromise; - } - /// returns a writable stream that writes to the s3 path - pub fn s3WritableStream(this: *@This(), path: []const u8, globalThis: *JSC.JSGlobalObject, options: MultiPartUpload.MultiPartUploadOptions, content_type: ?[]const u8, proxy: ?[]const u8) bun.JSError!JSC.JSValue { - const Wrapper = struct { - pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.NetworkSink) void { - if (sink.endPromise.hasValue()) { - if (sink.endPromise.globalObject()) |globalObject| { - const event_loop = globalObject.bunVM().eventLoop(); - event_loop.enter(); - defer event_loop.exit(); - switch (result) { - .success => { - sink.endPromise.resolve(globalObject, JSC.jsNumber(0)); - }, - .failure => |err| { - if (!sink.done) { - sink.abort(); - return; - } - - sink.endPromise.reject(globalObject, err.toJS(globalObject, sink.path())); - }, - } - } - } - sink.finalize(); - } - }; - const proxy_url = (proxy orelse ""); - this.ref(); // ref the credentials - const task = MultiPartUpload.new(.{ - .credentials = this, - .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), - .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", - .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, - - .callback = @ptrCast(&Wrapper.callback), - .callback_context = undefined, - .globalThis = globalThis, - .options = options, - .vm = JSC.VirtualMachine.get(), - }); - - task.poll_ref.ref(task.vm); - - task.ref(); // + 1 for the stream - var response_stream = JSC.WebCore.NetworkSink.new(.{ - .task = .{ .s3_upload = task }, - .buffer = .{}, - .globalThis = globalThis, - .encoded = false, - .endPromise = JSC.JSPromise.Strong.init(globalThis), - }).toSink(); - - task.callback_context = @ptrCast(response_stream); - var signal = &response_stream.sink.signal; - - signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); - - // explicitly set it to a dead pointer - // we use this memory address to disable signals being sent - signal.clear(); - bun.assert(signal.isDead()); - return response_stream.sink.toJS(globalThis); - } -}; - -pub const MultiPartUpload = struct { - pub const OneMiB: usize = 1048576; - pub const MAX_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5120; // we limit to 5 GiB - pub const MAX_SINGLE_UPLOAD_SIZE: usize = MAX_SINGLE_UPLOAD_SIZE_IN_MiB * OneMiB; // we limit to 5 GiB - pub const MIN_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5; - pub const DefaultPartSize = OneMiB * MIN_SINGLE_UPLOAD_SIZE_IN_MiB; - const MAX_QUEUE_SIZE = 64; // dont make sense more than this because we use fetch anything greater will be 64 - const AWS = AWSCredentials; - queue: std.ArrayListUnmanaged(UploadPart) = .{}, - available: bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE) = bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull(), - - currentPartNumber: u16 = 1, - ref_count: u16 = 1, - ended: bool = false, - - options: MultiPartUploadOptions = .{}, - acl: ?ACL = null, - credentials: *AWSCredentials, - poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), - vm: *JSC.VirtualMachine, - globalThis: *JSC.JSGlobalObject, - - buffered: std.ArrayListUnmanaged(u8) = .{}, - offset: usize = 0, - - path: []const u8, - proxy: []const u8, - content_type: ?[]const u8 = null, - upload_id: []const u8 = "", - uploadid_buffer: bun.MutableString = .{ .allocator = bun.default_allocator, .list = .{} }, - - multipart_etags: std.ArrayListUnmanaged(UploadPart.UploadPartResult) = .{}, - multipart_upload_list: bun.ByteList = .{}, - - state: enum { - wait_stream_check, - not_started, - multipart_started, - multipart_completed, - singlefile_started, - finished, - } = .not_started, - - callback: *const fn (AWS.S3UploadResult, *anyopaque) void, - callback_context: *anyopaque, - - pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); - - const log = bun.Output.scoped(.S3MultiPartUpload, true); - pub const MultiPartUploadOptions = struct { - /// more than 255 dont make sense http thread cannot handle more than that - queueSize: u8 = 5, - /// in s3 client sdk they set it in bytes but the min is still 5 MiB - /// var params = {Bucket: 'bucket', Key: 'key', Body: stream}; - /// var options = {partSize: 10 * 1024 * 1024, queueSize: 1}; - /// s3.upload(params, options, function(err, data) { - /// console.log(err, data); - /// }); - /// See. https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property - /// The value is in MiB min is 5 and max 5120 (but we limit to 4 GiB aka 4096) - partSize: u16 = 5, - /// default is 3 max 255 - retry: u8 = 3, - }; - - pub const UploadPart = struct { - data: []const u8, - state: enum { - pending, - started, - completed, - canceled, - }, - owns_data: bool, - partNumber: u16, // max is 10,000 - retry: u8, // auto retry, decrement until 0 and fail after this - index: u8, - ctx: *MultiPartUpload, - - pub const UploadPartResult = struct { - number: u16, - etag: []const u8, - }; - fn sortEtags(_: *MultiPartUpload, a: UploadPart.UploadPartResult, b: UploadPart.UploadPartResult) bool { - return a.number < b.number; - } - - pub fn onPartResponse(result: AWS.S3PartResult, this: *@This()) void { - if (this.state == .canceled or this.ctx.state == .finished) { - log("onPartResponse {} canceled", .{this.partNumber}); - if (this.owns_data) bun.default_allocator.free(this.data); - this.ctx.deref(); - return; - } - - this.state = .completed; - - switch (result) { - .failure => |err| { - if (this.retry > 0) { - log("onPartResponse {} retry", .{this.partNumber}); - this.retry -= 1; - // retry failed - this.perform(); - return; - } else { - log("onPartResponse {} failed", .{this.partNumber}); - if (this.owns_data) bun.default_allocator.free(this.data); - defer this.ctx.deref(); - return this.ctx.fail(err); - } - }, - .etag => |etag| { - log("onPartResponse {} success", .{this.partNumber}); - - if (this.owns_data) bun.default_allocator.free(this.data); - // we will need to order this - this.ctx.multipart_etags.append(bun.default_allocator, .{ - .number = this.partNumber, - .etag = bun.default_allocator.dupe(u8, etag) catch bun.outOfMemory(), - }) catch bun.outOfMemory(); - - defer this.ctx.deref(); - // mark as available - this.ctx.available.set(this.index); - // drain more - this.ctx.drainEnqueuedParts(); - }, - } - } - - fn perform(this: *@This()) void { - var params_buffer: [2048]u8 = undefined; - const search_params = std.fmt.bufPrint(¶ms_buffer, "?partNumber={}&uploadId={s}&x-id=UploadPart", .{ - this.partNumber, - this.ctx.upload_id, - }) catch unreachable; - this.ctx.credentials.executeSimpleS3Request(.{ - .path = this.ctx.path, - .method = .PUT, - .proxy_url = this.ctx.proxyUrl(), - .body = this.data, - .search_params = search_params, - }, .{ .part = @ptrCast(&onPartResponse) }, this); - } - pub fn start(this: *@This()) void { - if (this.state != .pending or this.ctx.state != .multipart_completed or this.ctx.state == .finished) return; - this.ctx.ref(); - this.state = .started; - this.perform(); - } - pub fn cancel(this: *@This()) void { - const state = this.state; - this.state = .canceled; - - switch (state) { - .pending => { - if (this.owns_data) bun.default_allocator.free(this.data); - }, - // if is not pending we will free later or is already freed - else => {}, - } - } - }; - - fn deinit(this: *@This()) void { - log("deinit", .{}); - if (this.queue.capacity > 0) - this.queue.deinit(bun.default_allocator); - this.poll_ref.unref(this.vm); - bun.default_allocator.free(this.path); - if (this.proxy.len > 0) { - bun.default_allocator.free(this.proxy); - } - if (this.content_type) |ct| { - if (ct.len > 0) { - bun.default_allocator.free(ct); - } - } - this.credentials.deref(); - this.uploadid_buffer.deinit(); - for (this.multipart_etags.items) |tag| { - bun.default_allocator.free(tag.etag); - } - if (this.multipart_etags.capacity > 0) - this.multipart_etags.deinit(bun.default_allocator); - if (this.multipart_upload_list.cap > 0) - this.multipart_upload_list.deinitWithAllocator(bun.default_allocator); - this.destroy(); - } - - pub fn singleSendUploadResponse(result: AWS.S3UploadResult, this: *@This()) void { - defer this.deref(); - if (this.state == .finished) return; - switch (result) { - .failure => |err| { - if (this.options.retry > 0) { - log("singleSendUploadResponse {} retry", .{this.options.retry}); - this.options.retry -= 1; - this.ref(); - // retry failed - this.credentials.executeSimpleS3Request(.{ - .path = this.path, - .method = .PUT, - .proxy_url = this.proxyUrl(), - .body = this.buffered.items, - .content_type = this.content_type, - .acl = this.acl, - }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); - - return; - } else { - log("singleSendUploadResponse failed", .{}); - return this.fail(err); - } - }, - .success => { - log("singleSendUploadResponse success", .{}); - this.done(); - }, - } - } - - fn getCreatePart(this: *@This(), chunk: []const u8, owns_data: bool) ?*UploadPart { - const index = this.available.findFirstSet() orelse { - // this means that the queue is full and we cannot flush it - return null; - }; - - if (index >= this.options.queueSize) { - // ops too much concurrency wait more - return null; - } - this.available.unset(index); - defer this.currentPartNumber += 1; - - if (this.queue.items.len <= index) { - this.queue.append(bun.default_allocator, .{ - .data = chunk, - .partNumber = this.currentPartNumber, - .owns_data = owns_data, - .ctx = this, - .index = @truncate(index), - .retry = this.options.retry, - .state = .pending, - }) catch bun.outOfMemory(); - return &this.queue.items[index]; - } - this.queue.items[index] = .{ - .data = chunk, - .partNumber = this.currentPartNumber, - .owns_data = owns_data, - .ctx = this, - .index = @truncate(index), - .retry = this.options.retry, - .state = .pending, - }; - return &this.queue.items[index]; - } - - fn drainEnqueuedParts(this: *@This()) void { - if (this.state == .finished) { - return; - } - // check pending to start or transformed buffered ones into tasks - if (this.state == .multipart_completed) { - for (this.queue.items) |*part| { - if (part.state == .pending) { - // lets start the part request - part.start(); - } - } - } - const partSize = this.partSizeInBytes(); - if (this.ended or this.buffered.items.len >= partSize) { - this.processMultiPart(partSize); - } - - if (this.ended and this.available.mask == std.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull().mask) { - // we are done - this.done(); - } - } - pub fn fail(this: *@This(), _err: AWS.S3Error) void { - log("fail {s}:{s}", .{ _err.code, _err.message }); - this.ended = true; - for (this.queue.items) |*task| { - task.cancel(); - } - if (this.state != .finished) { - const old_state = this.state; - this.state = .finished; - this.callback(.{ .failure = _err }, this.callback_context); - - if (old_state == .multipart_completed) { - // will deref after rollback - this.rollbackMultiPartRequest(); - } else { - this.deref(); - } - } - } - - fn done(this: *@This()) void { - if (this.state == .multipart_completed) { - this.state = .finished; - - std.sort.block(UploadPart.UploadPartResult, this.multipart_etags.items, this, UploadPart.sortEtags); - this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); - for (this.multipart_etags.items) |tag| { - this.multipart_upload_list.appendFmt(bun.default_allocator, "{}{s}", .{ tag.number, tag.etag }) catch bun.outOfMemory(); - - bun.default_allocator.free(tag.etag); - } - this.multipart_etags.deinit(bun.default_allocator); - this.multipart_etags = .{}; - this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); - // will deref and ends after commit - this.commitMultiPartRequest(); - } else { - this.callback(.{ .success = {} }, this.callback_context); - this.state = .finished; - this.deref(); - } - } - pub fn startMultiPartRequestResult(result: AWS.S3DownloadResult, this: *@This()) void { - defer this.deref(); - if (this.state == .finished) return; - switch (result) { - .failure => |err| { - log("startMultiPartRequestResult {s} failed {s}: {s}", .{ this.path, err.message, err.message }); - this.fail(err); - }, - .success => |response| { - const slice = response.body.list.items; - this.uploadid_buffer = result.success.body; - - if (strings.indexOf(slice, "")) |start| { - if (strings.indexOf(slice, "")) |end| { - this.upload_id = slice[start + 10 .. end]; - } - } - if (this.upload_id.len == 0) { - // Unknown type of response error from AWS - log("startMultiPartRequestResult {s} failed invalid id", .{this.path}); - this.fail(.{ - .code = "UnknownError", - .message = "Failed to initiate multipart upload", - }); - return; - } - log("startMultiPartRequestResult {s} success id: {s}", .{ this.path, this.upload_id }); - this.state = .multipart_completed; - this.drainEnqueuedParts(); - }, - // this is "unreachable" but we cover in case AWS returns 404 - .not_found => this.fail(.{ - .code = "UnknownError", - .message = "Failed to initiate multipart upload", - }), - } - } - - pub fn onCommitMultiPartRequest(result: AWS.S3CommitResult, this: *@This()) void { - log("onCommitMultiPartRequest {s}", .{this.upload_id}); - - switch (result) { - .failure => |err| { - if (this.options.retry > 0) { - this.options.retry -= 1; - // retry commit - this.commitMultiPartRequest(); - return; - } - this.callback(.{ .failure = err }, this.callback_context); - this.deref(); - }, - .success => { - this.callback(.{ .success = {} }, this.callback_context); - this.state = .finished; - this.deref(); - }, - } - } - - pub fn onRollbackMultiPartRequest(result: AWS.S3UploadResult, this: *@This()) void { - log("onRollbackMultiPartRequest {s}", .{this.upload_id}); - switch (result) { - .failure => { - if (this.options.retry > 0) { - this.options.retry -= 1; - // retry rollback - this.rollbackMultiPartRequest(); - return; - } - this.deref(); - }, - .success => { - this.deref(); - }, - } - } - - fn commitMultiPartRequest(this: *@This()) void { - log("commitMultiPartRequest {s}", .{this.upload_id}); - var params_buffer: [2048]u8 = undefined; - const searchParams = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ - this.upload_id, - }) catch unreachable; - - this.credentials.executeSimpleS3Request(.{ - .path = this.path, - .method = .POST, - .proxy_url = this.proxyUrl(), - .body = this.multipart_upload_list.slice(), - .search_params = searchParams, - }, .{ .commit = @ptrCast(&onCommitMultiPartRequest) }, this); - } - fn rollbackMultiPartRequest(this: *@This()) void { - log("rollbackMultiPartRequest {s}", .{this.upload_id}); - var params_buffer: [2048]u8 = undefined; - const search_params = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ - this.upload_id, - }) catch unreachable; - - this.credentials.executeSimpleS3Request(.{ - .path = this.path, - .method = .DELETE, - .proxy_url = this.proxyUrl(), - .body = "", - .search_params = search_params, - }, .{ .upload = @ptrCast(&onRollbackMultiPartRequest) }, this); - } - fn enqueuePart(this: *@This(), chunk: []const u8, owns_data: bool) bool { - const part = this.getCreatePart(chunk, owns_data) orelse return false; - - if (this.state == .not_started) { - // will auto start later - this.state = .multipart_started; - this.ref(); - this.credentials.executeSimpleS3Request(.{ - .path = this.path, - .method = .POST, - .proxy_url = this.proxyUrl(), - .body = "", - .search_params = "?uploads=", - .content_type = this.content_type, - .acl = this.acl, - }, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this); - } else if (this.state == .multipart_completed) { - part.start(); - } - return true; - } - - fn processMultiPart(this: *@This(), part_size: usize) void { - // need to split in multiple parts because of the size - var buffer = this.buffered.items[this.offset..]; - var queue_full = false; - defer if (!this.ended and queue_full == false) { - this.buffered = .{}; - this.offset = 0; - }; - - while (buffer.len > 0) { - const len = @min(part_size, buffer.len); - const slice = buffer[0..len]; - buffer = buffer[len..]; - // its one big buffer lets free after we are done with everything, part dont own the data - if (this.enqueuePart(slice, this.ended)) { - this.offset += len; - } else { - queue_full = true; - break; - } - } - } - - pub fn proxyUrl(this: *@This()) ?[]const u8 { - return this.proxy; - } - fn processBuffered(this: *@This(), part_size: usize) void { - if (this.ended and this.buffered.items.len < this.partSizeInBytes() and this.state == .not_started) { - log("processBuffered {s} singlefile_started", .{this.path}); - this.state = .singlefile_started; - this.ref(); - // we can do only 1 request - this.credentials.executeSimpleS3Request(.{ - .path = this.path, - .method = .PUT, - .proxy_url = this.proxyUrl(), - .body = this.buffered.items, - .content_type = this.content_type, - .acl = this.acl, - }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); - } else { - // we need to split - this.processMultiPart(part_size); - } - } - - pub fn partSizeInBytes(this: *@This()) usize { - return this.options.partSize * OneMiB; - } - - pub fn continueStream(this: *@This()) void { - if (this.state == .wait_stream_check) { - this.state = .not_started; - if (this.ended) { - this.processBuffered(this.partSizeInBytes()); - } - } - } - - pub fn sendRequestData(this: *@This(), chunk: []const u8, is_last: bool) void { - if (this.ended) return; - if (this.state == .wait_stream_check and chunk.len == 0 and is_last) { - // we do this because stream will close if the file dont exists and we dont wanna to send an empty part in this case - this.ended = true; - return; - } - if (is_last) { - this.ended = true; - if (chunk.len > 0) { - this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); - } - this.processBuffered(this.partSizeInBytes()); - } else { - // still have more data and receive empty, nothing todo here - if (chunk.len == 0) return; - this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); - const partSize = this.partSizeInBytes(); - if (this.buffered.items.len >= partSize) { - // send the part we have enough data - this.processBuffered(partSize); - return; - } - - // wait for more - } - } -}; diff --git a/src/s3/acl.zig b/src/s3/acl.zig new file mode 100644 index 0000000000..2d69bed30d --- /dev/null +++ b/src/s3/acl.zig @@ -0,0 +1,43 @@ +const bun = @import("root").bun; + +pub const ACL = enum { + /// Owner gets FULL_CONTROL. No one else has access rights (default). + private, + /// Owner gets FULL_CONTROL. The AllUsers group (see Who is a grantee?) gets READ access. + public_read, + /// Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended. + public_read_write, + /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3. + aws_exec_read, + /// Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access. + authenticated_read, + /// Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_read, + /// Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it. + bucket_owner_full_control, + log_delivery_write, + + pub fn toString(this: @This()) []const u8 { + return switch (this) { + .private => "private", + .public_read => "public-read", + .public_read_write => "public-read-write", + .aws_exec_read => "aws-exec-read", + .authenticated_read => "authenticated-read", + .bucket_owner_read => "bucket-owner-read", + .bucket_owner_full_control => "bucket-owner-full-control", + .log_delivery_write => "log-delivery-write", + }; + } + + pub const Map = bun.ComptimeStringMap(ACL, .{ + .{ "private", .private }, + .{ "public-read", .public_read }, + .{ "public-read-write", .public_read_write }, + .{ "aws-exec-read", .aws_exec_read }, + .{ "authenticated-read", .authenticated_read }, + .{ "bucket-owner-read", .bucket_owner_read }, + .{ "bucket-owner-full-control", .bucket_owner_full_control }, + .{ "log-delivery-write", .log_delivery_write }, + }); +}; diff --git a/src/s3/client.zig b/src/s3/client.zig new file mode 100644 index 0000000000..684ae76ca4 --- /dev/null +++ b/src/s3/client.zig @@ -0,0 +1,629 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const picohttp = bun.picohttp; + +pub const ACL = @import("./acl.zig").ACL; +pub const S3HttpDownloadStreamingTask = @import("./download_stream.zig").S3HttpDownloadStreamingTask; +pub const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +pub const MultiPartUpload = @import("./multipart.zig").MultiPartUpload; + +pub const Error = @import("./error.zig"); +pub const throwSignError = Error.throwSignError; +pub const getJSSignError = Error.getJSSignError; + +const Credentials = @import("./credentials.zig"); +pub const S3Credentials = Credentials.S3Credentials; +pub const S3CredentialsWithOptions = Credentials.S3CredentialsWithOptions; + +const S3SimpleRequest = @import("./simple_request.zig"); +pub const S3HttpSimpleTask = S3SimpleRequest.S3HttpSimpleTask; +pub const S3UploadResult = S3SimpleRequest.S3UploadResult; +pub const S3StatResult = S3SimpleRequest.S3StatResult; +pub const S3DownloadResult = S3SimpleRequest.S3DownloadResult; +pub const S3DeleteResult = S3SimpleRequest.S3DeleteResult; + +pub fn stat( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3StatResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .HEAD, + .proxy_url = proxy_url, + .body = "", + }, .{ .stat = callback }, callback_context); +} + +pub fn download( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3DownloadResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .GET, + .proxy_url = proxy_url, + .body = "", + }, .{ .download = callback }, callback_context); +} + +pub fn downloadSlice( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + callback: *const fn (S3DownloadResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + const range = brk: { + if (size) |size_| { + if (offset == 0) break :brk null; + + var end = (offset + size_); + if (size_ > 0) { + end -= 1; + } + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); + } + if (offset == 0) break :brk null; + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); + }; + + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .GET, + .proxy_url = proxy_url, + .body = "", + .range = range, + }, .{ .download = callback }, callback_context); +} + +pub fn delete( + this: *S3Credentials, + path: []const u8, + callback: *const fn (S3DeleteResult, *anyopaque) void, + callback_context: *anyopaque, + proxy_url: ?[]const u8, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .DELETE, + .proxy_url = proxy_url, + .body = "", + }, .{ .delete = callback }, callback_context); +} + +pub fn upload( + this: *S3Credentials, + path: []const u8, + content: []const u8, + content_type: ?[]const u8, + acl: ?ACL, + proxy_url: ?[]const u8, + callback: *const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, +) void { + S3SimpleRequest.executeSimpleS3Request(this, .{ + .path = path, + .method = .PUT, + .proxy_url = proxy_url, + .body = content, + .content_type = content_type, + .acl = acl, + }, .{ .upload = callback }, callback_context); +} +/// returns a writable stream that writes to the s3 path +pub fn writableStream( + this: *S3Credentials, + path: []const u8, + globalThis: *JSC.JSGlobalObject, + options: MultiPartUploadOptions, + content_type: ?[]const u8, + proxy: ?[]const u8, +) bun.JSError!JSC.JSValue { + const Wrapper = struct { + pub fn callback(result: S3UploadResult, sink: *JSC.WebCore.NetworkSink) void { + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + const event_loop = globalObject.bunVM().eventLoop(); + event_loop.enter(); + defer event_loop.exit(); + switch (result) { + .success => { + sink.endPromise.resolve(globalObject, JSC.jsNumber(0)); + }, + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } + + sink.endPromise.reject(globalObject, err.toJS(globalObject, sink.path())); + }, + } + } + } + sink.finalize(); + } + }; + const proxy_url = (proxy orelse ""); + this.ref(); // ref the credentials + const task = MultiPartUpload.new(.{ + .credentials = this, + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", + .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, + + .callback = @ptrCast(&Wrapper.callback), + .callback_context = undefined, + .globalThis = globalThis, + .options = options, + .vm = JSC.VirtualMachine.get(), + }); + + task.poll_ref.ref(task.vm); + + task.ref(); // + 1 for the stream + var response_stream = JSC.WebCore.NetworkSink.new(.{ + .task = .{ .s3_upload = task }, + .buffer = .{}, + .globalThis = globalThis, + .encoded = false, + .endPromise = JSC.JSPromise.Strong.init(globalThis), + }).toSink(); + + task.callback_context = @ptrCast(response_stream); + var signal = &response_stream.sink.signal; + + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + return response_stream.sink.toJS(globalThis); +} + +const S3UploadStreamWrapper = struct { + readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + sink: *JSC.WebCore.NetworkSink, + task: *MultiPartUpload, + callback: ?*const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, + ref_count: u32 = 1, + path: []const u8, // this is owned by the task not by the wrapper + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + pub fn resolve(result: S3UploadResult, self: *@This()) void { + const sink = self.sink; + defer self.deref(); + if (sink.endPromise.hasValue()) { + if (sink.endPromise.globalObject()) |globalObject| { + switch (result) { + .success => sink.endPromise.resolve(globalObject, JSC.jsNumber(0)), + .failure => |err| { + if (!sink.done) { + sink.abort(); + return; + } + sink.endPromise.reject(globalObject, err.toJS(globalObject, self.path)); + }, + } + } + } + if (self.callback) |callback| { + callback(result, self.callback_context); + } + } + + pub fn deinit(self: *@This()) void { + self.readable_stream_ref.deinit(); + self.sink.finalize(); + self.sink.destroy(); + self.task.deref(); + self.destroy(); + } +}; + +pub fn onUploadStreamResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); + defer this.deref(); + + if (this.readable_stream_ref.get()) |stream| { + stream.done(globalThis); + } + this.readable_stream_ref.deinit(); + this.task.continueStream(); + + return .undefined; +} + +pub fn onUploadStreamRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(S3UploadStreamWrapper); + defer this.deref(); + + const err = args.ptr[0]; + if (this.sink.endPromise.hasValue()) { + this.sink.endPromise.reject(globalThis, err); + } + + if (this.readable_stream_ref.get()) |stream| { + stream.cancel(globalThis); + this.readable_stream_ref.deinit(); + } + if (this.sink.task) |task| { + if (task == .s3_upload) { + task.s3_upload.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + } + } + this.task.continueStream(); + + return .undefined; +} +pub const shim = JSC.Shimmer("Bun", "S3UploadStream", @This()); + +pub const Export = shim.exportFunctions(.{ + .onResolveRequestStream = onUploadStreamResolveRequestStream, + .onRejectRequestStream = onUploadStreamRejectRequestStream, +}); +comptime { + const jsonResolveRequestStream = JSC.toJSHostFunction(onUploadStreamResolveRequestStream); + @export(jsonResolveRequestStream, .{ .name = Export[0].symbol_name }); + const jsonRejectRequestStream = JSC.toJSHostFunction(onUploadStreamRejectRequestStream); + @export(jsonRejectRequestStream, .{ .name = Export[1].symbol_name }); +} + +/// consumes the readable stream and upload to s3 +pub fn uploadStream( + this: *S3Credentials, + path: []const u8, + readable_stream: JSC.WebCore.ReadableStream, + globalThis: *JSC.JSGlobalObject, + options: MultiPartUploadOptions, + acl: ?ACL, + content_type: ?[]const u8, + proxy: ?[]const u8, + callback: ?*const fn (S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, +) JSC.JSValue { + this.ref(); // ref the credentials + const proxy_url = (proxy orelse ""); + + if (readable_stream.isDisturbed(globalThis)) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is already disturbed").toErrorInstance(globalThis)); + } + + switch (readable_stream.ptr) { + .Invalid => { + return JSC.JSPromise.rejectedPromiseValue(globalThis, bun.String.static("ReadableStream is invalid").toErrorInstance(globalThis)); + }, + inline .File, .Bytes => |stream| { + if (stream.pending.result == .err) { + // we got an error, fail early + const err = stream.pending.result.err; + stream.pending = .{ .result = .{ .done = {} } }; + const js_err, const was_strong = err.toJSWeak(globalThis); + if (was_strong == .Strong) { + js_err.unprotect(); + } + js_err.ensureStillAlive(); + return JSC.JSPromise.rejectedPromise(globalThis, js_err).asValue(globalThis); + } + }, + else => {}, + } + + const task = MultiPartUpload.new(.{ + .credentials = this, + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .proxy = if (proxy_url.len > 0) bun.default_allocator.dupe(u8, proxy_url) catch bun.outOfMemory() else "", + .content_type = if (content_type) |ct| bun.default_allocator.dupe(u8, ct) catch bun.outOfMemory() else null, + .callback = @ptrCast(&S3UploadStreamWrapper.resolve), + .callback_context = undefined, + .globalThis = globalThis, + .state = .wait_stream_check, + .options = options, + .acl = acl, + .vm = JSC.VirtualMachine.get(), + }); + + task.poll_ref.ref(task.vm); + + task.ref(); // + 1 for the stream sink + + var response_stream = JSC.WebCore.NetworkSink.new(.{ + .task = .{ .s3_upload = task }, + .buffer = .{}, + .globalThis = globalThis, + .encoded = false, + .endPromise = JSC.JSPromise.Strong.init(globalThis), + }).toSink(); + task.ref(); // + 1 for the stream wrapper + + const endPromise = response_stream.sink.endPromise.value(); + const ctx = S3UploadStreamWrapper.new(.{ + .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable_stream, globalThis), + .sink = &response_stream.sink, + .callback = callback, + .callback_context = callback_context, + .path = task.path, + .task = task, + }); + task.callback_context = @ptrCast(ctx); + // keep the task alive until we are done configuring the signal + task.ref(); + defer task.deref(); + + var signal = &response_stream.sink.signal; + + signal.* = JSC.WebCore.NetworkSink.JSSink.SinkSignal.init(.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + + // We are already corked! + const assignment_result: JSC.JSValue = JSC.WebCore.NetworkSink.JSSink.assignToStream( + globalThis, + readable_stream.value, + response_stream, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + bun.assert(!signal.isDead()); + + if (assignment_result.toError()) |err| { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, err); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + return endPromise; + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + switch (promise.status(globalThis.vm())) { + .pending => { + // if we eended and its not canceled the promise is the endPromise + // because assignToStream can return the sink.end() promise + // we set the endPromise in the NetworkSink so we need to resolve it + if (response_stream.sink.ended and !response_stream.sink.cancel) { + task.continueStream(); + + readable_stream.done(globalThis); + return endPromise; + } + ctx.ref(); + + assignment_result.then( + globalThis, + task.callback_context, + onUploadStreamResolveRequestStream, + onUploadStreamRejectRequestStream, + ); + // we need to wait the promise to resolve because can be an error/cancel here + if (!task.ended) + task.continueStream(); + }, + .fulfilled => { + task.continueStream(); + + readable_stream.done(globalThis); + }, + .rejected => { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, promise.result(globalThis.vm())); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + }, + } + } else { + if (response_stream.sink.endPromise.hasValue()) { + response_stream.sink.endPromise.reject(globalThis, assignment_result); + } + + task.fail(.{ + .code = "UnknownError", + .message = "ReadableStream ended with an error", + }); + readable_stream.cancel(globalThis); + } + } + return endPromise; +} + +/// download a file from s3 chunk by chunk aka streaming (used on readableStream) +pub fn downloadStream( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + proxy_url: ?[]const u8, + callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?Error.S3Error, *anyopaque) void, + callback_context: *anyopaque, +) void { + const range = brk: { + if (size) |size_| { + if (offset == 0) break :brk null; + + var end = (offset + size_); + if (size_ > 0) { + end -= 1; + } + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-{}", .{ offset, end }) catch bun.outOfMemory(); + } + if (offset == 0) break :brk null; + break :brk std.fmt.allocPrint(bun.default_allocator, "bytes={}-", .{offset}) catch bun.outOfMemory(); + }; + + var result = this.signRequest(.{ + .path = path, + .method = .GET, + }, null) catch |sign_err| { + if (range) |range_| bun.default_allocator.free(range_); + const error_code_and_message = Error.getSignErrorCodeAndMessage(sign_err); + callback(.{ .allocator = bun.default_allocator, .list = .{} }, false, .{ + .code = error_code_and_message.code, + .message = error_code_and_message.message, + }, callback_context); + return; + }; + + var header_buffer: [10]picohttp.Header = undefined; + const headers = brk: { + if (range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } else { + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); + } + }; + const proxy = proxy_url orelse ""; + const owned_proxy = if (proxy.len > 0) bun.default_allocator.dupe(u8, proxy) catch bun.outOfMemory() else ""; + const task = S3HttpDownloadStreamingTask.new(.{ + .http = undefined, + .sign_result = result, + .proxy_url = owned_proxy, + .callback_context = callback_context, + .callback = callback, + .range = range, + .headers = headers, + .vm = JSC.VirtualMachine.get(), + }); + task.poll_ref.ref(task.vm); + + const url = bun.URL.parse(result.url); + + task.signals = task.signal_store.to(); + + task.http = bun.http.AsyncHTTP.init( + bun.default_allocator, + .GET, + url, + task.headers.entries, + task.headers.buf.items, + &task.response_buffer, + "", + bun.http.HTTPClientResult.Callback.New( + *S3HttpDownloadStreamingTask, + S3HttpDownloadStreamingTask.httpCallback, + ).init(task), + .follow, + .{ + .http_proxy = if (owned_proxy.len > 0) bun.URL.parse(owned_proxy) else null, + .verbose = task.vm.getVerboseFetch(), + .signals = task.signals, + .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), + }, + ); + // enable streaming + task.http.enableBodyStreaming(); + // queue http request + bun.http.HTTPThread.init(&.{}); + var batch = bun.ThreadPool.Batch{}; + task.http.schedule(bun.default_allocator, &batch); + bun.http.http_thread.schedule(batch); +} + +/// returns a readable stream that reads from the s3 path +pub fn readableStream( + this: *S3Credentials, + path: []const u8, + offset: usize, + size: ?usize, + proxy_url: ?[]const u8, + globalThis: *JSC.JSGlobalObject, +) JSC.JSValue { + var reader = JSC.WebCore.ByteStream.Source.new(.{ + .context = undefined, + .globalThis = globalThis, + }); + + reader.context.setup(); + const readable_value = reader.toReadableStream(globalThis); + + const S3DownloadStreamWrapper = struct { + readable_stream_ref: JSC.WebCore.ReadableStream.Strong, + path: []const u8, + pub usingnamespace bun.New(@This()); + + pub fn callback(chunk: bun.MutableString, has_more: bool, request_err: ?Error.S3Error, self: *@This()) void { + defer if (!has_more) self.deinit(); + + if (self.readable_stream_ref.get()) |readable| { + if (readable.ptr == .Bytes) { + if (request_err) |err| { + readable.ptr.Bytes.onData( + .{ + .err = .{ + .JSValue = err.toJS(self.readable_stream_ref.globalThis().?, self.path), + }, + }, + bun.default_allocator, + ); + return; + } + if (has_more) { + readable.ptr.Bytes.onData( + .{ + .temporary = bun.ByteList.initConst(chunk.list.items), + }, + bun.default_allocator, + ); + return; + } + + readable.ptr.Bytes.onData( + .{ + .temporary_and_done = bun.ByteList.initConst(chunk.list.items), + }, + bun.default_allocator, + ); + return; + } + } + } + + pub fn deinit(self: *@This()) void { + self.readable_stream_ref.deinit(); + bun.default_allocator.free(self.path); + self.destroy(); + } + }; + + downloadStream(this, path, offset, size, proxy_url, @ptrCast(&S3DownloadStreamWrapper.callback), S3DownloadStreamWrapper.new(.{ + .readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(.{ + .ptr = .{ .Bytes = &reader.context }, + .value = readable_value, + }, globalThis), + .path = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + })); + return readable_value; +} diff --git a/src/s3/credentials.zig b/src/s3/credentials.zig new file mode 100644 index 0000000000..e053b681f3 --- /dev/null +++ b/src/s3/credentials.zig @@ -0,0 +1,775 @@ +const bun = @import("root").bun; +const picohttp = bun.picohttp; +const std = @import("std"); + +const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +const ACL = @import("./acl.zig").ACL; +const JSC = bun.JSC; +const RareData = JSC.RareData; +const strings = bun.strings; +const DotEnv = bun.DotEnv; + +pub const S3Credentials = struct { + accessKeyId: []const u8, + secretAccessKey: []const u8, + region: []const u8, + endpoint: []const u8, + bucket: []const u8, + sessionToken: []const u8, + + /// Important for MinIO support. + insecure_http: bool = false, + + ref_count: u32 = 1, + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + pub fn estimatedSize(this: *const @This()) usize { + return @sizeOf(S3Credentials) + this.accessKeyId.len + this.region.len + this.secretAccessKey.len + this.endpoint.len + this.bucket.len; + } + + fn hashConst(acl: []const u8) u64 { + var hasher = std.hash.Wyhash.init(0); + var remain = acl; + + var buf: [@sizeOf(@TypeOf(hasher.buf))]u8 = undefined; + + while (remain.len > 0) { + const end = @min(hasher.buf.len, remain.len); + + hasher.update(strings.copyLowercaseIfNeeded(remain[0..end], &buf)); + remain = remain[end..]; + } + + return hasher.final(); + } + pub fn getCredentialsWithOptions(this: S3Credentials, default_options: MultiPartUploadOptions, options: ?JSC.JSValue, default_acl: ?ACL, globalObject: *JSC.JSGlobalObject) bun.JSError!S3CredentialsWithOptions { + // get ENV config + var new_credentials = S3CredentialsWithOptions{ + .credentials = this, + .options = default_options, + .acl = default_acl, + }; + errdefer { + new_credentials.deinit(); + } + + if (options) |opts| { + if (opts.isObject()) { + if (try opts.getTruthyComptime(globalObject, "accessKeyId")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._accessKeyIdSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.accessKeyId = new_credentials._accessKeyIdSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("accessKeyId", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "secretAccessKey")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._secretAccessKeySlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.secretAccessKey = new_credentials._secretAccessKeySlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("secretAccessKey", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "region")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._regionSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.region = new_credentials._regionSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("region", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "endpoint")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._endpointSlice = str.toUTF8(bun.default_allocator); + const endpoint = new_credentials._endpointSlice.?.slice(); + const url = bun.URL.parse(endpoint); + const normalized_endpoint = url.host; + if (normalized_endpoint.len > 0) { + new_credentials.credentials.endpoint = normalized_endpoint; + + // Default to https:// + // Only use http:// if the endpoint specifically starts with 'http://' + new_credentials.credentials.insecure_http = url.isHTTP(); + + new_credentials.changed_credentials = true; + } else if (endpoint.len > 0) { + // endpoint is not a valid URL + return globalObject.throwInvalidArgumentTypeValue("endpoint", "string", js_value); + } + } + } else { + return globalObject.throwInvalidArgumentTypeValue("endpoint", "string", js_value); + } + } + } + if (try opts.getTruthyComptime(globalObject, "bucket")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._bucketSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.bucket = new_credentials._bucketSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); + } + } + } + + if (try opts.getTruthyComptime(globalObject, "sessionToken")) |js_value| { + if (!js_value.isEmptyOrUndefinedOrNull()) { + if (js_value.isString()) { + const str = bun.String.fromJS(js_value, globalObject); + defer str.deref(); + if (str.tag != .Empty and str.tag != .Dead) { + new_credentials._sessionTokenSlice = str.toUTF8(bun.default_allocator); + new_credentials.credentials.sessionToken = new_credentials._sessionTokenSlice.?.slice(); + new_credentials.changed_credentials = true; + } + } else { + return globalObject.throwInvalidArgumentTypeValue("bucket", "string", js_value); + } + } + } + + if (try opts.getOptional(globalObject, "pageSize", i32)) |pageSize| { + if (pageSize < MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB and pageSize > MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB) { + return globalObject.throwRangeError(pageSize, .{ + .min = @intCast(MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB), + .max = @intCast(MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB), + .field_name = "pageSize", + }); + } else { + new_credentials.options.partSize = @intCast(pageSize); + } + } + + if (try opts.getOptional(globalObject, "queueSize", i32)) |queueSize| { + if (queueSize < 1) { + return globalObject.throwRangeError(queueSize, .{ + .min = 1, + .field_name = "queueSize", + }); + } else { + new_credentials.options.queueSize = @intCast(@max(queueSize, std.math.maxInt(u8))); + } + } + + if (try opts.getOptional(globalObject, "retry", i32)) |retry| { + if (retry < 0 and retry > 255) { + return globalObject.throwRangeError(retry, .{ + .min = 0, + .max = 255, + .field_name = "retry", + }); + } else { + new_credentials.options.retry = @intCast(retry); + } + } + if (try opts.getOptionalEnum(globalObject, "acl", ACL)) |acl| { + new_credentials.acl = acl; + } + } + } + return new_credentials; + } + pub fn dupe(this: *const @This()) *S3Credentials { + return S3Credentials.new(.{ + .accessKeyId = if (this.accessKeyId.len > 0) + bun.default_allocator.dupe(u8, this.accessKeyId) catch bun.outOfMemory() + else + "", + + .secretAccessKey = if (this.secretAccessKey.len > 0) + bun.default_allocator.dupe(u8, this.secretAccessKey) catch bun.outOfMemory() + else + "", + + .region = if (this.region.len > 0) + bun.default_allocator.dupe(u8, this.region) catch bun.outOfMemory() + else + "", + + .endpoint = if (this.endpoint.len > 0) + bun.default_allocator.dupe(u8, this.endpoint) catch bun.outOfMemory() + else + "", + + .bucket = if (this.bucket.len > 0) + bun.default_allocator.dupe(u8, this.bucket) catch bun.outOfMemory() + else + "", + + .sessionToken = if (this.sessionToken.len > 0) + bun.default_allocator.dupe(u8, this.sessionToken) catch bun.outOfMemory() + else + "", + + .insecure_http = this.insecure_http, + }); + } + pub fn deinit(this: *@This()) void { + if (this.accessKeyId.len > 0) { + bun.default_allocator.free(this.accessKeyId); + } + if (this.secretAccessKey.len > 0) { + bun.default_allocator.free(this.secretAccessKey); + } + if (this.region.len > 0) { + bun.default_allocator.free(this.region); + } + if (this.endpoint.len > 0) { + bun.default_allocator.free(this.endpoint); + } + if (this.bucket.len > 0) { + bun.default_allocator.free(this.bucket); + } + if (this.sessionToken.len > 0) { + bun.default_allocator.free(this.sessionToken); + } + this.destroy(); + } + + const log = bun.Output.scoped(.AWS, false); + + const DateResult = struct { + // numeric representation of year, month and day (excluding time components) + numeric_day: u64, + date: []const u8, + }; + + fn getAMZDate(allocator: std.mem.Allocator) DateResult { + // We can also use Date.now() but would be slower and would add JSC dependency + // var buffer: [28]u8 = undefined; + // the code bellow is the same as new Date(Date.now()).toISOString() + // JSC.JSValue.getDateNowISOString(globalObject, &buffer); + + // Create UTC timestamp + const secs: u64 = @intCast(@divFloor(std.time.milliTimestamp(), 1000)); + const utc_seconds = std.time.epoch.EpochSeconds{ .secs = secs }; + const utc_day = utc_seconds.getEpochDay(); + const year_and_day = utc_day.calculateYearDay(); + const month_and_day = year_and_day.calculateMonthDay(); + // Get UTC date components + const year = year_and_day.year; + const day = @as(u32, month_and_day.day_index) + 1; // this starts in 0 + const month = month_and_day.month.numeric(); // starts in 1 + + // Get UTC time components + const time = utc_seconds.getDaySeconds(); + const hours = time.getHoursIntoDay(); + const minutes = time.getMinutesIntoHour(); + const seconds = time.getSecondsIntoMinute(); + + // Format the date + return .{ + .numeric_day = secs - time.secs, + .date = std.fmt.allocPrint(allocator, "{d:0>4}{d:0>2}{d:0>2}T{d:0>2}{d:0>2}{d:0>2}Z", .{ + year, + month, + day, + hours, + minutes, + seconds, + }) catch bun.outOfMemory(), + }; + } + + const DIGESTED_HMAC_256_LEN = 32; + pub const SignResult = struct { + amz_date: []const u8, + host: []const u8, + authorization: []const u8, + url: []const u8, + + content_disposition: []const u8 = "", + session_token: []const u8 = "", + acl: ?ACL = null, + _headers: [7]picohttp.Header = .{ + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + }, + _headers_len: u8 = 0, + + pub fn headers(this: *const @This()) []const picohttp.Header { + return this._headers[0..this._headers_len]; + } + + pub fn mixWithHeader(this: *const @This(), headers_buffer: []picohttp.Header, header: picohttp.Header) []const picohttp.Header { + // copy the headers to buffer + const len = this._headers_len; + for (this._headers[0..len], 0..len) |existing_header, i| { + headers_buffer[i] = existing_header; + } + headers_buffer[len] = header; + return headers_buffer[0 .. len + 1]; + } + + pub fn deinit(this: *const @This()) void { + if (this.amz_date.len > 0) { + bun.default_allocator.free(this.amz_date); + } + + if (this.session_token.len > 0) { + bun.default_allocator.free(this.session_token); + } + + if (this.content_disposition.len > 0) { + bun.default_allocator.free(this.content_disposition); + } + + if (this.host.len > 0) { + bun.default_allocator.free(this.host); + } + + if (this.authorization.len > 0) { + bun.default_allocator.free(this.authorization); + } + + if (this.url.len > 0) { + bun.default_allocator.free(this.url); + } + } + }; + + pub const SignQueryOptions = struct { + expires: usize = 86400, + }; + pub const SignOptions = struct { + path: []const u8, + method: bun.http.Method, + content_hash: ?[]const u8 = null, + search_params: ?[]const u8 = null, + content_disposition: ?[]const u8 = null, + acl: ?ACL = null, + }; + + pub fn guessRegion(endpoint: []const u8) []const u8 { + if (endpoint.len > 0) { + if (strings.endsWith(endpoint, ".r2.cloudflarestorage.com")) return "auto"; + if (strings.indexOf(endpoint, ".amazonaws.com")) |end| { + if (strings.indexOf(endpoint, "s3.")) |start| { + return endpoint[start + 3 .. end]; + } + } + // endpoint is informed but is not s3 so auto detect + return "auto"; + } + + // no endpoint so we default to us-east-1 because s3.us-east-1.amazonaws.com is the default endpoint + return "us-east-1"; + } + fn toHexChar(value: u8) !u8 { + return switch (value) { + 0...9 => value + '0', + 10...15 => (value - 10) + 'A', + else => error.InvalidHexChar, + }; + } + fn encodeURIComponent(input: []const u8, buffer: []u8, comptime encode_slash: bool) ![]const u8 { + var written: usize = 0; + + for (input) |c| { + switch (c) { + // RFC 3986 Unreserved Characters (do not encode) + 'A'...'Z', 'a'...'z', '0'...'9', '-', '_', '.', '~' => { + if (written >= buffer.len) return error.BufferTooSmall; + buffer[written] = c; + written += 1; + }, + // All other characters need to be percent-encoded + else => { + if (!encode_slash and (c == '/' or c == '\\')) { + if (written >= buffer.len) return error.BufferTooSmall; + buffer[written] = if (c == '\\') '/' else c; + written += 1; + continue; + } + if (written + 3 > buffer.len) return error.BufferTooSmall; + buffer[written] = '%'; + // Convert byte to hex + const high_nibble: u8 = (c >> 4) & 0xF; + const low_nibble: u8 = c & 0xF; + buffer[written + 1] = try toHexChar(high_nibble); + buffer[written + 2] = try toHexChar(low_nibble); + written += 3; + }, + } + } + + return buffer[0..written]; + } + + pub fn signRequest(this: *const @This(), signOptions: SignOptions, signQueryOption: ?SignQueryOptions) !SignResult { + const method = signOptions.method; + const request_path = signOptions.path; + const content_hash = signOptions.content_hash; + const search_params = signOptions.search_params; + + var content_disposition = signOptions.content_disposition; + if (content_disposition != null and content_disposition.?.len == 0) { + content_disposition = null; + } + const session_token: ?[]const u8 = if (this.sessionToken.len == 0) null else this.sessionToken; + + const acl: ?[]const u8 = if (signOptions.acl) |acl_value| acl_value.toString() else null; + + if (this.accessKeyId.len == 0 or this.secretAccessKey.len == 0) return error.MissingCredentials; + const signQuery = signQueryOption != null; + const expires = if (signQueryOption) |options| options.expires else 0; + const method_name = switch (method) { + .GET => "GET", + .POST => "POST", + .PUT => "PUT", + .DELETE => "DELETE", + .HEAD => "HEAD", + else => return error.InvalidMethod, + }; + + const region = if (this.region.len > 0) this.region else guessRegion(this.endpoint); + var full_path = request_path; + // handle \\ on bucket name + if (strings.startsWith(full_path, "/")) { + full_path = full_path[1..]; + } else if (strings.startsWith(full_path, "\\")) { + full_path = full_path[1..]; + } + + var path: []const u8 = full_path; + var bucket: []const u8 = this.bucket; + + if (bucket.len == 0) { + //TODO: r2 supports bucket in the endpoint + + // guess bucket using path + if (strings.indexOf(full_path, "/")) |end| { + if (strings.indexOf(full_path, "\\")) |backslash_index| { + if (backslash_index < end) { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; + } + } + bucket = full_path[0..end]; + path = full_path[end + 1 ..]; + } else if (strings.indexOf(full_path, "\\")) |backslash_index| { + bucket = full_path[0..backslash_index]; + path = full_path[backslash_index + 1 ..]; + } else { + return error.InvalidPath; + } + } + if (strings.endsWith(path, "/")) { + path = path[0..path.len]; + } else if (strings.endsWith(path, "\\")) { + path = path[0 .. path.len - 1]; + } + if (strings.startsWith(path, "/")) { + path = path[1..]; + } else if (strings.startsWith(path, "\\")) { + path = path[1..]; + } + + // if we allow path.len == 0 it will list the bucket for now we disallow + if (path.len == 0) return error.InvalidPath; + + var normalized_path_buffer: [1024 + 63 + 2]u8 = undefined; // 1024 max key size and 63 max bucket name + var path_buffer: [1024]u8 = undefined; + var bucket_buffer: [63]u8 = undefined; + bucket = encodeURIComponent(bucket, &bucket_buffer, false) catch return error.InvalidPath; + path = encodeURIComponent(path, &path_buffer, false) catch return error.InvalidPath; + const normalizedPath = std.fmt.bufPrint(&normalized_path_buffer, "/{s}/{s}", .{ bucket, path }) catch return error.InvalidPath; + + const date_result = getAMZDate(bun.default_allocator); + const amz_date = date_result.date; + errdefer bun.default_allocator.free(amz_date); + + const amz_day = amz_date[0..8]; + const signed_headers = if (signQuery) "host" else brk: { + if (acl != null) { + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-acl;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-content-sha256;x-amz-date"; + } + } + } else { + if (content_disposition != null) { + if (session_token != null) { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "content-disposition;host;x-amz-content-sha256;x-amz-date"; + } + } else { + if (session_token != null) { + break :brk "host;x-amz-content-sha256;x-amz-date;x-amz-security-token"; + } else { + break :brk "host;x-amz-content-sha256;x-amz-date"; + } + } + } + }; + + // Default to https. Only use http if they explicit pass "http://" as the endpoint. + const protocol = if (this.insecure_http) "http" else "https"; + + // detect service name and host from region or endpoint + var encoded_host_buffer: [512]u8 = undefined; + var encoded_host: []const u8 = ""; + const host = brk_host: { + if (this.endpoint.len > 0) { + encoded_host = encodeURIComponent(this.endpoint, &encoded_host_buffer, true) catch return error.InvalidEndpoint; + break :brk_host try bun.default_allocator.dupe(u8, this.endpoint); + } else { + break :brk_host try std.fmt.allocPrint(bun.default_allocator, "s3.{s}.amazonaws.com", .{region}); + } + }; + const service_name = "s3"; + + errdefer bun.default_allocator.free(host); + + const aws_content_hash = if (content_hash) |hash| hash else ("UNSIGNED-PAYLOAD"); + var tmp_buffer: [4096]u8 = undefined; + + const authorization = brk: { + // we hash the hash so we need 2 buffers + var hmac_sig_service: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; + var hmac_sig_service2: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; + + const sigDateRegionServiceReq = brk_sign: { + const key = try std.fmt.bufPrint(&tmp_buffer, "{s}{s}{s}", .{ region, service_name, this.secretAccessKey }); + var cache = (JSC.VirtualMachine.getMainThreadVM() orelse JSC.VirtualMachine.get()).rareData().awsCache(); + if (cache.get(date_result.numeric_day, key)) |cached| { + break :brk_sign cached; + } + // not cached yet lets generate a new one + const sigDate = bun.hmac.generate(try std.fmt.bufPrint(&tmp_buffer, "AWS4{s}", .{this.secretAccessKey}), amz_day, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + const sigDateRegion = bun.hmac.generate(sigDate, region, .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; + const sigDateRegionService = bun.hmac.generate(sigDateRegion, service_name, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + const result = bun.hmac.generate(sigDateRegionService, "aws4_request", .sha256, &hmac_sig_service2) orelse return error.FailedToGenerateSignature; + + cache.set(date_result.numeric_day, key, hmac_sig_service2[0..DIGESTED_HMAC_256_LEN].*); + break :brk_sign result; + }; + if (signQuery) { + var token_encoded_buffer: [2048]u8 = undefined; // token is normaly like 600-700 but can be up to 2k + var encoded_session_token: ?[]const u8 = null; + if (session_token) |token| { + encoded_session_token = encodeURIComponent(token, &token_encoded_buffer, true) catch return error.InvalidSessionToken; + } + const canonical = brk_canonical: { + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } + } else { + if (encoded_session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + } + } + }; + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + + const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); + + const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + if (acl) |acl_value| { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } else { + if (encoded_session_token) |token| { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } else { + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "{s}://{s}{s}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host&X-Amz-Signature={s}", + .{ protocol, host, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + } + } else { + var encoded_content_disposition_buffer: [255]u8 = undefined; + const encoded_content_disposition: []const u8 = if (content_disposition) |cd| encodeURIComponent(cd, &encoded_content_disposition_buffer, true) catch return error.ContentTypeIsTooLong else ""; + const canonical = brk_canonical: { + if (acl) |acl_value| { + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } + } else { + if (content_disposition != null) { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } else { + if (session_token) |token| { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + } else { + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + } + } + } + }; + var sha_digest = std.mem.zeroes(bun.sha.SHA256.Digest); + bun.sha.SHA256.hash(canonical, &sha_digest, JSC.VirtualMachine.get().rareData().boringEngine()); + + const signValue = try std.fmt.bufPrint(&tmp_buffer, "AWS4-HMAC-SHA256\n{s}\n{s}/{s}/{s}/aws4_request\n{s}", .{ amz_date, amz_day, region, service_name, bun.fmt.bytesToHex(sha_digest[0..bun.sha.SHA256.digest], .lower) }); + + const signature = bun.hmac.generate(sigDateRegionServiceReq, signValue, .sha256, &hmac_sig_service) orelse return error.FailedToGenerateSignature; + + break :brk try std.fmt.allocPrint( + bun.default_allocator, + "AWS4-HMAC-SHA256 Credential={s}/{s}/{s}/{s}/aws4_request, SignedHeaders={s}, Signature={s}", + .{ this.accessKeyId, amz_day, region, service_name, signed_headers, bun.fmt.bytesToHex(signature[0..DIGESTED_HMAC_256_LEN], .lower) }, + ); + } + }; + errdefer bun.default_allocator.free(authorization); + + if (signQuery) { + defer bun.default_allocator.free(host); + defer bun.default_allocator.free(amz_date); + + return SignResult{ + .amz_date = "", + .host = "", + .authorization = "", + .acl = signOptions.acl, + .url = authorization, + }; + } + + var result = SignResult{ + .amz_date = amz_date, + .host = host, + .authorization = authorization, + .acl = signOptions.acl, + .url = try std.fmt.allocPrint(bun.default_allocator, "{s}://{s}{s}{s}", .{ protocol, host, normalizedPath, if (search_params) |s| s else "" }), + ._headers = [_]picohttp.Header{ + .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, + .{ .name = "x-amz-date", .value = amz_date }, + .{ .name = "Authorization", .value = authorization[0..] }, + .{ .name = "Host", .value = host }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + .{ .name = "", .value = "" }, + }, + ._headers_len = 4, + }; + + if (acl) |acl_value| { + result._headers[result._headers_len] = .{ .name = "x-amz-acl", .value = acl_value }; + result._headers_len += 1; + } + + if (session_token) |token| { + const session_token_value = bun.default_allocator.dupe(u8, token) catch bun.outOfMemory(); + result.session_token = session_token_value; + result._headers[result._headers_len] = .{ .name = "x-amz-security-token", .value = session_token_value }; + result._headers_len += 1; + } + + if (content_disposition) |cd| { + const content_disposition_value = bun.default_allocator.dupe(u8, cd) catch bun.outOfMemory(); + result.content_disposition = content_disposition_value; + result._headers[result._headers_len] = .{ .name = "Content-Disposition", .value = content_disposition_value }; + result._headers_len += 1; + } + + return result; + } +}; + +pub const S3CredentialsWithOptions = struct { + credentials: S3Credentials, + options: MultiPartUploadOptions = .{}, + acl: ?ACL = null, + /// indicates if the credentials have changed + changed_credentials: bool = false, + + _accessKeyIdSlice: ?JSC.ZigString.Slice = null, + _secretAccessKeySlice: ?JSC.ZigString.Slice = null, + _regionSlice: ?JSC.ZigString.Slice = null, + _endpointSlice: ?JSC.ZigString.Slice = null, + _bucketSlice: ?JSC.ZigString.Slice = null, + _sessionTokenSlice: ?JSC.ZigString.Slice = null, + + pub fn deinit(this: *@This()) void { + if (this._accessKeyIdSlice) |slice| slice.deinit(); + if (this._secretAccessKeySlice) |slice| slice.deinit(); + if (this._regionSlice) |slice| slice.deinit(); + if (this._endpointSlice) |slice| slice.deinit(); + if (this._bucketSlice) |slice| slice.deinit(); + if (this._sessionTokenSlice) |slice| slice.deinit(); + } +}; diff --git a/src/s3/download_stream.zig b/src/s3/download_stream.zig new file mode 100644 index 0000000000..9adfbb65af --- /dev/null +++ b/src/s3/download_stream.zig @@ -0,0 +1,242 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const picohttp = JSC.WebCore.picohttp; +const S3Error = @import("./error.zig").S3Error; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const SignResult = S3Credentials.SignResult; +const strings = bun.strings; +const log = bun.Output.scoped(.S3, true); +pub const S3HttpDownloadStreamingTask = struct { + http: bun.http.AsyncHTTP, + vm: *JSC.VirtualMachine, + sign_result: SignResult, + headers: JSC.WebCore.Headers, + callback_context: *anyopaque, + // this transfers ownership from the chunk + callback: *const fn (chunk: bun.MutableString, has_more: bool, err: ?S3Error, *anyopaque) void, + has_schedule_callback: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + signal_store: bun.http.Signals.Store = .{}, + signals: bun.http.Signals = .{}, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + + response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + mutex: bun.Lock = .{}, + reported_response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + state: State.AtomicType = State.AtomicType.init(@bitCast(State{})), + + concurrent_task: JSC.ConcurrentTask = .{}, + range: ?[]const u8, + proxy_url: []const u8, + + pub usingnamespace bun.New(@This()); + pub const State = packed struct(u64) { + pub const AtomicType = std.atomic.Value(u64); + status_code: u32 = 0, + request_error: u16 = 0, + has_more: bool = true, + _reserved: u15 = 0, + }; + + pub fn getState(this: @This()) State { + const state: State = @bitCast(this.state.load(.acquire)); + return state; + } + + pub fn setState(this: *@This(), state: State) void { + this.state.store(@bitCast(state), .monotonic); + } + + pub fn deinit(this: *@This()) void { + this.poll_ref.unref(this.vm); + this.response_buffer.deinit(); + this.reported_response_buffer.deinit(); + this.headers.deinit(); + this.sign_result.deinit(); + this.http.clearData(); + if (this.range) |range| { + bun.default_allocator.free(range); + } + if (this.proxy_url.len > 0) { + bun.default_allocator.free(this.proxy_url); + } + + this.destroy(); + } + + fn reportProgress(this: *@This(), state: State) void { + const has_more = state.has_more; + var err: ?S3Error = null; + var failed = false; + + const chunk = brk: { + switch (state.status_code) { + 200, 204, 206 => { + failed = state.request_error != 0; + }, + else => { + failed = true; + }, + } + if (failed) { + if (!has_more) { + var has_body_code = false; + var has_body_message = false; + + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + if (state.request_error != 0) { + const req_err = @errorFromInt(state.request_error); + code = @errorName(req_err); + has_body_code = true; + } else { + const bytes = this.reported_response_buffer.list.items; + if (bytes.len > 0) { + message = bytes[0..]; + + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + has_body_code = true; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + has_body_message = true; + } + } + } + } + + err = .{ + .code = code, + .message = message, + }; + } + break :brk bun.MutableString{ .allocator = bun.default_allocator, .list = .{} }; + } else { + const buffer = this.reported_response_buffer; + break :brk buffer; + } + }; + log("reportProgres failed: {} has_more: {} len: {d}", .{ failed, has_more, chunk.list.items.len }); + if (failed) { + if (!has_more) { + this.callback(chunk, false, err, this.callback_context); + } + } else { + // dont report empty chunks if we have more data to read + if (!has_more or chunk.list.items.len > 0) { + this.callback(chunk, has_more, null, this.callback_context); + this.reported_response_buffer.reset(); + } + } + } + /// this is the task callback from the last task result and is always in the main thread + pub fn onResponse(this: *@This()) void { + // lets lock and unlock the reported response buffer + this.mutex.lock(); + // the state is atomic let's load it once + const state = this.getState(); + const has_more = state.has_more; + defer { + // always unlock when done + this.mutex.unlock(); + // if we dont have more we should deinit at the end of the function + if (!has_more) this.deinit(); + } + + // there is no reason to set has_schedule_callback to true if we dont have more data to read + if (has_more) this.has_schedule_callback.store(false, .monotonic); + this.reportProgress(state); + } + + /// this function is only called from the http callback in the HTTPThread and returns true if we should wait until we are done buffering the response body to report + /// should only be called when already locked + fn updateState(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult, state: *State) bool { + const is_done = !result.has_more; + // if we got a error or fail wait until we are done buffering the response body to report + var wait_until_done = false; + { + state.has_more = !is_done; + + state.request_error = if (result.fail) |err| @intFromError(err) else 0; + if (state.status_code == 0) { + if (result.certificate_info) |*certificate| { + certificate.deinit(bun.default_allocator); + } + if (result.metadata) |m| { + var metadata = m; + state.status_code = metadata.response.status_code; + metadata.deinit(bun.default_allocator); + } + } + switch (state.status_code) { + 200, 204, 206 => wait_until_done = state.request_error != 0, + else => wait_until_done = true, + } + // store the new state + this.setState(state.*); + this.http = async_http.*; + } + return wait_until_done; + } + + /// this functions is only called from the http callback in the HTTPThread and returns true if we should enqueue another task + fn processHttpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) bool { + // lets lock and unlock to be safe we know the state is not in the middle of a callback when locked + this.mutex.lock(); + defer this.mutex.unlock(); + + // remember the state is atomic load it once, and store it again + var state = this.getState(); + // old state should have more otherwise its a http.zig bug + bun.assert(state.has_more); + const is_done = !result.has_more; + const wait_until_done = updateState(this, async_http, result, &state); + const should_enqueue = !wait_until_done or is_done; + log("state err: {} status_code: {} has_more: {} should_enqueue: {}", .{ state.request_error, state.status_code, state.has_more, should_enqueue }); + + if (should_enqueue) { + if (result.body) |body| { + this.response_buffer = body.*; + if (body.list.items.len > 0) { + _ = this.reported_response_buffer.write(body.list.items) catch bun.outOfMemory(); + } + this.response_buffer.reset(); + if (this.reported_response_buffer.list.items.len == 0 and !is_done) { + return false; + } + } else if (!is_done) { + return false; + } + if (this.has_schedule_callback.cmpxchgStrong(false, true, .acquire, .monotonic)) |has_schedule_callback| { + if (has_schedule_callback) { + return false; + } + } + return true; + } + return false; + } + /// this is the callback from the http.zig AsyncHTTP is always called from the HTTPThread + pub fn httpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { + if (processHttpCallback(this, async_http, result)) { + // we are always unlocked here and its safe to enqueue + this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); + } + } +}; diff --git a/src/s3/error.zig b/src/s3/error.zig new file mode 100644 index 0000000000..fd8f732e84 --- /dev/null +++ b/src/s3/error.zig @@ -0,0 +1,86 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +pub const ErrorCodeAndMessage = struct { + code: []const u8, + message: []const u8, +}; +pub fn getSignErrorMessage(comptime err: anyerror) [:0]const u8 { + return switch (err) { + error.MissingCredentials => return "Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required", + error.InvalidMethod => return "Method must be GET, PUT, DELETE or HEAD when using s3:// protocol", + error.InvalidPath => return "Invalid S3 bucket, key combination", + error.InvalidEndpoint => return "Invalid S3 endpoint", + error.InvalidSessionToken => return "Invalid session token", + else => return "Failed to retrieve S3 content. Are the credentials correct?", + }; +} +pub fn getJSSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return switch (err) { + error.MissingCredentials => return globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).toJS(), + error.InvalidMethod => return globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).toJS(), + error.InvalidPath => return globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).toJS(), + error.InvalidEndpoint => return globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).toJS(), + error.InvalidSessionToken => return globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).toJS(), + else => return globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).toJS(), + }; +} +pub fn throwSignError(err: anyerror, globalThis: *JSC.JSGlobalObject) bun.JSError { + return switch (err) { + error.MissingCredentials => globalThis.ERR_S3_MISSING_CREDENTIALS(getSignErrorMessage(error.MissingCredentials), .{}).throw(), + error.InvalidMethod => globalThis.ERR_S3_INVALID_METHOD(getSignErrorMessage(error.InvalidMethod), .{}).throw(), + error.InvalidPath => globalThis.ERR_S3_INVALID_PATH(getSignErrorMessage(error.InvalidPath), .{}).throw(), + error.InvalidEndpoint => globalThis.ERR_S3_INVALID_ENDPOINT(getSignErrorMessage(error.InvalidEndpoint), .{}).throw(), + error.InvalidSessionToken => globalThis.ERR_S3_INVALID_SESSION_TOKEN(getSignErrorMessage(error.InvalidSessionToken), .{}).throw(), + else => globalThis.ERR_S3_INVALID_SIGNATURE(getSignErrorMessage(error.SignError), .{}).throw(), + }; +} +pub fn getSignErrorCodeAndMessage(err: anyerror) ErrorCodeAndMessage { + // keep error codes consistent for internal errors + return switch (err) { + error.MissingCredentials => .{ .code = "ERR_S3_MISSING_CREDENTIALS", .message = getSignErrorMessage(error.MissingCredentials) }, + error.InvalidMethod => .{ .code = "ERR_S3_INVALID_METHOD", .message = getSignErrorMessage(error.InvalidMethod) }, + error.InvalidPath => .{ .code = "ERR_S3_INVALID_PATH", .message = getSignErrorMessage(error.InvalidPath) }, + error.InvalidEndpoint => .{ .code = "ERR_S3_INVALID_ENDPOINT", .message = getSignErrorMessage(error.InvalidEndpoint) }, + error.InvalidSessionToken => .{ .code = "ERR_S3_INVALID_SESSION_TOKEN", .message = getSignErrorMessage(error.InvalidSessionToken) }, + else => .{ .code = "ERR_S3_INVALID_SIGNATURE", .message = getSignErrorMessage(error.SignError) }, + }; +} + +const JSS3Error = extern struct { + code: bun.String = bun.String.empty, + message: bun.String = bun.String.empty, + path: bun.String = bun.String.empty, + + pub fn init(code: []const u8, message: []const u8, path: ?[]const u8) @This() { + return .{ + // lets make sure we can reuse code and message and keep it service independent + .code = bun.String.createAtomIfPossible(code), + .message = bun.String.createAtomIfPossible(message), + .path = if (path) |p| bun.String.init(p) else bun.String.empty, + }; + } + + pub fn deinit(this: *const @This()) void { + this.path.deref(); + this.code.deref(); + this.message.deref(); + } + + pub fn toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) JSC.JSValue { + defer this.deinit(); + + return S3Error__toErrorInstance(this, global); + } + extern fn S3Error__toErrorInstance(this: *const @This(), global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue; +}; + +pub const S3Error = struct { + code: []const u8, + message: []const u8, + + pub fn toJS(err: *const @This(), globalObject: *JSC.JSGlobalObject, path: ?[]const u8) JSC.JSValue { + const value = JSS3Error.init(err.code, err.message, path).toErrorInstance(globalObject); + bun.assert(!globalObject.hasException()); + return value; + } +}; diff --git a/src/s3/multipart.zig b/src/s3/multipart.zig new file mode 100644 index 0000000000..9b3faadf25 --- /dev/null +++ b/src/s3/multipart.zig @@ -0,0 +1,538 @@ +const std = @import("std"); +const bun = @import("root").bun; +const strings = bun.strings; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const ACL = @import("./acl.zig").ACL; +const JSC = bun.JSC; +const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions; +const S3SimpleRequest = @import("./simple_request.zig"); +const executeSimpleS3Request = S3SimpleRequest.executeSimpleS3Request; +const S3Error = @import("./error.zig").S3Error; + +pub const MultiPartUpload = struct { + const OneMiB: usize = MultiPartUploadOptions.OneMiB; + const MAX_SINGLE_UPLOAD_SIZE_IN_MiB: usize = MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB; // we limit to 5 GiB + const MAX_SINGLE_UPLOAD_SIZE: usize = MAX_SINGLE_UPLOAD_SIZE_IN_MiB * OneMiB; // we limit to 5 GiB + const MIN_SINGLE_UPLOAD_SIZE_IN_MiB: usize = MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB; + const DefaultPartSize = MultiPartUploadOptions.DefaultPartSize; + const MAX_QUEUE_SIZE = MultiPartUploadOptions.MAX_QUEUE_SIZE; + const AWS = S3Credentials; + queue: std.ArrayListUnmanaged(UploadPart) = .{}, + available: bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE) = bun.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull(), + + currentPartNumber: u16 = 1, + ref_count: u16 = 1, + ended: bool = false, + + options: MultiPartUploadOptions = .{}, + acl: ?ACL = null, + credentials: *S3Credentials, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + vm: *JSC.VirtualMachine, + globalThis: *JSC.JSGlobalObject, + + buffered: std.ArrayListUnmanaged(u8) = .{}, + offset: usize = 0, + + path: []const u8, + proxy: []const u8, + content_type: ?[]const u8 = null, + upload_id: []const u8 = "", + uploadid_buffer: bun.MutableString = .{ .allocator = bun.default_allocator, .list = .{} }, + + multipart_etags: std.ArrayListUnmanaged(UploadPart.UploadPartResult) = .{}, + multipart_upload_list: bun.ByteList = .{}, + + state: enum { + wait_stream_check, + not_started, + multipart_started, + multipart_completed, + singlefile_started, + finished, + } = .not_started, + + callback: *const fn (S3SimpleRequest.S3UploadResult, *anyopaque) void, + callback_context: *anyopaque, + + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + const log = bun.Output.scoped(.S3MultiPartUpload, true); + + pub const UploadPart = struct { + data: []const u8, + state: enum { + pending, + started, + completed, + canceled, + }, + owns_data: bool, + partNumber: u16, // max is 10,000 + retry: u8, // auto retry, decrement until 0 and fail after this + index: u8, + ctx: *MultiPartUpload, + + pub const UploadPartResult = struct { + number: u16, + etag: []const u8, + }; + fn sortEtags(_: *MultiPartUpload, a: UploadPart.UploadPartResult, b: UploadPart.UploadPartResult) bool { + return a.number < b.number; + } + + pub fn onPartResponse(result: S3SimpleRequest.S3PartResult, this: *@This()) void { + if (this.state == .canceled or this.ctx.state == .finished) { + log("onPartResponse {} canceled", .{this.partNumber}); + if (this.owns_data) bun.default_allocator.free(this.data); + this.ctx.deref(); + return; + } + + this.state = .completed; + + switch (result) { + .failure => |err| { + if (this.retry > 0) { + log("onPartResponse {} retry", .{this.partNumber}); + this.retry -= 1; + // retry failed + this.perform(); + return; + } else { + log("onPartResponse {} failed", .{this.partNumber}); + if (this.owns_data) bun.default_allocator.free(this.data); + defer this.ctx.deref(); + return this.ctx.fail(err); + } + }, + .etag => |etag| { + log("onPartResponse {} success", .{this.partNumber}); + + if (this.owns_data) bun.default_allocator.free(this.data); + // we will need to order this + this.ctx.multipart_etags.append(bun.default_allocator, .{ + .number = this.partNumber, + .etag = bun.default_allocator.dupe(u8, etag) catch bun.outOfMemory(), + }) catch bun.outOfMemory(); + + defer this.ctx.deref(); + // mark as available + this.ctx.available.set(this.index); + // drain more + this.ctx.drainEnqueuedParts(); + }, + } + } + + fn perform(this: *@This()) void { + var params_buffer: [2048]u8 = undefined; + const search_params = std.fmt.bufPrint(¶ms_buffer, "?partNumber={}&uploadId={s}&x-id=UploadPart", .{ + this.partNumber, + this.ctx.upload_id, + }) catch unreachable; + executeSimpleS3Request(this.ctx.credentials, .{ + .path = this.ctx.path, + .method = .PUT, + .proxy_url = this.ctx.proxyUrl(), + .body = this.data, + .search_params = search_params, + }, .{ .part = @ptrCast(&onPartResponse) }, this); + } + pub fn start(this: *@This()) void { + if (this.state != .pending or this.ctx.state != .multipart_completed or this.ctx.state == .finished) return; + this.ctx.ref(); + this.state = .started; + this.perform(); + } + pub fn cancel(this: *@This()) void { + const state = this.state; + this.state = .canceled; + + switch (state) { + .pending => { + if (this.owns_data) bun.default_allocator.free(this.data); + }, + // if is not pending we will free later or is already freed + else => {}, + } + } + }; + + fn deinit(this: *@This()) void { + log("deinit", .{}); + if (this.queue.capacity > 0) + this.queue.deinit(bun.default_allocator); + this.poll_ref.unref(this.vm); + bun.default_allocator.free(this.path); + if (this.proxy.len > 0) { + bun.default_allocator.free(this.proxy); + } + if (this.content_type) |ct| { + if (ct.len > 0) { + bun.default_allocator.free(ct); + } + } + this.credentials.deref(); + this.uploadid_buffer.deinit(); + for (this.multipart_etags.items) |tag| { + bun.default_allocator.free(tag.etag); + } + if (this.multipart_etags.capacity > 0) + this.multipart_etags.deinit(bun.default_allocator); + if (this.multipart_upload_list.cap > 0) + this.multipart_upload_list.deinitWithAllocator(bun.default_allocator); + this.destroy(); + } + + pub fn singleSendUploadResponse(result: S3SimpleRequest.S3UploadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; + switch (result) { + .failure => |err| { + if (this.options.retry > 0) { + log("singleSendUploadResponse {} retry", .{this.options.retry}); + this.options.retry -= 1; + this.ref(); + // retry failed + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .PUT, + .proxy_url = this.proxyUrl(), + .body = this.buffered.items, + .content_type = this.content_type, + .acl = this.acl, + }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); + + return; + } else { + log("singleSendUploadResponse failed", .{}); + return this.fail(err); + } + }, + .success => { + log("singleSendUploadResponse success", .{}); + this.done(); + }, + } + } + + fn getCreatePart(this: *@This(), chunk: []const u8, owns_data: bool) ?*UploadPart { + const index = this.available.findFirstSet() orelse { + // this means that the queue is full and we cannot flush it + return null; + }; + + if (index >= this.options.queueSize) { + // ops too much concurrency wait more + return null; + } + this.available.unset(index); + defer this.currentPartNumber += 1; + + if (this.queue.items.len <= index) { + this.queue.append(bun.default_allocator, .{ + .data = chunk, + .partNumber = this.currentPartNumber, + .owns_data = owns_data, + .ctx = this, + .index = @truncate(index), + .retry = this.options.retry, + .state = .pending, + }) catch bun.outOfMemory(); + return &this.queue.items[index]; + } + this.queue.items[index] = .{ + .data = chunk, + .partNumber = this.currentPartNumber, + .owns_data = owns_data, + .ctx = this, + .index = @truncate(index), + .retry = this.options.retry, + .state = .pending, + }; + return &this.queue.items[index]; + } + + fn drainEnqueuedParts(this: *@This()) void { + if (this.state == .finished) { + return; + } + // check pending to start or transformed buffered ones into tasks + if (this.state == .multipart_completed) { + for (this.queue.items) |*part| { + if (part.state == .pending) { + // lets start the part request + part.start(); + } + } + } + const partSize = this.partSizeInBytes(); + if (this.ended or this.buffered.items.len >= partSize) { + this.processMultiPart(partSize); + } + + if (this.ended and this.available.mask == std.bit_set.IntegerBitSet(MAX_QUEUE_SIZE).initFull().mask) { + // we are done + this.done(); + } + } + pub fn fail(this: *@This(), _err: S3Error) void { + log("fail {s}:{s}", .{ _err.code, _err.message }); + this.ended = true; + for (this.queue.items) |*task| { + task.cancel(); + } + if (this.state != .finished) { + const old_state = this.state; + this.state = .finished; + this.callback(.{ .failure = _err }, this.callback_context); + + if (old_state == .multipart_completed) { + // will deref after rollback + this.rollbackMultiPartRequest(); + } else { + this.deref(); + } + } + } + + fn done(this: *@This()) void { + if (this.state == .multipart_completed) { + this.state = .finished; + + std.sort.block(UploadPart.UploadPartResult, this.multipart_etags.items, this, UploadPart.sortEtags); + this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); + for (this.multipart_etags.items) |tag| { + this.multipart_upload_list.appendFmt(bun.default_allocator, "{}{s}", .{ tag.number, tag.etag }) catch bun.outOfMemory(); + + bun.default_allocator.free(tag.etag); + } + this.multipart_etags.deinit(bun.default_allocator); + this.multipart_etags = .{}; + this.multipart_upload_list.append(bun.default_allocator, "") catch bun.outOfMemory(); + // will deref and ends after commit + this.commitMultiPartRequest(); + } else { + this.callback(.{ .success = {} }, this.callback_context); + this.state = .finished; + this.deref(); + } + } + pub fn startMultiPartRequestResult(result: S3SimpleRequest.S3DownloadResult, this: *@This()) void { + defer this.deref(); + if (this.state == .finished) return; + switch (result) { + .failure => |err| { + log("startMultiPartRequestResult {s} failed {s}: {s}", .{ this.path, err.message, err.message }); + this.fail(err); + }, + .success => |response| { + const slice = response.body.list.items; + this.uploadid_buffer = result.success.body; + + if (strings.indexOf(slice, "")) |start| { + if (strings.indexOf(slice, "")) |end| { + this.upload_id = slice[start + 10 .. end]; + } + } + if (this.upload_id.len == 0) { + // Unknown type of response error from AWS + log("startMultiPartRequestResult {s} failed invalid id", .{this.path}); + this.fail(.{ + .code = "UnknownError", + .message = "Failed to initiate multipart upload", + }); + return; + } + log("startMultiPartRequestResult {s} success id: {s}", .{ this.path, this.upload_id }); + this.state = .multipart_completed; + this.drainEnqueuedParts(); + }, + // this is "unreachable" but we cover in case AWS returns 404 + .not_found => this.fail(.{ + .code = "UnknownError", + .message = "Failed to initiate multipart upload", + }), + } + } + + pub fn onCommitMultiPartRequest(result: S3SimpleRequest.S3CommitResult, this: *@This()) void { + log("onCommitMultiPartRequest {s}", .{this.upload_id}); + + switch (result) { + .failure => |err| { + if (this.options.retry > 0) { + this.options.retry -= 1; + // retry commit + this.commitMultiPartRequest(); + return; + } + this.callback(.{ .failure = err }, this.callback_context); + this.deref(); + }, + .success => { + this.callback(.{ .success = {} }, this.callback_context); + this.state = .finished; + this.deref(); + }, + } + } + + pub fn onRollbackMultiPartRequest(result: S3SimpleRequest.S3UploadResult, this: *@This()) void { + log("onRollbackMultiPartRequest {s}", .{this.upload_id}); + switch (result) { + .failure => { + if (this.options.retry > 0) { + this.options.retry -= 1; + // retry rollback + this.rollbackMultiPartRequest(); + return; + } + this.deref(); + }, + .success => { + this.deref(); + }, + } + } + + fn commitMultiPartRequest(this: *@This()) void { + log("commitMultiPartRequest {s}", .{this.upload_id}); + var params_buffer: [2048]u8 = undefined; + const searchParams = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ + this.upload_id, + }) catch unreachable; + + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .POST, + .proxy_url = this.proxyUrl(), + .body = this.multipart_upload_list.slice(), + .search_params = searchParams, + }, .{ .commit = @ptrCast(&onCommitMultiPartRequest) }, this); + } + fn rollbackMultiPartRequest(this: *@This()) void { + log("rollbackMultiPartRequest {s}", .{this.upload_id}); + var params_buffer: [2048]u8 = undefined; + const search_params = std.fmt.bufPrint(¶ms_buffer, "?uploadId={s}", .{ + this.upload_id, + }) catch unreachable; + + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .DELETE, + .proxy_url = this.proxyUrl(), + .body = "", + .search_params = search_params, + }, .{ .upload = @ptrCast(&onRollbackMultiPartRequest) }, this); + } + fn enqueuePart(this: *@This(), chunk: []const u8, owns_data: bool) bool { + const part = this.getCreatePart(chunk, owns_data) orelse return false; + + if (this.state == .not_started) { + // will auto start later + this.state = .multipart_started; + this.ref(); + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .POST, + .proxy_url = this.proxyUrl(), + .body = "", + .search_params = "?uploads=", + .content_type = this.content_type, + .acl = this.acl, + }, .{ .download = @ptrCast(&startMultiPartRequestResult) }, this); + } else if (this.state == .multipart_completed) { + part.start(); + } + return true; + } + + fn processMultiPart(this: *@This(), part_size: usize) void { + // need to split in multiple parts because of the size + var buffer = this.buffered.items[this.offset..]; + var queue_full = false; + defer if (!this.ended and queue_full == false) { + this.buffered = .{}; + this.offset = 0; + }; + + while (buffer.len > 0) { + const len = @min(part_size, buffer.len); + const slice = buffer[0..len]; + buffer = buffer[len..]; + // its one big buffer lets free after we are done with everything, part dont own the data + if (this.enqueuePart(slice, this.ended)) { + this.offset += len; + } else { + queue_full = true; + break; + } + } + } + + pub fn proxyUrl(this: *@This()) ?[]const u8 { + return this.proxy; + } + fn processBuffered(this: *@This(), part_size: usize) void { + if (this.ended and this.buffered.items.len < this.partSizeInBytes() and this.state == .not_started) { + log("processBuffered {s} singlefile_started", .{this.path}); + this.state = .singlefile_started; + this.ref(); + // we can do only 1 request + executeSimpleS3Request(this.credentials, .{ + .path = this.path, + .method = .PUT, + .proxy_url = this.proxyUrl(), + .body = this.buffered.items, + .content_type = this.content_type, + .acl = this.acl, + }, .{ .upload = @ptrCast(&singleSendUploadResponse) }, this); + } else { + // we need to split + this.processMultiPart(part_size); + } + } + + pub fn partSizeInBytes(this: *@This()) usize { + return this.options.partSize * OneMiB; + } + + pub fn continueStream(this: *@This()) void { + if (this.state == .wait_stream_check) { + this.state = .not_started; + if (this.ended) { + this.processBuffered(this.partSizeInBytes()); + } + } + } + + pub fn sendRequestData(this: *@This(), chunk: []const u8, is_last: bool) void { + if (this.ended) return; + if (this.state == .wait_stream_check and chunk.len == 0 and is_last) { + // we do this because stream will close if the file dont exists and we dont wanna to send an empty part in this case + this.ended = true; + return; + } + if (is_last) { + this.ended = true; + if (chunk.len > 0) { + this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); + } + this.processBuffered(this.partSizeInBytes()); + } else { + // still have more data and receive empty, nothing todo here + if (chunk.len == 0) return; + this.buffered.appendSlice(bun.default_allocator, chunk) catch bun.outOfMemory(); + const partSize = this.partSizeInBytes(); + if (this.buffered.items.len >= partSize) { + // send the part we have enough data + this.processBuffered(partSize); + return; + } + + // wait for more + } + } +}; diff --git a/src/s3/multipart_options.zig b/src/s3/multipart_options.zig new file mode 100644 index 0000000000..e99d18d09e --- /dev/null +++ b/src/s3/multipart_options.zig @@ -0,0 +1,22 @@ +pub const MultiPartUploadOptions = struct { + pub const OneMiB: usize = 1048576; + pub const MAX_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5120; // we limit to 5 GiB + pub const MAX_SINGLE_UPLOAD_SIZE: usize = MAX_SINGLE_UPLOAD_SIZE_IN_MiB * OneMiB; // we limit to 5 GiB + pub const MIN_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5; + pub const DefaultPartSize = OneMiB * MIN_SINGLE_UPLOAD_SIZE_IN_MiB; + pub const MAX_QUEUE_SIZE = 64; // dont make sense more than this because we use fetch anything greater will be 64 + + /// more than 255 dont make sense http thread cannot handle more than that + queueSize: u8 = 5, + /// in s3 client sdk they set it in bytes but the min is still 5 MiB + /// var params = {Bucket: 'bucket', Key: 'key', Body: stream}; + /// var options = {partSize: 10 * 1024 * 1024, queueSize: 1}; + /// s3.upload(params, options, function(err, data) { + /// console.log(err, data); + /// }); + /// See. https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property + /// The value is in MiB min is 5 and max 5120 (but we limit to 4 GiB aka 4096) + partSize: u16 = 5, + /// default is 3 max 255 + retry: u8 = 3, +}; diff --git a/src/s3/simple_request.zig b/src/s3/simple_request.zig new file mode 100644 index 0000000000..e78d7ecddb --- /dev/null +++ b/src/s3/simple_request.zig @@ -0,0 +1,404 @@ +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const strings = bun.strings; +const SignResult = @import("./credentials.zig").S3Credentials.SignResult; +const S3Error = @import("./error.zig").S3Error; +const getSignErrorCodeAndMessage = @import("./error.zig").getSignErrorCodeAndMessage; +const S3Credentials = @import("./credentials.zig").S3Credentials; +const picohttp = bun.picohttp; +const ACL = @import("./acl.zig").ACL; +pub const S3StatResult = union(enum) { + success: struct { + size: usize = 0, + /// etag is not owned and need to be copied if used after this callback + etag: []const u8 = "", + }, + not_found: S3Error, + + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3DownloadResult = union(enum) { + success: struct { + /// etag is not owned and need to be copied if used after this callback + etag: []const u8 = "", + /// body is owned and dont need to be copied, but dont forget to free it + body: bun.MutableString, + }, + not_found: S3Error, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3UploadResult = union(enum) { + success: void, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +pub const S3DeleteResult = union(enum) { + success: void, + not_found: S3Error, + + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +// commit result also fails if status 200 but with body containing an Error +pub const S3CommitResult = union(enum) { + success: void, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; +// commit result also fails if status 200 but with body containing an Error +pub const S3PartResult = union(enum) { + etag: []const u8, + /// failure error is not owned and need to be copied if used after this callback + failure: S3Error, +}; + +pub const S3HttpSimpleTask = struct { + http: bun.http.AsyncHTTP, + vm: *JSC.VirtualMachine, + sign_result: SignResult, + headers: JSC.WebCore.Headers, + callback_context: *anyopaque, + callback: Callback, + response_buffer: bun.MutableString = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }, + result: bun.http.HTTPClientResult = .{}, + concurrent_task: JSC.ConcurrentTask = .{}, + range: ?[]const u8, + poll_ref: bun.Async.KeepAlive = bun.Async.KeepAlive.init(), + + usingnamespace bun.New(@This()); + pub const Callback = union(enum) { + stat: *const fn (S3StatResult, *anyopaque) void, + download: *const fn (S3DownloadResult, *anyopaque) void, + upload: *const fn (S3UploadResult, *anyopaque) void, + delete: *const fn (S3DeleteResult, *anyopaque) void, + commit: *const fn (S3CommitResult, *anyopaque) void, + part: *const fn (S3PartResult, *anyopaque) void, + + pub fn fail(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { + switch (this) { + inline .upload, + .download, + .stat, + .delete, + .commit, + .part, + => |callback| callback(.{ + .failure = .{ + .code = code, + .message = message, + }, + }, context), + } + } + pub fn notFound(this: @This(), code: []const u8, message: []const u8, context: *anyopaque) void { + switch (this) { + inline .download, + .stat, + .delete, + => |callback| callback(.{ + .not_found = .{ + .code = code, + .message = message, + }, + }, context), + else => this.fail(code, message, context), + } + } + }; + pub fn deinit(this: *@This()) void { + if (this.result.certificate_info) |*certificate| { + certificate.deinit(bun.default_allocator); + } + this.poll_ref.unref(this.vm); + this.response_buffer.deinit(); + this.headers.deinit(); + this.sign_result.deinit(); + this.http.clearData(); + if (this.range) |range| { + bun.default_allocator.free(range); + } + if (this.result.metadata) |*metadata| { + metadata.deinit(bun.default_allocator); + } + this.destroy(); + } + + const ErrorType = enum { + not_found, + failure, + }; + fn errorWithBody(this: @This(), comptime error_type: ErrorType) void { + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + var has_error_code = false; + if (this.result.fail) |err| { + code = @errorName(err); + has_error_code = true; + } else if (this.result.body) |body| { + const bytes = body.list.items; + if (bytes.len > 0) { + message = bytes[0..]; + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + has_error_code = true; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + } + } + } + } + + if (error_type == .not_found) { + if (!has_error_code) { + code = "NoSuchKey"; + message = "The specified key does not exist."; + } + this.callback.notFound(code, message, this.callback_context); + } else { + this.callback.fail(code, message, this.callback_context); + } + } + + fn failIfContainsError(this: *@This(), status: u32) bool { + var code: []const u8 = "UnknownError"; + var message: []const u8 = "an unexpected error has occurred"; + + if (this.result.fail) |err| { + code = @errorName(err); + } else if (this.result.body) |body| { + const bytes = body.list.items; + var has_error = false; + if (bytes.len > 0) { + message = bytes[0..]; + if (strings.indexOf(bytes, "") != null) { + has_error = true; + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + code = bytes[start + "".len .. end]; + } + } + if (strings.indexOf(bytes, "")) |start| { + if (strings.indexOf(bytes, "")) |end| { + message = bytes[start + "".len .. end]; + } + } + } + } + if (!has_error and status == 200 or status == 206) { + return false; + } + } else if (status == 200 or status == 206) { + return false; + } + this.callback.fail(code, message, this.callback_context); + return true; + } + /// this is the task callback from the last task result and is always in the main thread + pub fn onResponse(this: *@This()) void { + defer this.deinit(); + if (!this.result.isSuccess()) { + this.errorWithBody(.failure); + return; + } + bun.assert(this.result.metadata != null); + const response = this.result.metadata.?.response; + switch (this.callback) { + .stat => |callback| { + switch (response.status_code) { + 200 => { + callback(.{ + .success = .{ + .etag = response.headers.get("etag") orelse "", + .size = if (response.headers.get("content-length")) |content_len| (std.fmt.parseInt(usize, content_len, 10) catch 0) else 0, + }, + }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .delete => |callback| { + switch (response.status_code) { + 200, 204 => { + callback(.{ .success = {} }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .upload => |callback| { + switch (response.status_code) { + 200 => { + callback(.{ .success = {} }, this.callback_context); + }, + else => { + this.errorWithBody(.failure); + }, + } + }, + .download => |callback| { + switch (response.status_code) { + 200, 204, 206 => { + const body = this.response_buffer; + this.response_buffer = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }; + callback(.{ + .success = .{ + .etag = response.headers.get("etag") orelse "", + .body = body, + }, + }, this.callback_context); + }, + 404 => { + this.errorWithBody(.not_found); + }, + else => { + //error + this.errorWithBody(.failure); + }, + } + }, + .commit => |callback| { + // commit multipart upload can fail with status 200 + if (!this.failIfContainsError(response.status_code)) { + callback(.{ .success = {} }, this.callback_context); + } + }, + .part => |callback| { + if (!this.failIfContainsError(response.status_code)) { + if (response.headers.get("etag")) |etag| { + callback(.{ .etag = etag }, this.callback_context); + } else { + this.errorWithBody(.failure); + } + } + }, + } + } + + /// this is the callback from the http.zig AsyncHTTP is always called from the HTTPThread + pub fn httpCallback(this: *@This(), async_http: *bun.http.AsyncHTTP, result: bun.http.HTTPClientResult) void { + const is_done = !result.has_more; + this.result = result; + this.http = async_http.*; + this.response_buffer = async_http.response_buffer.*; + if (is_done) { + this.vm.eventLoop().enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); + } + } +}; + +pub const S3SimpleRequestOptions = struct { + // signing options + path: []const u8, + method: bun.http.Method, + search_params: ?[]const u8 = null, + content_type: ?[]const u8 = null, + content_disposition: ?[]const u8 = null, + + // http request options + body: []const u8, + proxy_url: ?[]const u8 = null, + range: ?[]const u8 = null, + acl: ?ACL = null, +}; + +pub fn executeSimpleS3Request( + this: *const S3Credentials, + options: S3SimpleRequestOptions, + callback: S3HttpSimpleTask.Callback, + callback_context: *anyopaque, +) void { + var result = this.signRequest(.{ + .path = options.path, + .method = options.method, + .search_params = options.search_params, + .content_disposition = options.content_disposition, + .acl = options.acl, + }, null) catch |sign_err| { + if (options.range) |range_| bun.default_allocator.free(range_); + const error_code_and_message = getSignErrorCodeAndMessage(sign_err); + callback.fail(error_code_and_message.code, error_code_and_message.message, callback_context); + return; + }; + + const headers = brk: { + var header_buffer: [10]picohttp.Header = undefined; + if (options.range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } else { + if (options.content_type) |content_type| { + if (content_type.len > 0) { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = content_type }); + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(_headers, bun.default_allocator) catch bun.outOfMemory(); + } + } + + break :brk JSC.WebCore.Headers.fromPicoHttpHeaders(result.headers(), bun.default_allocator) catch bun.outOfMemory(); + } + }; + const task = S3HttpSimpleTask.new(.{ + .http = undefined, + .sign_result = result, + .callback_context = callback_context, + .callback = callback, + .range = options.range, + .headers = headers, + .vm = JSC.VirtualMachine.get(), + }); + task.poll_ref.ref(task.vm); + + const url = bun.URL.parse(result.url); + const proxy = options.proxy_url orelse ""; + task.http = bun.http.AsyncHTTP.init( + bun.default_allocator, + options.method, + url, + task.headers.entries, + task.headers.buf.items, + &task.response_buffer, + options.body, + bun.http.HTTPClientResult.Callback.New( + *S3HttpSimpleTask, + S3HttpSimpleTask.httpCallback, + ).init(task), + .follow, + .{ + .http_proxy = if (proxy.len > 0) bun.URL.parse(proxy) else null, + .verbose = task.vm.getVerboseFetch(), + .reject_unauthorized = task.vm.getTLSRejectUnauthorized(), + }, + ); + // queue http request + bun.http.HTTPThread.init(&.{}); + var batch = bun.ThreadPool.Batch{}; + task.http.schedule(bun.default_allocator, &batch); + bun.http.http_thread.schedule(batch); +} diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index 3fb3670cb1..6aa06c93a6 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -651,32 +651,47 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { it("should error with invalid endpoint", async () => { await Promise.all( [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "🙂.🥯", - }); try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "🙂.🥯", + }); await s3file.write("Hello Bun!"); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBeOneOf(["FailedToOpenSocket", "ConnectionRefused"]); + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); } }), ); }); - it("should error with invalid endpoint", async () => { await Promise.all( [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "..asd.@%&&&%%", - }); try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, // credentials and endpoint dont match + endpoint: "s3.us-west-1.amazonaws.com", + }); await s3file.write("Hello Bun!"); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBeOneOf(["FailedToOpenSocket", "ConnectionRefused"]); + expect(e?.code).toBe("PermanentRedirect"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "..asd.@%&&&%%", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); } }), ); @@ -782,6 +797,36 @@ describe.skipIf(!s3Options.accessKeyId)("s3", () => { expect(url.includes("X-Amz-Algorithm")).toBe(true); expect(url.includes("X-Amz-SignedHeaders")).toBe(true); }); + it("default endpoint and region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = undefined; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-east-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint + region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = "us-west-1"; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-west-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); it("should work with expires", async () => { const s3file = s3("s3://bucket/credentials-test", s3Options); const url = s3file.presign({ From 8d82302ec57f5d33a0c427b0a600c97da004a872 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sun, 5 Jan 2025 18:50:03 -0800 Subject: [PATCH 36/56] docs(plugins): fix typos (#16174) --- docs/bundler/plugins.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bundler/plugins.md b/docs/bundler/plugins.md index 8e6b79c0e7..1831e8d6cf 100644 --- a/docs/bundler/plugins.md +++ b/docs/bundler/plugins.md @@ -69,7 +69,7 @@ await Bun.build({ ### Namespaces -`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespaace? +`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespace? Every module has a namespace. Namespaces are used to prefix the import in transpiled code; for instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` will transform an import from `./myfile.yaml` into `yaml:./myfile.yaml`. @@ -239,7 +239,7 @@ One of the arguments passed to the `onLoad` callback is a `defer` function. This This allows you to delay execution of the `onLoad` callback until all other modules have been loaded. -This is useful for returning contens of a module that depends on other modules. +This is useful for returning contents of a module that depends on other modules. ##### Example: tracking and reporting unused exports From 189684f1734c27d4b773024c4673815651a76136 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Mon, 6 Jan 2025 12:36:11 -0600 Subject: [PATCH 37/56] feat(node/path): support `matchesGlob` (#15917) --- src/js/node/path.ts | 46 ++++++++++++ test/js/bun/glob/match.test.ts | 7 ++ test/js/node/path/matches-glob.test.ts | 78 ++++++++++++++++++++ test/js/node/test/parallel/test-path-glob.js | 44 +++++++++++ 4 files changed, 175 insertions(+) create mode 100644 test/js/node/path/matches-glob.test.ts create mode 100644 test/js/node/test/parallel/test-path-glob.js diff --git a/src/js/node/path.ts b/src/js/node/path.ts index f7364a82bb..8129e60bbf 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -1,4 +1,6 @@ // Hardcoded module "node:path" +const { validateString } = require("internal/validators"); + const [bindingPosix, bindingWin32] = $cpp("Path.cpp", "createNodePathBinding"); const toNamespacedPathPosix = bindingPosix.toNamespacedPath.bind(bindingPosix); const toNamespacedPathWin32 = bindingWin32.toNamespacedPath.bind(bindingWin32); @@ -40,4 +42,48 @@ const win32 = { }; posix.win32 = win32.win32 = win32; posix.posix = posix; + +type Glob = import("bun").Glob; + +let LazyGlob: Glob | undefined; +function loadGlob(): LazyGlob { + LazyGlob = require("bun").Glob; +} + +// the most-recently used glob is memoized in case `matchesGlob` is called in a +// loop with the same pattern +let prevGlob: Glob | undefined; +let prevPattern: string | undefined; +function matchesGlob(isWindows, path, pattern) { + let glob: Glob; + + validateString(path, "path"); + if (isWindows) path = path.replaceAll("\\", "/"); + + if (prevGlob) { + $assert(prevPattern !== undefined); + if (prevPattern === pattern) { + glob = prevGlob; + } else { + if (LazyGlob === undefined) loadGlob(); + validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + } else { + loadGlob(); // no prevGlob implies LazyGlob isn't loaded + validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + + return glob.match(path); +} + +// posix.matchesGlob = win32.matchesGlob = matchesGlob; +posix.matchesGlob = matchesGlob.bind(null, false); +win32.matchesGlob = matchesGlob.bind(null, true); + export default process.platform === "win32" ? win32 : posix; diff --git a/test/js/bun/glob/match.test.ts b/test/js/bun/glob/match.test.ts index c09f8b7cd0..9a98d44c40 100644 --- a/test/js/bun/glob/match.test.ts +++ b/test/js/bun/glob/match.test.ts @@ -634,6 +634,13 @@ describe("Glob.match", () => { expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue(); }); + test("square braces", () => { + expect(new Glob("src/*.[tj]s").match("src/foo.js")).toBeTrue(); + expect(new Glob("src/*.[tj]s").match("src/foo.ts")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/bar.md")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/baz.md")).toBeTrue(); + }); + test("bash wildmatch", () => { expect(new Glob("a[]-]b").match("aab")).toBeFalse(); expect(new Glob("[ten]").match("ten")).toBeFalse(); diff --git a/test/js/node/path/matches-glob.test.ts b/test/js/node/path/matches-glob.test.ts new file mode 100644 index 0000000000..8802be251b --- /dev/null +++ b/test/js/node/path/matches-glob.test.ts @@ -0,0 +1,78 @@ +import path from "path"; + +describe("path.matchesGlob(path, glob)", () => { + const stringLikeObject = { + toString() { + return "hi"; + }, + }; + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `path` is not a string", (notAString: any) => { + expect(() => path.matchesGlob(notAString, "*")).toThrow(TypeError); + }); + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `glob` is not a string", (notAString: any) => { + expect(() => path.matchesGlob("hi", notAString)).toThrow(TypeError); + }); +}); + +describe("path.posix.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**/*.js"], + ["src/bar/foo.js", "**/*.js"], + ["foo/bar/baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["src/foo.js", "*.js"], + ["foo.js", "src/*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); + +describe("path.win32.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**/*.js"], + ["foo\\bar\\baz", "foo\\[bcr]ar\\baz"], + ["foo\\bar\\baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches gattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["foo.js", "src\\*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); diff --git a/test/js/node/test/parallel/test-path-glob.js b/test/js/node/test/parallel/test-path-glob.js new file mode 100644 index 0000000000..47647e1278 --- /dev/null +++ b/test/js/node/test/parallel/test-path-glob.js @@ -0,0 +1,44 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const globs = { + win32: [ + ['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1' + ['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5' + ['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx' + ['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo' + ['foo\\bar\\baz', '*', false], // No match + ], + posix: [ + ['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar' + ['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1' + ['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5' + ['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx' + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo' + ['foo/bar/baz', '*', false], // No match + ], +}; + + +for (const [platform, platformGlobs] of Object.entries(globs)) { + for (const [pathStr, glob, expected] of platformGlobs) { + const actual = path[platform].matchesGlob(pathStr, glob); + assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`); + } +} + +// Test for non-string input +assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/); +assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/); From 178e3737120ee615c58794aa6c497619d647b51c Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Mon, 6 Jan 2025 13:46:25 -0600 Subject: [PATCH 38/56] build(bindgen): check for corresponding .zig file (#15896) Co-authored-by: Don Isaac --- src/codegen/bindgen-lib.ts | 101 ++++++++++++++++++++++++++++--------- src/codegen/bindgen.ts | 9 ++++ 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 483e6d2531..5594df29fc 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -1,5 +1,9 @@ -// This is the public API for `bind.ts` files -// It is aliased as `import {} from 'bindgen'` +/** + * This is the public API for `bind.ts` files + * It is aliased as `import {} from 'bindgen'` + * @see https://bun.sh/docs/project/bindgen + */ + import { isType, dictionaryImpl, @@ -161,41 +165,57 @@ export namespace t { /** * The DOMString type corresponds to strings. * - * **Note**: A DOMString value might include unmatched surrogate code points. + * @note A DOMString value might include unmatched surrogate code points. * Use USVString if this is not desirable. * - * https://webidl.spec.whatwg.org/#idl-DOMString + * @see https://webidl.spec.whatwg.org/#idl-DOMString */ export const DOMString = builtinType()("DOMString"); - /* - * The USVString type corresponds to scalar value strings. Depending on the + /** + * The {@link USVString} type corresponds to scalar value strings. Depending on the * context, these can be treated as sequences of code units or scalar values. * * Specifications should only use USVString for APIs that perform text * processing and need a string of scalar values to operate on. Most APIs that - * use strings should instead be using DOMString, which does not make any + * use strings should instead be using {@link DOMString}, which does not make any * interpretations of the code units in the string. When in doubt, use - * DOMString + * {@link DOMString} * - * https://webidl.spec.whatwg.org/#idl-USVString + * @see https://webidl.spec.whatwg.org/#idl-USVString */ export const USVString = builtinType()("USVString"); /** * The ByteString type corresponds to byte sequences. * - * WARNING: Specifications should only use ByteString for interfacing with protocols - * that use bytes and strings interchangeably, such as HTTP. In general, - * strings should be represented with DOMString values, even if it is expected - * that values of the string will always be in ASCII or some 8-bit character - * encoding. Sequences or frozen arrays with octet or byte elements, - * Uint8Array, or Int8Array should be used for holding 8-bit data rather than - * ByteString. + * WARNING: Specifications should only use ByteString for interfacing with + * protocols that use bytes and strings interchangeably, such as HTTP. In + * general, strings should be represented with {@link DOMString} values, even + * if it is expected that values of the string will always be in ASCII or some + * 8-bit character encoding. Sequences or frozen arrays with octet or byte + * elements, {@link Uint8Array}, or {@link Int8Array} should be used for + * holding 8-bit data rather than `ByteString`. * * https://webidl.spec.whatwg.org/#idl-ByteString */ export const ByteString = builtinType()("ByteString"); /** * DOMString but encoded as `[]const u8` + * + * ```ts + * // foo.bind.ts + * import { fn, t } from "bindgen"; + * + * export const foo = fn({ + * args: { bar: t.UTF8String }, + * }) + * ``` + * + * ```zig + * // foo.zig + * pub fn foo(bar: []const u8) void { + * // ... + * } + * ``` */ export const UTF8String = builtinType()("UTF8String"); @@ -217,7 +237,7 @@ export namespace t { /** * Reference a type by string name instead of by object reference. This is - * required in some siutations like `Request` which can take an existing + * required in some siutations like {@link Request} which can take an existing * request object in as itself. */ export function ref(name: string): Type { @@ -275,13 +295,46 @@ export namespace t { } } -export type FuncOptions = FuncMetadata & - ( - | { - variants: FuncVariant[]; - } - | FuncVariant - ); +interface FuncOptionsWithVariant extends FuncMetadata { + /** + * Declare a function with multiple overloads. Each overload gets its own + * native function named "name`n`" where `n` is the 1-based index of the + * overload. + * + * ## Example + * ```ts + * // foo.bind.ts + * import { fn } from "bindgen"; + * + * export const foo = fn({ + * variants: [ + * { + * args: { a: t.i32 }, + * ret: t.i32, + * }, + * { + * args: { a: t.i32, b: t.i32 }, + * ret: t.boolean, + * } + * ] + * }); + * ``` + * + * ```zig + * // foo.zig + * pub fn foo1(a: i32) i32 { + * return a; + * } + * + * pub fn foo2(a: i32, b: i32) bool { + * return a == b; + * } + * ``` + */ + variants: FuncVariant[]; +} +type FuncWithoutOverloads = FuncMetadata & FuncVariant; +type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; export interface FuncMetadata { /** diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index f9f9d5e402..b3d8be92c6 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -4,6 +4,7 @@ // Generated bindings are available in `bun.generated..*` in Zig, // or `Generated::::*` in C++ from including `Generated.h`. import * as path from "node:path"; +import fs from "node:fs"; import { CodeWriter, TypeImpl, @@ -1076,7 +1077,15 @@ const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["nod // Sort for deterministic output for (const fileName of [...unsortedFiles].sort()) { const zigFile = path.relative(src, fileName.replace(/\.bind\.ts$/, ".zig")); + const zigFilePath = path.join(src, zigFile); let file = files.get(zigFile); + if (!fs.existsSync(zigFilePath)) { + // It would be nice if this would generate the file with the correct boilerplate + const bindName = path.basename(fileName); + throw new Error( + `${bindName} is missing a corresponding Zig file at ${zigFile}. Please create it and make sure it matches signatures in ${bindName}.`, + ); + } if (!file) { file = { functions: [], typedefs: [] }; files.set(zigFile, file); From e92a487fad207f4a9193db18c1cca9bb4015258c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 6 Jan 2025 12:05:09 -0800 Subject: [PATCH 39/56] Handle duplicate column names and numeric column names in Bun.sql (#16178) --- src/bun.js/bindings/SQLClient.cpp | 140 +++++++++++++++++++++---- src/bun.js/bindings/bindings.zig | 26 ++++- src/sql/postgres.zig | 135 ++++++++++++++++++++---- src/sql/postgres/postgres_protocol.zig | 49 ++++++++- test/js/sql/sql.test.ts | 56 ++++++++++ 5 files changed, 359 insertions(+), 47 deletions(-) diff --git a/src/bun.js/bindings/SQLClient.cpp b/src/bun.js/bindings/SQLClient.cpp index 514af1b664..2077cb29b5 100644 --- a/src/bun.js/bindings/SQLClient.cpp +++ b/src/bun.js/bindings/SQLClient.cpp @@ -74,8 +74,28 @@ typedef struct DataCell { DataCellTag tag; DataCellValue value; uint8_t freeValue; + uint8_t _indexedColumnFlag; + uint32_t index; + + bool isIndexedColumn() const { return _indexedColumnFlag == 1; } + bool isNamedColumn() const { return _indexedColumnFlag == 0; } + bool isDuplicateColumn() const { return _indexedColumnFlag == 2; } } DataCell; +class BunStructureFlags { +public: + uint32_t flags; + + BunStructureFlags(uint32_t flags) + : flags(flags) + { + } + + bool hasIndexedColumns() const { return flags & (1 << 0); } + bool hasNamedColumns() const { return flags & (1 << 1); } + bool hasDuplicateColumns() const { return flags & (1 << 2); } +}; + static JSC::JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, DataCell& cell) { switch (cell.tag) { @@ -230,25 +250,79 @@ static JSC::JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, DataCel } } -static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject) +static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject, Bun::BunStructureFlags flags) { auto& vm = globalObject->vm(); auto* object = JSC::constructEmptyObject(vm, structure); auto scope = DECLARE_THROW_SCOPE(vm); - for (unsigned i = 0; i < count; i++) { - auto& cell = cells[i]; - JSValue value = toJS(vm, globalObject, cell); - RETURN_IF_EXCEPTION(scope, {}); - object->putDirectOffset(vm, i, value); + // TODO: once we have more tests for this, let's add another branch for + // "only mixed names and mixed indexed columns, no duplicates" + // then we cna remove this sort and instead do two passes. + if (flags.hasIndexedColumns() && flags.hasNamedColumns()) { + // sort the cells by if they're named or indexed, put named first. + // this is to conform to the Structure offsets from earlier. + std::sort(cells, cells + count, [](DataCell& a, DataCell& b) { + return a.isNamedColumn() && !b.isNamedColumn(); + }); } + // Fast path: named columns only, no duplicate columns + if (flags.hasNamedColumns() && !flags.hasDuplicateColumns() && !flags.hasIndexedColumns()) { + for (unsigned i = 0; i < count; i++) { + auto& cell = cells[i]; + JSValue value = toJS(vm, globalObject, cell); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(!cell.isDuplicateColumn()); + ASSERT(!cell.isIndexedColumn()); + ASSERT(cell.isNamedColumn()); + object->putDirectOffset(vm, i, value); + } + } else if (flags.hasIndexedColumns() && !flags.hasNamedColumns() && !flags.hasDuplicateColumns()) { + for (unsigned i = 0; i < count; i++) { + auto& cell = cells[i]; + JSValue value = toJS(vm, globalObject, cell); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(!cell.isDuplicateColumn()); + ASSERT(cell.isIndexedColumn()); + ASSERT(!cell.isNamedColumn()); + // cell.index can be > count + // for example: + // select 1 as "8", 2 as "2", 3 as "3" + // -> { "8": 1, "2": 2, "3": 3 } + // 8 > count + object->putDirectIndex(globalObject, cell.index, value); + } + } else { + unsigned structureOffsetIndex = 0; + // slow path: named columns with duplicate columns or indexed columns + for (unsigned i = 0; i < count; i++) { + auto& cell = cells[i]; + if (cell.isIndexedColumn()) { + JSValue value = toJS(vm, globalObject, cell); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(cell.index < count); + ASSERT(!cell.isNamedColumn()); + ASSERT(!cell.isDuplicateColumn()); + object->putDirectIndex(globalObject, cell.index, value); + } else if (cell.isNamedColumn()) { + JSValue value = toJS(vm, globalObject, cell); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(!cell.isIndexedColumn()); + ASSERT(!cell.isDuplicateColumn()); + ASSERT(cell.index < count); + object->putDirectOffset(vm, structureOffsetIndex++, value); + } else if (cell.isDuplicateColumn()) { + // skip it! + } + } + } return object; } -static JSC::JSValue toJS(JSC::JSArray* array, JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject) +static JSC::JSValue toJS(JSC::JSArray* array, JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject, Bun::BunStructureFlags flags) { - JSValue value = toJS(structure, cells, count, globalObject); + JSValue value = toJS(structure, cells, count, globalObject, flags); if (value.isEmpty()) return {}; @@ -268,20 +342,44 @@ static JSC::JSValue toJS(JSC::JSArray* array, JSC::Structure* structure, DataCel extern "C" EncodedJSValue JSC__constructObjectFromDataCell( JSC::JSGlobalObject* globalObject, EncodedJSValue encodedArrayValue, - EncodedJSValue encodedStructureValue, DataCell* cells, unsigned count) + EncodedJSValue encodedStructureValue, DataCell* cells, unsigned count, unsigned flags) { JSValue arrayValue = JSValue::decode(encodedArrayValue); JSValue structureValue = JSValue::decode(encodedStructureValue); auto* array = arrayValue ? jsDynamicCast(arrayValue) : nullptr; auto* structure = jsDynamicCast(structureValue); - return JSValue::encode(toJS(array, structure, cells, count, globalObject)); + return JSValue::encode(toJS(array, structure, cells, count, globalObject, Bun::BunStructureFlags(flags))); } -extern "C" EncodedJSValue JSC__createStructure(JSC::JSGlobalObject* globalObject, JSC::JSCell* owner, unsigned int inlineCapacity, BunString* names) +typedef struct ExternColumnIdentifier { + uint8_t tag; + union { + uint32_t index; + BunString name; + }; + + bool isIndexedColumn() const { return tag == 1; } + bool isNamedColumn() const { return tag == 2; } + bool isDuplicateColumn() const { return tag == 0; } +} ExternColumnIdentifier; + +extern "C" EncodedJSValue JSC__createStructure(JSC::JSGlobalObject* globalObject, JSC::JSCell* owner, unsigned int inlineCapacity, ExternColumnIdentifier* namesPtr) { auto& vm = globalObject->vm(); - Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), inlineCapacity); + + PropertyNameArray propertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + std::span names(namesPtr, inlineCapacity); + unsigned nonDuplicateCount = 0; + for (unsigned i = 0; i < inlineCapacity; i++) { + ExternColumnIdentifier& name = names[i]; + if (name.isNamedColumn()) { + propertyNames.add(Identifier::fromString(vm, name.name.toWTFString())); + } + nonDuplicateCount += !name.isDuplicateColumn(); + } + + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), std::min(nonDuplicateCount, JSFinalObject::maxInlineCapacity)); if (owner) { vm.writeBarrier(owner, structure); } else { @@ -289,14 +387,15 @@ extern "C" EncodedJSValue JSC__createStructure(JSC::JSGlobalObject* globalObject } ensureStillAliveHere(structure); - PropertyNameArray propertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); - for (unsigned i = 0; i < inlineCapacity; i++) { - propertyNames.add(Identifier::fromString(vm, names[i].toWTFString())); - } - - PropertyOffset offset = 0; - for (unsigned i = 0; i < inlineCapacity; i++) { - structure = structure->addPropertyTransition(vm, structure, propertyNames[i], 0, offset); + if (names.size() > 0) { + PropertyOffset offset = 0; + unsigned indexInPropertyNamesArray = 0; + for (unsigned i = 0; i < inlineCapacity; i++) { + ExternColumnIdentifier& name = names[i]; + if (name.isNamedColumn()) { + structure = structure->addPropertyTransition(vm, structure, propertyNames[indexInPropertyNamesArray++], 0, offset); + } + } } return JSValue::encode(structure); @@ -317,5 +416,4 @@ extern "C" void JSC__putDirectOffset(JSC::VM* vm, JSC::EncodedJSValue object, un { JSValue::decode(object).getObject()->putDirectOffset(*vm, offset, JSValue::decode(value)); } - } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index da43353ea4..eecc319413 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -102,11 +102,31 @@ pub const JSObject = extern struct { } } - extern fn JSC__createStructure(*JSC.JSGlobalObject, *JSC.JSCell, u32, names: [*]bun.String) JSC.JSValue; + extern fn JSC__createStructure(*JSC.JSGlobalObject, *JSC.JSCell, u32, names: [*]ExternColumnIdentifier, flags: u32) JSC.JSValue; - pub fn createStructure(global: *JSGlobalObject, owner: JSC.JSValue, length: u32, names: [*]bun.String) JSValue { + pub const ExternColumnIdentifier = extern struct { + tag: u8 = 0, + value: extern union { + index: u32, + name: bun.String, + }, + + pub fn string(this: *ExternColumnIdentifier) ?*bun.String { + return switch (this.tag) { + 2 => &this.value.name, + else => null, + }; + } + + pub fn deinit(this: *ExternColumnIdentifier) void { + if (this.string()) |str| { + str.deref(); + } + } + }; + pub fn createStructure(global: *JSGlobalObject, owner: JSC.JSValue, length: u32, names: [*]ExternColumnIdentifier, flags: u32) JSValue { JSC.markBinding(@src()); - return JSC__createStructure(global, owner.asCell(), length, names); + return JSC__createStructure(global, owner.asCell(), length, names, flags); } const InitializeCallback = *const fn (ctx: *anyopaque, obj: *JSObject, global: *JSGlobalObject) callconv(.C) void; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 296ce7ee1c..c0f2bbef84 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -1398,7 +1398,9 @@ pub const PostgresSQLConnection = struct { pub fn onClose(this: *PostgresSQLConnection) void { var vm = this.globalObject.bunVM(); - defer vm.drainMicrotasks(); + const loop = vm.eventLoop(); + loop.enter(); + defer loop.exit(); this.fail("Connection closed", error.ConnectionClosed); } @@ -1987,6 +1989,8 @@ pub const PostgresSQLConnection = struct { value: Value, free_value: u8 = 0, + isIndexedColumn: u8 = 0, + index: u32 = 0, pub const Tag = enum(u8) { null = 0, @@ -2280,6 +2284,13 @@ pub const PostgresSQLConnection = struct { } } + pub const Flags = packed struct(u32) { + has_indexed_columns: bool = false, + has_named_columns: bool = false, + has_duplicate_columns: bool = false, + _: u29 = 0, + }; + pub const Putter = struct { list: []DataCell, fields: []const protocol.FieldDescription, @@ -2287,16 +2298,25 @@ pub const PostgresSQLConnection = struct { count: usize = 0, globalObject: *JSC.JSGlobalObject, - extern fn JSC__constructObjectFromDataCell(*JSC.JSGlobalObject, JSValue, JSValue, [*]DataCell, u32) JSValue; - pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSValue, structure: JSValue) JSValue { - return JSC__constructObjectFromDataCell(globalObject, array, structure, this.list.ptr, @truncate(this.fields.len)); + extern fn JSC__constructObjectFromDataCell( + *JSC.JSGlobalObject, + JSValue, + JSValue, + [*]DataCell, + u32, + Flags, + ) JSValue; + + pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSValue, structure: JSValue, flags: Flags) JSValue { + return JSC__constructObjectFromDataCell(globalObject, array, structure, this.list.ptr, @truncate(this.fields.len), flags); } pub fn put(this: *Putter, index: u32, optional_bytes: ?*Data) !bool { - const oid = this.fields[index].type_oid; + const field = &this.fields[index]; + const oid = field.type_oid; debug("index: {d}, oid: {d}", .{ index, oid }); - - this.list[index] = if (optional_bytes) |data| + const cell: *DataCell = &this.list[index]; + cell.* = if (optional_bytes) |data| try DataCell.fromBytes(this.binary, oid, data.slice(), this.globalObject) else DataCell{ @@ -2306,6 +2326,21 @@ pub const PostgresSQLConnection = struct { }, }; this.count += 1; + cell.index = switch (field.name_or_index) { + // The indexed columns can be out of order. + .index => |i| i, + + else => @intCast(index), + }; + + // TODO: when duplicate and we know the result will be an object + // and not a .values() array, we can discard the data + // immediately. + cell.isIndexedColumn = switch (field.name_or_index) { + .duplicate => 2, + .index => 1, + .name => 0, + }; return true; } }; @@ -2380,6 +2415,7 @@ pub const PostgresSQLConnection = struct { .DataRow => { const request = this.current() orelse return error.ExpectedRequest; var statement = request.statement orelse return error.ExpectedStatement; + statement.checkForDuplicateFields(); var putter = DataCell.Putter{ .list = &.{}, @@ -2413,7 +2449,7 @@ pub const PostgresSQLConnection = struct { const pending_value = PostgresSQLQuery.pendingValueGetCached(request.thisValue) orelse .zero; pending_value.ensureStillAlive(); - const result = putter.toJS(this.globalObject, pending_value, statement.structure(this.js_value, this.globalObject)); + const result = putter.toJS(this.globalObject, pending_value, statement.structure(this.js_value, this.globalObject), statement.fields_flags); if (pending_value == .zero) { PostgresSQLQuery.pendingValueSetCached(request.thisValue, this.globalObject, result); @@ -2802,11 +2838,13 @@ pub const PostgresSQLConnection = struct { pub const PostgresSQLStatement = struct { cached_structure: JSC.Strong = .{}, ref_count: u32 = 1, - fields: []const protocol.FieldDescription = &[_]protocol.FieldDescription{}, + fields: []protocol.FieldDescription = &[_]protocol.FieldDescription{}, parameters: []const int4 = &[_]int4{}, signature: Signature, status: Status = Status.parsing, error_response: protocol.ErrorResponse = .{}, + needs_duplicate_check: bool = true, + fields_flags: PostgresSQLConnection.DataCell.Flags = .{}, pub const Status = enum { parsing, @@ -2827,13 +2865,58 @@ pub const PostgresSQLStatement = struct { } } + pub fn checkForDuplicateFields(this: *PostgresSQLStatement) void { + if (!this.needs_duplicate_check) return; + this.needs_duplicate_check = false; + + var seen_numbers = std.ArrayList(u32).init(bun.default_allocator); + defer seen_numbers.deinit(); + var seen_fields = bun.StringHashMap(void).init(bun.default_allocator); + seen_fields.ensureUnusedCapacity(@intCast(this.fields.len)) catch bun.outOfMemory(); + defer seen_fields.deinit(); + + // iterate backwards + var remaining = this.fields.len; + var flags: PostgresSQLConnection.DataCell.Flags = .{}; + while (remaining > 0) { + remaining -= 1; + const field: *protocol.FieldDescription = &this.fields[remaining]; + switch (field.name_or_index) { + .name => |*name| { + const seen = seen_fields.getOrPut(name.slice()) catch unreachable; + if (seen.found_existing) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } + + flags.has_named_columns = true; + }, + .index => |index| { + if (std.mem.indexOfScalar(u32, seen_numbers.items, index) != null) { + field.name_or_index = .duplicate; + flags.has_duplicate_columns = true; + } else { + seen_numbers.append(index) catch bun.outOfMemory(); + } + + flags.has_indexed_columns = true; + }, + .duplicate => { + flags.has_duplicate_columns = true; + }, + } + } + + this.fields_flags = flags; + } + pub fn deinit(this: *PostgresSQLStatement) void { debug("PostgresSQLStatement deinit", .{}); bun.assert(this.ref_count == 0); for (this.fields) |*field| { - @constCast(field).deinit(); + field.deinit(); } bun.default_allocator.free(this.fields); bun.default_allocator.free(this.parameters); @@ -2845,21 +2928,37 @@ pub const PostgresSQLStatement = struct { pub fn structure(this: *PostgresSQLStatement, owner: JSValue, globalObject: *JSC.JSGlobalObject) JSValue { return this.cached_structure.get() orelse { - const names = bun.default_allocator.alloc(bun.String, this.fields.len) catch return .undefined; + const ids = bun.default_allocator.alloc(JSC.JSObject.ExternColumnIdentifier, this.fields.len) catch return .undefined; + this.checkForDuplicateFields(); defer { - for (names) |*name| { - name.deref(); + for (ids) |*name| { + name.deinit(); } - bun.default_allocator.free(names); + bun.default_allocator.free(ids); } - for (this.fields, names) |*field, *name| { - name.* = String.fromUTF8(field.name.slice()); + + for (this.fields, ids) |*field, *id| { + id.tag = switch (field.name_or_index) { + .name => 2, + .index => 1, + .duplicate => 0, + }; + switch (field.name_or_index) { + .name => |name| { + id.value.name = String.createUTF8(name.slice()); + }, + .index => |index| { + id.value.index = index; + }, + .duplicate => {}, + } } const structure_ = JSC.JSObject.createStructure( globalObject, owner, - @truncate(this.fields.len), - names.ptr, + @truncate(ids.len), + ids.ptr, + @bitCast(this.fields_flags), ); this.cached_structure.set(globalObject, structure_); return structure_; diff --git a/src/sql/postgres/postgres_protocol.zig b/src/sql/postgres/postgres_protocol.zig index 60eeaf9f9d..2abeff0787 100644 --- a/src/sql/postgres/postgres_protocol.zig +++ b/src/sql/postgres/postgres_protocol.zig @@ -963,8 +963,47 @@ pub const DataRow = struct { pub const BindComplete = [_]u8{'2'} ++ toBytes(Int32(4)); +pub const ColumnIdentifier = union(enum) { + name: Data, + index: u32, + duplicate: void, + + pub fn init(name: Data) !@This() { + if (switch (name.slice().len) { + 1..."4294967295".len => true, + 0 => return .{ .name = .{ .empty = {} } }, + else => false, + }) might_be_int: { + // use a u64 to avoid overflow + var int: u64 = 0; + for (name.slice()) |byte| { + int = int * 10 + switch (byte) { + '0'...'9' => @as(u64, byte - '0'), + else => break :might_be_int, + }; + } + + // JSC only supports indexed property names up to 2^32 + if (int < std.math.maxInt(u32)) + return .{ .index = @intCast(int) }; + } + + return .{ .name = .{ .owned = try name.toOwned() } }; + } + + pub fn deinit(this: *@This()) void { + switch (this.*) { + .name => |*name| name.deinit(), + else => {}, + } + } +}; pub const FieldDescription = struct { - name: Data = .{ .empty = {} }, + /// JavaScriptCore treats numeric property names differently than string property names. + /// so we do the work to figure out if the property name is a number ahead of time. + name_or_index: ColumnIdentifier = .{ + .name = .{ .empty = {} }, + }, table_oid: int4 = 0, column_index: short = 0, type_oid: int4 = 0, @@ -974,7 +1013,7 @@ pub const FieldDescription = struct { } pub fn deinit(this: *@This()) void { - this.name.deinit(); + this.name_or_index.deinit(); } pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) AnyPostgresError!void { @@ -997,7 +1036,7 @@ pub const FieldDescription = struct { .table_oid = try reader.int4(), .column_index = try reader.short(), .type_oid = try reader.int4(), - .name = .{ .owned = try name.toOwned() }, + .name_or_index = try ColumnIdentifier.init(name), }; try reader.skip(2 + 4 + 2); @@ -1007,10 +1046,10 @@ pub const FieldDescription = struct { }; pub const RowDescription = struct { - fields: []const FieldDescription = &[_]FieldDescription{}, + fields: []FieldDescription = &[_]FieldDescription{}, pub fn deinit(this: *@This()) void { for (this.fields) |*field| { - @constCast(field).deinit(); + field.deinit(); } bun.default_allocator.free(this.fields); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 92fd82931b..4f16d46073 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -157,6 +157,62 @@ if (!isCI) { expect(error.code).toBe(`ERR_POSTGRES_LIFETIME_TIMEOUT`); }); + // Last one wins. + test("Handles duplicate string column names", async () => { + const result = await sql`select 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ x: 3 }]); + }); + + test("Handles numeric column names", async () => { + // deliberately out of order + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 0 as "0"`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, "0": 0 }]); + + expect(Object.keys(result[0])).toEqual(["0", "1", "2", "3"]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + // Last one wins. + test("Handles duplicate numeric column names", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "1"`; + expect(result).toEqual([{ "1": 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as x`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, x: 4 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 2 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + + // Named columns are inserted first, but they appear from JS as last. + expect(Object.keys(result[0])).toEqual(["1", "2", "3", "x"]); + }); + + test("Handles mixed column names with duplicates at the end", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x, 3 as x, 4 as "y"`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 3, y: 4 }]); + + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates at the start", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "2", 4 as "3", 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ "1": 2, "2": 3, "3": 4, x: 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + test("Uses default database without slash", async () => { const sql = postgres("postgres://localhost"); expect(sql.options.username).toBe(sql.options.database); From 0db90583e84cc449e0ec54587270f293eb821d97 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Mon, 6 Jan 2025 14:38:09 -0600 Subject: [PATCH 40/56] chore: remove unused `fifo.zig` (#16184) --- src/io/fifo.zig | 57 ------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/io/fifo.zig diff --git a/src/io/fifo.zig b/src/io/fifo.zig deleted file mode 100644 index e9a7ab1fab..0000000000 --- a/src/io/fifo.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; -const assert = bun.assert; - -/// An intrusive first in/first out linked list. -/// The element type T must have a field called "next" of type ?*T -pub fn FIFO(comptime T: type) type { - return struct { - const Self = @This(); - - in: ?*T = null, - out: ?*T = null, - - pub fn push(self: *Self, elem: *T) void { - assert(elem.next == null); - if (self.in) |in| { - in.next = elem; - self.in = elem; - } else { - assert(self.out == null); - self.in = elem; - self.out = elem; - } - } - - pub fn pop(self: *Self) ?*T { - const ret = self.out orelse return null; - self.out = ret.next; - ret.next = null; - if (self.in == ret) self.in = null; - return ret; - } - - pub fn peek(self: Self) ?*T { - return self.out; - } - - /// Remove an element from the FIFO. Asserts that the element is - /// in the FIFO. This operation is O(N), if this is done often you - /// probably want a different data structure. - pub fn remove(self: *Self, to_remove: *T) void { - if (to_remove == self.out) { - _ = self.pop(); - return; - } - var it = self.out; - while (it) |elem| : (it = elem.next) { - if (to_remove == elem.next) { - if (to_remove == self.in) self.in = elem; - elem.next = to_remove.next; - to_remove.next = null; - break; - } - } else unreachable; - } - }; -} From 193a6306d54b43d9f230874f8e6f949a1b930020 Mon Sep 17 00:00:00 2001 From: Michael H Date: Tue, 7 Jan 2025 07:59:59 +1100 Subject: [PATCH 41/56] Implement `bun add --peer ` (#16150) --- completions/bun.bash | 2 +- completions/bun.zsh | 2 + docs/cli/add.md | 8 ++ docs/guides/install/add-optional.md | 2 +- docs/install/index.md | 6 ++ src/install/install.zig | 16 ++- test/cli/install/bun-add.test.ts | 159 ++++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 4 deletions(-) diff --git a/completions/bun.bash b/completions/bun.bash index ccabb1d73b..eabdc343fb 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -87,7 +87,7 @@ _bun_completions() { GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js"; GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p"; - PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional"; + PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional --peer"; PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d"; PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]=""; PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]=""; diff --git a/completions/bun.zsh b/completions/bun.zsh index 49264ec3f9..f885ac03ad 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -35,6 +35,7 @@ _bun_add_completion() { '-D[]' \ '--development[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 @@ -339,6 +340,7 @@ _bun_install_completion() { '--development[]' \ '-D[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 diff --git a/docs/cli/add.md b/docs/cli/add.md index ff90730d73..ca9a8af46e 100644 --- a/docs/cli/add.md +++ b/docs/cli/add.md @@ -33,6 +33,14 @@ To add a package as an optional dependency (`"optionalDependencies"`): $ bun add --optional lodash ``` +## `--peer` + +To add a package as a peer dependency (`"peerDependencies"`): + +```bash +$ bun add --peer @types/bun +``` + ## `--exact` {% callout %} diff --git a/docs/guides/install/add-optional.md b/docs/guides/install/add-optional.md index 6ea2182f01..ad671aa0e0 100644 --- a/docs/guides/install/add-optional.md +++ b/docs/guides/install/add-optional.md @@ -2,7 +2,7 @@ name: Add an optional dependency --- -To add an npm package as a peer dependency, use the `--optional` flag. +To add an npm package as an optional dependency, use the `--optional` flag. ```sh $ bun add zod --optional diff --git a/docs/install/index.md b/docs/install/index.md index 379f6fcd50..0f04c92a12 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -143,6 +143,12 @@ To add a package as an optional dependency (`"optionalDependencies"`): $ bun add --optional lodash ``` +To add a package as a peer dependency (`"peerDependencies"`): + +```bash +$ bun add --peer @types/bun +``` + To install a package globally: ```bash diff --git a/src/install/install.zig b/src/install/install.zig index d16c20ce83..d7bed546cc 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -7245,6 +7245,7 @@ pub const PackageManager = struct { pub const Update = struct { development: bool = false, optional: bool = false, + peer: bool = false, }; pub fn openGlobalDir(explicit_global_dir: string) !std.fs.Dir { @@ -7659,8 +7660,13 @@ pub const PackageManager = struct { this.enable.force_save_lockfile = true; } - this.update.development = cli.development; - if (!this.update.development) this.update.optional = cli.optional; + if (cli.development) { + this.update.development = cli.development; + } else if (cli.optional) { + this.update.optional = cli.optional; + } else if (cli.peer) { + this.update.peer = cli.peer; + } switch (cli.patch) { .nothing => {}, @@ -9600,6 +9606,7 @@ pub const PackageManager = struct { clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, + clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, clap.parseParam("--filter ... Install packages for the matching workspaces") catch unreachable, clap.parseParam(" ... ") catch unreachable, @@ -9622,6 +9629,7 @@ pub const PackageManager = struct { clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, + clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, clap.parseParam(" ... \"name\" or \"name@version\" of package(s) to install") catch unreachable, }); @@ -9705,6 +9713,7 @@ pub const PackageManager = struct { development: bool = false, optional: bool = false, + peer: bool = false, omit: ?Omit = null, @@ -10181,6 +10190,7 @@ pub const PackageManager = struct { if (comptime subcommand == .add or subcommand == .install) { cli.development = args.flag("--development") or args.flag("--dev"); cli.optional = args.flag("--optional"); + cli.peer = args.flag("--peer"); cli.exact = args.flag("--exact"); } @@ -10770,6 +10780,8 @@ pub const PackageManager = struct { "devDependencies" else if (manager.options.update.optional) "optionalDependencies" + else if (manager.options.update.peer) + "peerDependencies" else "dependencies"; var any_changes = false; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 2cc47f7bef..d2deadd510 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -403,6 +403,165 @@ it("should add exact version with --exact", async () => { ); await access(join(package_dir, "bun.lockb")); }); +it("should add to devDependencies with --dev", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--dev", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + devDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to optionalDependencies with --optional", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--optional", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + optionalDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to peerDependencies with --peer", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--peer", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + peerDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); it("should add exact version with install.exact", async () => { const urls: string[] = []; From 8268af3a7d2de13f8c80859d417e7739ad033ccc Mon Sep 17 00:00:00 2001 From: 190n Date: Mon, 6 Jan 2025 13:59:50 -0800 Subject: [PATCH 42/56] Disable IPInt (#16188) --- src/bun.js/bindings/ZigGlobalObject.cpp | 3 +++ test/bun.lockb | Bin 437514 -> 437890 bytes .../@electric-sql/pglite/pglite.test.ts | 19 ++++++++++++++++++ test/package.json | 1 + 4 files changed, 23 insertions(+) create mode 100644 test/js/third_party/@electric-sql/pglite/pglite.test.ts diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index ce2cf49ca5..358e84d9da 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -243,6 +243,9 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c JSC::Options::useConcurrentJIT() = true; // JSC::Options::useSigillCrashAnalyzer() = true; JSC::Options::useWasm() = true; + // Disable IPInt, the in-place WASM interpreter, by default until it is more stable + // (it breaks pglite as of 2025-01-06) + JSC::Options::useWasmIPInt() = false; JSC::Options::useSourceProviderCache() = true; // JSC::Options::useUnlinkedCodeBlockJettisoning() = false; JSC::Options::exposeInternalModuleLoader() = true; diff --git a/test/bun.lockb b/test/bun.lockb index 7810462ead91fa59e07e7f148e01ddf1cff96bc5..091b3c31f83168e5fb9db45fcf446db096442d7c 100755 GIT binary patch delta 66568 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

m;$DPY5x7Oq|{gpQQIMHe+Gjg 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 MUL{bYZD!~H5B;!1$p8QV delta 65156 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 diff --git a/test/js/third_party/@electric-sql/pglite/pglite.test.ts b/test/js/third_party/@electric-sql/pglite/pglite.test.ts new file mode 100644 index 0000000000..45423d5a74 --- /dev/null +++ b/test/js/third_party/@electric-sql/pglite/pglite.test.ts @@ -0,0 +1,19 @@ +import { PGlite } from "@electric-sql/pglite"; + +describe("pglite", () => { + it("can initialize successfully", async () => { + const db = new PGlite(); + expect(await db.query("SELECT version()")).toEqual({ + rows: [ + { + version: + // since pglite is wasm, there is only one binary for all platforms. it always thinks it + // is x86_64-pc-linux-gnu. + "PostgreSQL 16.4 on x86_64-pc-linux-gnu, compiled by emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.72 (437140d149d9c977ffc8b09dbaf9b0f5a02db190), 32-bit", + }, + ], + fields: [{ name: "version", dataTypeID: 25 }], + affectedRows: 0, + }); + }); +}); diff --git a/test/package.json b/test/package.json index 5387f4f5cf..03fe468beb 100644 --- a/test/package.json +++ b/test/package.json @@ -11,6 +11,7 @@ "dependencies": { "@azure/service-bus": "7.9.4", "@duckdb/node-api": "1.1.3-alpha.7", + "@electric-sql/pglite": "0.2.15", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", "@napi-rs/canvas": "0.1.65", From e1cfea4925a7fadf39d04c4619a412d8b96ee5e2 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 6 Jan 2025 14:30:36 -0800 Subject: [PATCH 43/56] node: fix the rest of test-process (#16026) --- docs/runtime/nodejs-apis.md | 2 +- scripts/check-node-all.sh | 2 +- src/api/schema.zig | 2 +- src/bake/DevServer.zig | 2 +- src/bake/bake.zig | 6 +- src/bun.js/bindings/BunObject.cpp | 1 - src/bun.js/bindings/BunProcess.cpp | 707 ++++++++++++------ src/bun.js/bindings/BunProcess.h | 1 + src/bun.js/bindings/ErrorCode.cpp | 148 ++-- src/bun.js/bindings/ErrorCode.h | 11 +- src/bun.js/bindings/ErrorCode.ts | 11 + src/bun.js/bindings/ImportMetaObject.cpp | 2 - src/bun.js/bindings/JSBuffer.cpp | 22 +- src/bun.js/bindings/JSMockFunction.cpp | 1 - src/bun.js/bindings/NodeValidator.cpp | 88 ++- src/bun.js/bindings/NodeValidator.h | 6 + src/bun.js/bindings/OsBinding.cpp | 27 +- src/bun.js/bindings/ZigGlobalObject.cpp | 1 - src/bun.js/bindings/bindings.cpp | 21 +- src/bun.js/bindings/bindings.zig | 5 + src/bun.js/bindings/exports.zig | 15 +- src/bun.js/bindings/headers-handwritten.h | 1 + src/bun.js/bindings/helpers.cpp | 5 +- src/bun.js/bindings/helpers.h | 1 + src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 2 - src/bun.js/bindings/webcore/EventEmitter.cpp | 23 +- src/bun.js/bindings/webcore/EventEmitter.h | 6 +- .../webcore/JSDOMConstructorCallable.h | 73 ++ .../bindings/webcore/JSEventEmitter.cpp | 37 +- src/bun.js/bindings/webcore/JSWorker.cpp | 2 - .../bindings/webcore/JSWorkerOptions.cpp | 2 +- src/bun.js/javascript.zig | 16 +- src/bun.js/module_loader.zig | 29 +- src/bun.js/node/types.zig | 69 +- src/bun.js/test/expect.zig | 1 - src/bun.zig | 4 +- src/bundler/bundle_v2.zig | 2 +- src/c.zig | 4 + src/cli.zig | 25 +- src/cli/upgrade_command.zig | 2 +- src/codegen/generate-node-errors.ts | 4 + src/fs.zig | 16 +- src/install/install.zig | 13 +- src/install/lockfile.zig | 2 +- src/js/builtins.d.ts | 16 + src/js/builtins/BunBuiltinNames.h | 2 + src/js/builtins/ConsoleObject.ts | 8 +- src/js/internal/errors.ts | 7 - src/js/internal/shared.ts | 9 +- src/js/internal/validators.ts | 18 + src/js/node/assert.ts | 16 +- src/js/node/child_process.ts | 46 +- src/js/node/dgram.ts | 101 +-- src/js/node/diagnostics_channel.ts | 7 +- src/js/node/events.ts | 31 +- src/js/node/http.ts | 16 +- src/js/node/net.ts | 3 +- src/js/node/readline.ts | 36 +- src/js/node/stream.ts | 48 +- src/js/node/test.ts | 38 + src/js/node/timers.promises.ts | 15 +- src/js/node/util.ts | 34 +- src/js/node/zlib.ts | 16 +- src/sys.zig | 99 +-- test/bundler/bundler_compile.test.ts | 2 +- test/cli/hot/hot.test.ts | 6 +- test/harness.ts | 4 +- test/js/bun/net/socket.test.ts | 10 +- .../snapshot-tests/snapshots/snapshot.test.ts | 2 +- test/js/bun/test/stack.test.ts | 3 +- .../__snapshots__/inspect-error.test.js.snap | 8 +- test/js/node/fs/fs.test.ts | 9 +- test/js/node/net/node-net-server.test.ts | 3 +- test/js/node/process/call-constructor.test.js | 11 + test/js/node/process/process.test.js | 64 +- test/js/node/readline/readline.node.test.ts | 6 +- test/js/node/test/common/index.js | 9 +- .../test/parallel/test-child-process-stdio.js | 77 ++ .../test/parallel/test-console-tty-colors.js | 16 +- .../node/test/parallel/test-process-assert.js | 19 + .../parallel/test-process-available-memory.js | 5 + .../test-process-beforeexit-throw-exit.js | 12 + .../test/parallel/test-process-beforeexit.js | 81 ++ .../parallel/test-process-binding-util.js | 58 ++ .../test-process-chdir-errormessage.js | 20 + .../node/test/parallel/test-process-chdir.js | 44 ++ .../node/test/parallel/test-process-config.js | 69 ++ .../test-process-constrained-memory.js | 6 + .../test/parallel/test-process-cpuUsage.js | 118 +++ ...test-process-dlopen-error-message-crash.js | 47 ++ .../test/parallel/test-process-emitwarning.js | 81 ++ .../test/parallel/test-process-euid-egid.js | 70 ++ .../test-process-exception-capture-errors.js | 24 + .../test-process-exit-code-validation.js | 145 ++++ .../node/test/parallel/test-process-hrtime.js | 74 ++ .../test/parallel/test-process-kill-pid.js | 116 +++ .../parallel/test-process-no-deprecation.js | 32 + .../test/parallel/test-process-really-exit.js | 17 + .../test/parallel/test-process-release.js | 32 + .../test/parallel/test-process-setgroups.js | 55 ++ .../test/parallel/test-process-title-cli.js | 17 + .../test/parallel/test-process-uid-gid.js | 100 +++ .../test/parallel/test-process-umask-mask.js | 32 + .../node/test/parallel/test-process-umask.js | 65 ++ .../test/parallel/test-process-warning.js | 68 ++ ...est-queue-microtask-uncaught-asynchooks.js | 36 - test/js/web/fetch/fetch.stream.test.ts | 12 +- 107 files changed, 2835 insertions(+), 836 deletions(-) create mode 100644 src/bun.js/bindings/webcore/JSDOMConstructorCallable.h create mode 100644 src/js/node/test.ts create mode 100644 test/js/node/process/call-constructor.test.js create mode 100644 test/js/node/test/parallel/test-child-process-stdio.js create mode 100644 test/js/node/test/parallel/test-process-assert.js create mode 100644 test/js/node/test/parallel/test-process-available-memory.js create mode 100644 test/js/node/test/parallel/test-process-beforeexit-throw-exit.js create mode 100644 test/js/node/test/parallel/test-process-beforeexit.js create mode 100644 test/js/node/test/parallel/test-process-binding-util.js create mode 100644 test/js/node/test/parallel/test-process-chdir-errormessage.js create mode 100644 test/js/node/test/parallel/test-process-chdir.js create mode 100644 test/js/node/test/parallel/test-process-config.js create mode 100644 test/js/node/test/parallel/test-process-constrained-memory.js create mode 100644 test/js/node/test/parallel/test-process-cpuUsage.js create mode 100644 test/js/node/test/parallel/test-process-dlopen-error-message-crash.js create mode 100644 test/js/node/test/parallel/test-process-emitwarning.js create mode 100644 test/js/node/test/parallel/test-process-euid-egid.js create mode 100644 test/js/node/test/parallel/test-process-exception-capture-errors.js create mode 100644 test/js/node/test/parallel/test-process-exit-code-validation.js create mode 100644 test/js/node/test/parallel/test-process-hrtime.js create mode 100644 test/js/node/test/parallel/test-process-kill-pid.js create mode 100644 test/js/node/test/parallel/test-process-no-deprecation.js create mode 100644 test/js/node/test/parallel/test-process-really-exit.js create mode 100644 test/js/node/test/parallel/test-process-release.js create mode 100644 test/js/node/test/parallel/test-process-setgroups.js create mode 100644 test/js/node/test/parallel/test-process-title-cli.js create mode 100644 test/js/node/test/parallel/test-process-uid-gid.js create mode 100644 test/js/node/test/parallel/test-process-umask-mask.js create mode 100644 test/js/node/test/parallel/test-process-umask.js create mode 100644 test/js/node/test/parallel/test-process-warning.js delete mode 100644 test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index da92af28e4..b0e2630b6a 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -341,7 +341,7 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`process`](https://nodejs.org/api/process.html) -🟡 Missing `domain` `initgroups` `setegid` `seteuid` `setgid` `setgroups` `setuid` `allowedNodeEnvironmentFlags` `getActiveResourcesInfo` `setActiveResourcesInfo` `moduleLoadList` `setSourceMapsEnabled`. `process.binding` is partially implemented. +🟡 Missing `initgroups` `allowedNodeEnvironmentFlags` `getActiveResourcesInfo` `setActiveResourcesInfo` `moduleLoadList` `setSourceMapsEnabled`. `process.binding` is partially implemented. ### [`queueMicrotask()`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) diff --git a/scripts/check-node-all.sh b/scripts/check-node-all.sh index 9358e55ae9..4c907de593 100755 --- a/scripts/check-node-all.sh +++ b/scripts/check-node-all.sh @@ -25,7 +25,7 @@ esac export BUN_DEBUG_QUIET_LOGS=1 -for x in $(find test/js/node/test/parallel -type f -name "test-$1*.js") +for x in $(find test/js/node/test/parallel -type f -name "test-$1*.js" | sort) do i=$((i+1)) echo ./$x diff --git a/src/api/schema.zig b/src/api/schema.zig index 763d3954f1..77ed146a3e 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1631,7 +1631,7 @@ pub const Api = struct { origin: ?[]const u8 = null, /// absolute_working_dir - absolute_working_dir: ?[]const u8 = null, + absolute_working_dir: ?[:0]const u8 = null, /// define define: ?StringMap = null, diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 78aec7e11e..5f59b52401 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -13,7 +13,7 @@ pub const igLog = bun.Output.scoped(.IncrementalGraph, false); pub const Options = struct { /// Arena must live until DevServer.deinit() arena: Allocator, - root: []const u8, + root: [:0]const u8, vm: *VirtualMachine, framework: bake.Framework, bundler_options: bake.SplitBundlerOptions, diff --git a/src/bake/bake.zig b/src/bake/bake.zig index eead195a75..3722c8efdc 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -15,7 +15,7 @@ pub const UserOptions = struct { arena: std.heap.ArenaAllocator, allocations: StringRefList, - root: []const u8, + root: [:0]const u8, framework: Framework, bundler_options: SplitBundlerOptions, @@ -78,9 +78,9 @@ pub const UserOptions = struct { const StringRefList = struct { strings: std.ArrayListUnmanaged(ZigString.Slice), - pub fn track(al: *StringRefList, str: ZigString.Slice) []const u8 { + pub fn track(al: *StringRefList, str: ZigString.Slice) [:0]const u8 { al.strings.append(bun.default_allocator, str) catch bun.outOfMemory(); - return str.slice(); + return str.sliceZ(); } pub fn free(al: *StringRefList) void { diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 9693629256..bd1760cfb0 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -247,7 +247,6 @@ JSC_DEFINE_HOST_FUNCTION(functionConcatTypedArrays, (JSGlobalObject * globalObje auto arg2 = callFrame->argument(2); if (!arg2.isUndefined()) { asUint8Array = arg2.toBoolean(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); } return flattenArrayOfBuffersIntoArrayBufferOrUint8Array(globalObject, arrayValue, maxLength, asUint8Array); diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 95c5ca6f4f..3631362357 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -4,11 +4,17 @@ #include #include #include "CommonJSModuleRecord.h" +#include "ErrorCode+List.h" +#include "JavaScriptCore/ArgList.h" #include "JavaScriptCore/CallData.h" #include "JavaScriptCore/CatchScope.h" #include "JavaScriptCore/JSCJSValue.h" #include "JavaScriptCore/JSCast.h" +#include "JavaScriptCore/JSMap.h" +#include "JavaScriptCore/JSMapInlines.h" +#include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/JSString.h" +#include "JavaScriptCore/JSType.h" #include "JavaScriptCore/MathCommon.h" #include "JavaScriptCore/Protect.h" #include "JavaScriptCore/PutPropertySlot.h" @@ -35,7 +41,10 @@ #include #include "ProcessBindingTTYWrap.h" #include "wtf/text/ASCIILiteral.h" +#include "wtf/text/StringToIntegerConversion.h" #include "wtf/text/OrdinalNumber.h" +#include "NodeValidator.h" +#include "NodeModuleModule.h" #include "AsyncContextFrame.h" #include "ErrorCode.h" @@ -51,6 +60,9 @@ #include #include #include +#include +#include +#include #else #include #include @@ -89,6 +101,9 @@ typedef int mode_t; #include // setuid, getuid #endif +extern "C" bool Bun__Node__ProcessNoDeprecation; +extern "C" bool Bun__Node__ProcessThrowDeprecation; + namespace Bun { using namespace JSC; @@ -134,6 +149,8 @@ BUN_DECLARE_HOST_FUNCTION(Bun__Process__send); extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global); extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValue value); +bool setProcessExitCodeInner(JSC::JSGlobalObject* lexicalGlobalObject, Process* process, JSValue code); + static JSValue constructArch(VM& vm, JSObject* processObject) { #if CPU(X86_64) @@ -228,13 +245,9 @@ static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) auto* globalObject = processObject->globalObject(); auto* release = JSC::constructEmptyObject(globalObject); - // SvelteKit compatibility hack - release->putDirect(vm, vm.propertyNames->name, jsOwnedString(vm, WTF::String("node"_s)), 0); - - release->putDirect(vm, Identifier::fromString(vm, "lts"_s), jsBoolean(false), 0); + release->putDirect(vm, vm.propertyNames->name, jsOwnedString(vm, String("node"_s)), 0); // maybe this should be 'bun' eventually release->putDirect(vm, Identifier::fromString(vm, "sourceUrl"_s), jsOwnedString(vm, WTF::String(std::span { Bun__githubURL, strlen(Bun__githubURL) })), 0); - release->putDirect(vm, Identifier::fromString(vm, "headersUrl"_s), jsEmptyString(vm), 0); - release->putDirect(vm, Identifier::fromString(vm, "libUrl"_s), jsEmptyString(vm), 0); + release->putDirect(vm, Identifier::fromString(vm, "headersUrl"_s), jsOwnedString(vm, String("https://nodejs.org/download/release/v" REPORTED_NODEJS_VERSION "/node-v" REPORTED_NODEJS_VERSION "-headers.tar.gz"_s)), 0); return release; } @@ -262,9 +275,7 @@ static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* pro emitter.emit(event, arguments); } -JSC_DEFINE_CUSTOM_SETTER(Process_defaultSetter, - (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, - JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +JSC_DEFINE_CUSTOM_SETTER(Process_defaultSetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName propertyName)) { JSC::VM& vm = globalObject->vm(); @@ -282,8 +293,7 @@ extern "C" HMODULE Bun__LoadLibraryBunString(BunString*); extern "C" size_t Bun__process_dlopen_count; -JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, - (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); auto callCountAtStart = globalObject->napiModuleRegisterCallCount; @@ -376,8 +386,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, #else WTF::String msg = WTF::String::fromUTF8(dlerror()); #endif - JSC::throwTypeError(globalObject, scope, msg); - return {}; + return throwError(globalObject, scope, ErrorCode::ERR_DLOPEN_FAILED, msg); } if (callCountAtStart != globalObject->napiModuleRegisterCallCount) { @@ -463,32 +472,29 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, return JSValue::encode(resultValue); } -JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSGlobalObject * globalObject, CallFrame* callFrame)) { if (callFrame->argumentCount() == 0 || callFrame->argument(0).isUndefined()) { mode_t currentMask = umask(0); umask(currentMask); - return JSC::JSValue::encode(JSC::jsNumber(currentMask)); + return JSValue::encode(jsNumber(currentMask)); } auto& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); - JSValue numberValue = callFrame->argument(0); + auto value = callFrame->argument(0); - if (!numberValue.isNumber()) { - return Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "mask"_s, "number"_s, numberValue); - } - - if (!numberValue.isAnyInt()) { - return Bun::ERR::OUT_OF_RANGE(throwScope, globalObject, "mask"_s, "an integer"_s, numberValue); - } - - double number = numberValue.toNumber(globalObject); - int64_t newUmask = isInt52(number) ? tryConvertToInt52(number) : numberValue.toInt32(globalObject); - RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); - if (newUmask < 0 || newUmask > 4294967295) { - return Bun::ERR::OUT_OF_RANGE(throwScope, globalObject, "mask"_s, 0, 4294967295, numberValue); + mode_t newUmask; + if (value.isString()) { + auto str = value.getString(globalObject); + auto policy = WTF::TrailingJunkPolicy::Disallow; + auto opt = str.is8Bit() ? WTF::parseInteger(str.span8(), 8, policy) : WTF::parseInteger(str.span16(), 8, policy); + if (!opt.has_value()) return Bun::ERR::INVALID_ARG_VALUE(throwScope, globalObject, "mask"_s, value, "must be a 32-bit unsigned integer or an octal string"_s); + newUmask = opt.value(); + } else { + Bun::V::validateUint32(throwScope, globalObject, value, "mask"_s, jsUndefined()); + RETURN_IF_EXCEPTION(throwScope, {}); + newUmask = value.toUInt32(globalObject); } return JSC::JSValue::encode(JSC::jsNumber(umask(newUmask))); @@ -496,6 +502,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, extern "C" uint64_t Bun__readOriginTimer(void*); extern "C" double Bun__readOriginTimerStart(void*); +extern "C" void Bun__VirtualMachine__exitDuringUncaughtException(void*); // https://github.com/nodejs/node/blob/1936160c31afc9780e4365de033789f39b7cbc0c/src/api/hooks.cc#L49 extern "C" void Process__dispatchOnBeforeExit(Zig::GlobalObject* globalObject, uint8_t exitCode) @@ -503,11 +510,18 @@ extern "C" void Process__dispatchOnBeforeExit(Zig::GlobalObject* globalObject, u if (!globalObject->hasProcessObject()) { return; } - + auto& vm = globalObject->vm(); auto* process = jsCast(globalObject->processObject()); MarkedArgumentBuffer arguments; arguments.append(jsNumber(exitCode)); - process->wrapped().emit(Identifier::fromString(globalObject->vm(), "beforeExit"_s), arguments); + Bun__VirtualMachine__exitDuringUncaughtException(bunVM(vm)); + auto fired = process->wrapped().emit(Identifier::fromString(vm, "beforeExit"_s), arguments); + if (fired) { + if (globalObject->m_nextTickQueue) { + auto nextTickQueue = jsDynamicCast(globalObject->m_nextTickQueue.get()); + if (nextTickQueue) nextTickQueue->drain(vm, globalObject); + } + } } extern "C" void Process__dispatchOnExit(Zig::GlobalObject* globalObject, uint8_t exitCode) @@ -522,58 +536,65 @@ extern "C" void Process__dispatchOnExit(Zig::GlobalObject* globalObject, uint8_t dispatchExitInternal(globalObject, process, exitCode); } -JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, - (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { double now = static_cast(Bun__readOriginTimer(bunVM(lexicalGlobalObject))); double result = (now / 1000000.0) / 1000.0; return JSC::JSValue::encode(JSC::jsNumber(result)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionExit, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionExit, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); - uint8_t exitCode = 0; - JSValue arg0 = callFrame->argument(0); - if (arg0.isAnyInt()) { - int extiCode32 = arg0.toInt32(globalObject) % 256; - RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); - - exitCode = static_cast(extiCode32); - Bun__setExitCode(bunVM(globalObject), exitCode); - } else if (!arg0.isUndefinedOrNull()) { - throwTypeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); - return {}; - } else { - exitCode = Bun__getExitCode(bunVM(globalObject)); - } - + auto& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); auto* zigGlobal = defaultGlobalObject(globalObject); auto process = jsCast(zigGlobal->processObject()); - process->m_isExitCodeObservable = true; + auto code = callFrame->argument(0); + + setProcessExitCodeInner(globalObject, process, code); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto exitCode = Bun__getExitCode(bunVM(zigGlobal)); Process__dispatchOnExit(zigGlobal, exitCode); - Bun__Process__exit(zigGlobal, exitCode); + + // process.reallyExit(exitCode); + auto reallyExitVal = process->get(globalObject, Identifier::fromString(vm, "reallyExit"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + MarkedArgumentBuffer args; + args.append(jsNumber(exitCode)); + JSC::call(globalObject, reallyExitVal, args, ""_s); + RETURN_IF_EXCEPTION(throwScope, {}); + return JSC::JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(Process_setUncaughtExceptionCaptureCallback, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_setUncaughtExceptionCaptureCallback, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { - auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSValue arg0 = callFrame->argument(0); - if (!arg0.isCallable() && !arg0.isNull()) { - throwTypeError(globalObject, throwScope, "The \"callback\" argument must be callable or null"_s); - return {}; + auto* globalObject = reinterpret_cast(lexicalGlobalObject); + auto& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto arg0 = callFrame->argument(0); + auto process = jsCast(globalObject->processObject()); + + if (arg0.isNull()) { + process->setUncaughtExceptionCaptureCallback(arg0); + process->m_reportOnUncaughtException = false; + return JSC::JSValue::encode(jsUndefined()); } - auto* zigGlobal = defaultGlobalObject(globalObject); - jsCast(zigGlobal->processObject())->setUncaughtExceptionCaptureCallback(arg0); + if (!arg0.isCallable()) { + return Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "fn"_s, "function or null"_s, arg0); + } + if (process->m_reportOnUncaughtException) { + return Bun::ERR::UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(throwScope, globalObject); + } + + process->setUncaughtExceptionCaptureCallback(arg0); + process->m_reportOnUncaughtException = true; return JSC::JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(Process_hasUncaughtExceptionCaptureCallback, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_hasUncaughtExceptionCaptureCallback, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto* zigGlobal = defaultGlobalObject(globalObject); JSValue cb = jsCast(zigGlobal->processObject())->getUncaughtExceptionCaptureCallback(); @@ -586,12 +607,9 @@ JSC_DEFINE_HOST_FUNCTION(Process_hasUncaughtExceptionCaptureCallback, extern "C" uint64_t Bun__readOriginTimer(void*); -JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, - (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { - - Zig::GlobalObject* globalObject - = reinterpret_cast(globalObject_); + Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); auto& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -599,29 +617,24 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, int64_t seconds = static_cast(time / 1000000000); int64_t nanoseconds = time % 1000000000; - if (callFrame->argumentCount() > 0) { - JSC::JSValue arg0 = callFrame->uncheckedArgument(0); - if (!arg0.isUndefinedOrNull()) { - JSArray* relativeArray = JSC::jsDynamicCast(arg0); - if ((!relativeArray && !arg0.isUndefinedOrNull()) || relativeArray->length() < 2) { - JSC::throwTypeError(globalObject, throwScope, "hrtime() argument must be an array or undefined"_s); - return {}; - } - JSValue relativeSecondsValue = relativeArray->getIndexQuickly(0); - JSValue relativeNanosecondsValue = relativeArray->getIndexQuickly(1); - if (!relativeSecondsValue.isNumber() || !relativeNanosecondsValue.isNumber()) { - JSC::throwTypeError(globalObject, throwScope, "hrtime() argument must be an array of 2 integers"_s); - return {}; - } + auto arg0 = callFrame->argument(0); + if (callFrame->argumentCount() > 0 && !arg0.isUndefined()) { + JSArray* relativeArray = JSC::jsDynamicCast(arg0); + if (!relativeArray) { + return Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "time"_s, "Array"_s, arg0); + } + if (relativeArray->length() != 2) return Bun::ERR::OUT_OF_RANGE(throwScope, globalObject_, "time"_s, "2"_s, jsNumber(relativeArray->length())); - int64_t relativeSeconds = JSC__JSValue__toInt64(JSC::JSValue::encode(relativeSecondsValue)); - int64_t relativeNanoseconds = JSC__JSValue__toInt64(JSC::JSValue::encode(relativeNanosecondsValue)); - seconds -= relativeSeconds; - nanoseconds -= relativeNanoseconds; - if (nanoseconds < 0) { - seconds--; - nanoseconds += 1000000000; - } + JSValue relativeSecondsValue = relativeArray->getIndexQuickly(0); + JSValue relativeNanosecondsValue = relativeArray->getIndexQuickly(1); + + int64_t relativeSeconds = JSC__JSValue__toInt64(JSC::JSValue::encode(relativeSecondsValue)); + int64_t relativeNanoseconds = JSC__JSValue__toInt64(JSC::JSValue::encode(relativeNanosecondsValue)); + seconds -= relativeSeconds; + nanoseconds -= relativeNanoseconds; + if (nanoseconds < 0) { + seconds--; + nanoseconds += 1000000000; } } @@ -646,24 +659,22 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(array)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, - (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); return JSC::JSValue::encode(JSValue(JSC::JSBigInt::createFrom(globalObject, Bun__readOriginTimer(globalObject->bunVM())))); } -JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - ZigString str = ZigString { nullptr, 0 }; - if (callFrame->argumentCount() > 0) { - str = Zig::toZigString(callFrame->uncheckedArgument(0).toWTFString(globalObject)); - } + auto value = callFrame->argument(0); + Bun::V::validateString(scope, globalObject, value, "directory"_s); + RETURN_IF_EXCEPTION(scope, {}); + ZigString str = Zig::toZigString(value.toWTFString(globalObject)); JSC::JSValue result = JSC::JSValue::decode(Bun__Process__setCwd(globalObject, &str)); RETURN_IF_EXCEPTION(scope, {}); @@ -1112,6 +1123,37 @@ Process::~Process() { } +JSC_DEFINE_HOST_FUNCTION(jsFunction_emitWarning, (Zig::JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto* globalObject = jsCast(lexicalGlobalObject); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* process = jsCast(globalObject->processObject()); + auto value = callFrame->argument(0); + + auto ident = builtinNames(vm).warningPublicName(); + if (process->wrapped().hasEventListeners(ident)) { + JSC::MarkedArgumentBuffer args; + args.append(value); + process->wrapped().emit(ident, args); + return JSValue::encode(jsUndefined()); + } + + auto jsArgs = JSValue::encode(value); + Bun__ConsoleObject__messageWithTypeAndLevel(reinterpret_cast(globalObject->consoleClient().get())->m_client, static_cast(MessageType::Log), static_cast(MessageLevel::Warning), globalObject, &jsArgs, 1); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunction_throwValue, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto value = callFrame->argument(0); + scope.throwException(globalObject, value); + return {}; +} + JSC_DEFINE_HOST_FUNCTION(Process_functionAbort, (JSGlobalObject * globalObject, CallFrame*)) { #if OS(WINDOWS) @@ -1127,40 +1169,89 @@ JSC_DEFINE_HOST_FUNCTION(Process_emitWarning, (JSGlobalObject * lexicalGlobalObj Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - - if (callFrame->argumentCount() < 1) { - throwVMError(globalObject, scope, "Not enough arguments"_s); - return {}; - } - - RETURN_IF_EXCEPTION(scope, {}); - auto* process = jsCast(globalObject->processObject()); - JSObject* errorInstance = ([&]() -> JSObject* { - JSValue arg0 = callFrame->uncheckedArgument(0); - if (!arg0.isEmpty() && arg0.isCell() && arg0.asCell()->type() == ErrorInstanceType) { - return arg0.getObject(); - } + auto warning = callFrame->argument(0); + auto type = callFrame->argument(1); + auto code = callFrame->argument(2); + auto ctor = callFrame->argument(3); + auto detail = jsUndefined(); - WTF::String str = arg0.toWTFString(globalObject); - auto err = createError(globalObject, str); - err->putDirect(vm, vm.propertyNames->name, jsString(vm, String("warn"_s)), JSC::PropertyAttribute::DontEnum | 0); - return err; - })(); + auto dep_warning = jsString(vm, String("DeprecationWarning"_s)); - auto ident = Identifier::fromString(vm, "warning"_s); - if (process->wrapped().hasEventListeners(ident)) { - JSC::MarkedArgumentBuffer args; - args.append(errorInstance); - - process->wrapped().emit(ident, args); + if (Bun__Node__ProcessNoDeprecation && JSC::JSValue::strictEqual(globalObject, type, dep_warning)) { return JSValue::encode(jsUndefined()); } - auto jsArgs = JSValue::encode(errorInstance); - Bun__ConsoleObject__messageWithTypeAndLevel(reinterpret_cast(globalObject->consoleClient().get())->m_client, static_cast(MessageType::Log), static_cast(MessageLevel::Warning), globalObject, &jsArgs, 1); - RETURN_IF_EXCEPTION(scope, {}); + if (!type.isNull() && type.isObject() && !isJSArray(type)) { + ctor = type.get(globalObject, Identifier::fromString(vm, "ctor"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + code = type.get(globalObject, builtinNames(vm).codePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + + detail = type.get(globalObject, vm.propertyNames->detail); + RETURN_IF_EXCEPTION(scope, {}); + if (!detail.isString()) detail = jsUndefined(); + + type = type.get(globalObject, vm.propertyNames->type); + RETURN_IF_EXCEPTION(scope, {}); + if (!type.toBoolean(globalObject)) type = jsString(vm, String("Warning"_s)); + } else if (type.isCallable()) { + ctor = type; + code = jsUndefined(); + type = jsString(vm, String("Warning"_s)); + } + + if (!type.isUndefined()) { + Bun::V::validateString(scope, globalObject, type, "type"_s); + RETURN_IF_EXCEPTION(scope, {}); + } else { + type = jsString(vm, String("Warning"_s)); + } + + if (code.isCallable()) { + ctor = code; + code = jsUndefined(); + } else if (!code.isUndefined()) { + Bun::V::validateString(scope, globalObject, code, "code"_s); + RETURN_IF_EXCEPTION(scope, {}); + } + + JSObject* errorInstance; + + if (warning.isString()) { + auto s = warning.getString(globalObject); + errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s); + errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0); + } else if (warning.isCell() && warning.asCell()->type() == ErrorInstanceType) { + errorInstance = warning.getObject(); + } else { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "warning"_s, "string or Error"_s, warning); + } + + if (!code.isUndefined()) errorInstance->putDirect(vm, builtinNames(vm).codePublicName(), code, JSC::PropertyAttribute::DontEnum | 0); + if (!detail.isUndefined()) errorInstance->putDirect(vm, vm.propertyNames->detail, detail, JSC::PropertyAttribute::DontEnum | 0); + // ErrorCaptureStackTrace(warning, ctor || process.emitWarning); + + if (JSC::JSValue::strictEqual(globalObject, type, dep_warning)) { + if (Bun__Node__ProcessNoDeprecation) { + return JSValue::encode(jsUndefined()); + } + if (Bun__Node__ProcessThrowDeprecation) { + // // Delay throwing the error to guarantee that all former warnings were properly logged. + // return process.nextTick(() => { + // throw warning; + // }); + auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_throwValue, JSC::ImplementationVisibility::Private); + process->queueNextTick(vm, globalObject, func, errorInstance); + return JSValue::encode(jsUndefined()); + } + } + + // process.nextTick(doEmitWarning, warning); + auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_emitWarning, JSC::ImplementationVisibility::Private); + process->queueNextTick(vm, globalObject, func, errorInstance); return JSValue::encode(jsUndefined()); } @@ -1177,28 +1268,39 @@ JSC_DEFINE_CUSTOM_GETTER(processExitCode, (JSC::JSGlobalObject * lexicalGlobalOb return JSValue::encode(jsNumber(Bun__getExitCode(jsCast(process->globalObject())->bunVM()))); } +bool setProcessExitCodeInner(JSC::JSGlobalObject* lexicalGlobalObject, Process* process, JSValue code) +{ + auto throwScope = DECLARE_THROW_SCOPE(process->vm()); + + if (!code.isUndefinedOrNull()) { + if (code.isString() && !code.getString(lexicalGlobalObject).isEmpty()) { + auto num = code.toNumber(lexicalGlobalObject); + if (!std::isnan(num)) { + code = jsDoubleNumber(num); + } + } + Bun::V::validateInteger(throwScope, lexicalGlobalObject, code, "code"_s, jsUndefined(), jsUndefined()); + RETURN_IF_EXCEPTION(throwScope, false); + + int exitCodeInt = code.toInt32(lexicalGlobalObject) % 256; + RETURN_IF_EXCEPTION(throwScope, false); + + process->m_isExitCodeObservable = true; + void* ptr = jsCast(process->globalObject())->bunVM(); + Bun__setExitCode(ptr, static_cast(exitCodeInt)); + } + return true; +} JSC_DEFINE_CUSTOM_SETTER(setProcessExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) { Process* process = jsDynamicCast(JSValue::decode(thisValue)); if (!process) { return false; } - auto throwScope = DECLARE_THROW_SCOPE(process->vm()); - JSValue exitCode = JSValue::decode(value); - if (!exitCode.isAnyInt()) { - throwTypeError(lexicalGlobalObject, throwScope, "exitCode must be an integer"_s); - return false; - } + auto code = JSValue::decode(value); - int exitCodeInt = exitCode.toInt32(lexicalGlobalObject) % 256; - RETURN_IF_EXCEPTION(throwScope, false); - - process->m_isExitCodeObservable = true; - void* ptr = jsCast(process->globalObject())->bunVM(); - Bun__setExitCode(ptr, static_cast(exitCodeInt)); - - return true; + return setProcessExitCodeInner(lexicalGlobalObject, process, code); } JSC_DEFINE_CUSTOM_GETTER(processConnected, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) @@ -1832,10 +1934,19 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionWriteReport, (JSGlobalObject * globalOb static JSValue constructProcessReportObject(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); - auto* report = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 4); - report->putDirect(vm, JSC::Identifier::fromString(vm, "getReport"_s), JSC::JSFunction::create(vm, globalObject, 0, String("getReport"_s), Process_functionGetReport, ImplementationVisibility::Public), 0); + // auto* globalObject = reinterpret_cast(lexicalGlobalObject); + auto process = jsCast(processObject); + + auto* report = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 10); + report->putDirect(vm, JSC::Identifier::fromString(vm, "compact"_s), JSC::jsBoolean(false), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "directory"_s), JSC::jsEmptyString(vm), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "filename"_s), JSC::jsEmptyString(vm), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "getReport"_s), JSC::JSFunction::create(vm, globalObject, 0, String("getReport"_s), Process_functionGetReport, ImplementationVisibility::Public), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "reportOnFatalError"_s), JSC::jsBoolean(false), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "reportOnSignal"_s), JSC::jsBoolean(false), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "reportOnUncaughtException"_s), JSC::jsBoolean(process->m_reportOnUncaughtException), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "writeReport"_s), JSC::JSFunction::create(vm, globalObject, 1, String("writeReport"_s), Process_functionWriteReport, ImplementationVisibility::Public), 0); return report; } @@ -1867,24 +1978,22 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) // } // } JSC::JSObject* config = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); - JSC::JSObject* variables = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1); - variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i8n_support"_s), - JSC::jsNumber(1), 0); + JSC::JSObject* variables = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i8n_support"_s), JSC::jsNumber(1), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "enable_lto"_s), JSC::jsBoolean(false), 0); config->putDirect(vm, JSC::Identifier::fromString(vm, "target_defaults"_s), JSC::constructEmptyObject(globalObject), 0); config->putDirect(vm, JSC::Identifier::fromString(vm, "variables"_s), variables, 0); + config->freeze(vm); return config; } static JSValue constructProcessHrtimeObject(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); - JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, - String("hrtime"_s), Process_functionHRTime, ImplementationVisibility::Public); + JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, String("hrtime"_s), Process_functionHRTime, ImplementationVisibility::Public); - JSC::JSFunction* hrtimeBigInt = JSC::JSFunction::create(vm, globalObject, 0, - String("bigint"_s), Process_functionHRTimeBigInt, ImplementationVisibility::Public); + JSC::JSFunction* hrtimeBigInt = JSC::JSFunction::create(vm, globalObject, 0, String("bigint"_s), Process_functionHRTimeBigInt, ImplementationVisibility::Public); hrtime->putDirect(vm, JSC::Identifier::fromString(vm, "bigint"_s), hrtimeBigInt); @@ -1974,6 +2083,16 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) RELEASE_AND_RETURN(scope, result); } +JSC_DEFINE_CUSTOM_GETTER(processThrowDeprecation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) +{ + return JSValue::encode(jsBoolean(Bun__Node__ProcessThrowDeprecation)); +} + +JSC_DEFINE_CUSTOM_SETTER(setProcessThrowDeprecation, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName)) +{ + return true; +} + static JSValue constructProcessSend(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); @@ -1996,7 +2115,7 @@ JSC_DEFINE_HOST_FUNCTION(Bun__Process__disconnect, (JSGlobalObject * globalObjec auto global = jsCast(globalObject); if (!Bun__GlobalObject__hasIPC(globalObject)) { - Process__emitErrorEvent(global, jsFunction_ERR_IPC_DISCONNECTED(globalObject, nullptr)); + Process__emitErrorEvent(global, JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s))); return JSC::JSValue::encode(jsUndefined()); } @@ -2154,6 +2273,167 @@ JSC_DEFINE_HOST_FUNCTION(Process_functiongetgroups, (JSGlobalObject * globalObje } return JSValue::encode(groups); } + +static JSValue maybe_uid_by_name(JSC::ThrowScope& throwScope, JSGlobalObject* globalObject, JSValue value) +{ + if (!value.isNumber() && !value.isString()) return JSValue::decode(Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "id"_s, "number or string"_s, value)); + if (!value.isString()) return value; + + auto str = value.getString(globalObject); + if (!str.is8Bit()) { + auto message = makeString("User identifier does not exist: "_s, str); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_CREDENTIAL, message)); + return {}; + } + + auto name = (const char*)(str.span8().data()); + struct passwd pwd; + struct passwd* pp = nullptr; + char buf[8192]; + + if (getpwnam_r(name, &pwd, buf, sizeof(buf), &pp) == 0 && pp != nullptr) { + return jsNumber(pp->pw_uid); + } + + auto message = makeString("User identifier does not exist: "_s, str); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_CREDENTIAL, message)); + return {}; +} + +static JSValue maybe_gid_by_name(JSC::ThrowScope& throwScope, JSGlobalObject* globalObject, JSValue value) +{ + if (!value.isNumber() && !value.isString()) return JSValue::decode(Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "id"_s, "number or string"_s, value)); + if (!value.isString()) return value; + + auto str = value.getString(globalObject); + if (!str.is8Bit()) { + auto message = makeString("Group identifier does not exist: "_s, str); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_CREDENTIAL, message)); + return {}; + } + + auto name = (const char*)(str.span8().data()); + struct group pwd; + struct group* pp = nullptr; + char buf[8192]; + + if (getgrnam_r(name, &pwd, buf, sizeof(buf), &pp) == 0 && pp != nullptr) { + return jsNumber(pp->gr_gid); + } + + auto message = makeString("Group identifier does not exist: "_s, str); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_CREDENTIAL, message)); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionsetuid, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto value = callFrame->argument(0); + auto is_number = value.isNumber(); + value = maybe_uid_by_name(scope, globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + if (is_number) Bun::V::validateInteger(scope, globalObject, value, "id"_s, jsNumber(0), jsNumber(std::pow(2, 31) - 1)); + RETURN_IF_EXCEPTION(scope, {}); + auto id = value.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto result = setuid(id); + if (result != 0) throwSystemError(scope, globalObject, "setuid"_s, errno); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsNumber(result)); +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionseteuid, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto value = callFrame->argument(0); + auto is_number = value.isNumber(); + value = maybe_uid_by_name(scope, globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + if (is_number) Bun::V::validateInteger(scope, globalObject, value, "id"_s, jsNumber(0), jsNumber(std::pow(2, 31) - 1)); + RETURN_IF_EXCEPTION(scope, {}); + auto id = value.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto result = seteuid(id); + if (result != 0) throwSystemError(scope, globalObject, "seteuid"_s, errno); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsNumber(result)); +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionsetegid, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto value = callFrame->argument(0); + auto is_number = value.isNumber(); + value = maybe_gid_by_name(scope, globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + if (is_number) Bun::V::validateInteger(scope, globalObject, value, "id"_s, jsNumber(0), jsNumber(std::pow(2, 31) - 1)); + RETURN_IF_EXCEPTION(scope, {}); + auto id = value.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto result = setegid(id); + if (result != 0) throwSystemError(scope, globalObject, "setegid"_s, errno); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsNumber(result)); +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionsetgid, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto value = callFrame->argument(0); + auto is_number = value.isNumber(); + value = maybe_gid_by_name(scope, globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + if (is_number) Bun::V::validateInteger(scope, globalObject, value, "id"_s, jsNumber(0), jsNumber(std::pow(2, 31) - 1)); + RETURN_IF_EXCEPTION(scope, {}); + auto id = value.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto result = setgid(id); + if (result != 0) throwSystemError(scope, globalObject, "setgid"_s, errno); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsNumber(result)); +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionsetgroups, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto groups = callFrame->argument(0); + Bun::V::validateArray(scope, globalObject, groups, "groups"_s, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + auto groupsArray = JSC::jsDynamicCast(groups); + auto count = groupsArray->length(); + gid_t groupsStack[64]; + if (count > 64) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "groups.length"_s, 0, 64, groups); + + for (unsigned i = 0; i < count; i++) { + auto item = groupsArray->getIndexQuickly(i); + auto name = makeString("groups["_s, i, "]"_s); + + if (item.isNumber()) { + Bun::V::validateUint32(scope, globalObject, item, jsString(vm, name), jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + groupsStack[i] = item.toUInt32(globalObject); + continue; + } else if (item.isString()) { + item = maybe_gid_by_name(scope, globalObject, item); + RETURN_IF_EXCEPTION(scope, {}); + groupsStack[i] = item.toUInt32(globalObject); + continue; + } + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "number or string"_s, item); + } + + auto result = setgroups(count, groupsStack); + if (result != 0) throwSystemError(scope, globalObject, "setgid"_s, errno); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsNumber(result)); +} + #endif JSC_DEFINE_HOST_FUNCTION(Process_functionAssert, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -2163,18 +2443,22 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionAssert, (JSGlobalObject * globalObject, JSValue arg0 = callFrame->argument(0); bool condition = arg0.toBoolean(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); if (condition) { return JSValue::encode(jsUndefined()); } - JSValue arg1 = callFrame->argument(1); - String message = arg1.isUndefined() ? String() : arg1.toWTFString(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); - auto error = createError(globalObject, makeString("Assertion failed: "_s, message)); - error->putDirect(vm, Identifier::fromString(vm, "code"_s), jsString(vm, makeString("ERR_ASSERTION"_s))); - throwException(globalObject, throwScope, error); - return {}; + auto msg = callFrame->argument(1); + auto msgb = msg.toBoolean(globalObject); + if (msgb) { + return Bun::ERR::ASSERTION(throwScope, globalObject, msg); + } + return Bun::ERR::ASSERTION(throwScope, globalObject, "assertion error"_s); +} + +extern "C" uint64_t Bun__Os__getFreeMemory(void); +JSC_DEFINE_HOST_FUNCTION(Process_availableMemory, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return JSValue::encode(jsDoubleNumber(Bun__Os__getFreeMemory())); } #define PROCESS_BINDING_NOT_IMPLEMENTED_ISSUE(str, issue) \ @@ -2270,12 +2554,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyExit, (JSGlobalObject * globalObj JSValue arg0 = callFrame->argument(0); if (arg0.isAnyInt()) { exitCode = static_cast(arg0.toInt32(globalObject) % 256); - RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); - } else if (!arg0.isUndefinedOrNull()) { - throwTypeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); - return {}; - } else { - exitCode = Bun__getExitCode(bunVM(globalObject)); + RETURN_IF_EXCEPTION(throwScope, {}); } auto* zigGlobal = defaultGlobalObject(globalObject); @@ -2346,18 +2625,12 @@ static Process* getProcessObject(JSC::JSGlobalObject* lexicalGlobalObject, JSVal return process; } -JSC_DEFINE_HOST_FUNCTION(Process_functionConstrainedMemory, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionConstrainedMemory, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { -#if OS(LINUX) || OS(FREEBSD) return JSValue::encode(jsDoubleNumber(static_cast(WTF::ramSize()))); -#else - return JSValue::encode(jsUndefined()); -#endif } -JSC_DEFINE_HOST_FUNCTION(Process_functionCpuUsage, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionCpuUsage, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -2389,8 +2662,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCpuUsage, if (!comparatorValue.isUndefined()) { JSC::JSObject* comparator = comparatorValue.getObject(); if (UNLIKELY(!comparator)) { - throwTypeError(globalObject, throwScope, "Expected an object as the first argument"_s); - return JSC::JSValue::encode(JSC::jsUndefined()); + return Bun::ERR::INVALID_ARG_TYPE(throwScope, globalObject, "prevValue"_s, "object"_s, comparatorValue); } JSValue userValue; @@ -2401,33 +2673,29 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCpuUsage, systemValue = comparator->getDirect(1); } else { userValue = comparator->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "user"_s)); - RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); + RETURN_IF_EXCEPTION(throwScope, {}); + if (userValue.isEmpty()) userValue = jsUndefined(); systemValue = comparator->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "system"_s)); - RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); + RETURN_IF_EXCEPTION(throwScope, {}); + if (systemValue.isEmpty()) systemValue = jsUndefined(); } - if (UNLIKELY(!userValue || !userValue.isNumber())) { - throwTypeError(globalObject, throwScope, "Expected a number for the 'user' property"_s); - return JSC::JSValue::encode(JSC::jsUndefined()); - } + Bun::V::validateNumber(throwScope, globalObject, userValue, "prevValue.user"_s, jsUndefined(), jsUndefined()); + RETURN_IF_EXCEPTION(throwScope, {}); - if (UNLIKELY(!systemValue || !systemValue.isNumber())) { - throwTypeError(globalObject, throwScope, "Expected a number for the 'system' property"_s); - return JSC::JSValue::encode(JSC::jsUndefined()); - } + Bun::V::validateNumber(throwScope, globalObject, systemValue, "prevValue.system"_s, jsUndefined(), jsUndefined()); + RETURN_IF_EXCEPTION(throwScope, {}); double userComparator = userValue.toNumber(globalObject); double systemComparator = systemValue.toNumber(globalObject); - if (userComparator > JSC::maxSafeInteger() || userComparator < 0 || std::isnan(userComparator)) { - throwRangeError(globalObject, throwScope, "The 'user' property must be a number between 0 and 2^53"_s); - return JSC::JSValue::encode(JSC::jsUndefined()); + if (!(userComparator >= 0 && userComparator <= JSC::maxSafeInteger())) { + return Bun::ERR::INVALID_ARG_VALUE_RangeError(throwScope, globalObject, "prevValue.user"_s, userValue, "is invalid"_s); } - if (systemComparator > JSC::maxSafeInteger() || systemComparator < 0 || std::isnan(systemComparator)) { - throwRangeError(globalObject, throwScope, "The 'system' property must be a number between 0 and 2^53"_s); - return JSC::JSValue::encode(JSC::jsUndefined()); + if (!(systemComparator >= 0 && systemComparator <= JSC::maxSafeInteger())) { + return Bun::ERR::INVALID_ARG_VALUE_RangeError(throwScope, globalObject, "prevValue.system"_s, systemValue, "is invalid"_s); } user -= userComparator; @@ -2529,8 +2797,7 @@ err: #endif } -JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -2544,7 +2811,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, JSC::JSObject* result = JSC::constructEmptyObject(vm, process->memoryUsageStructure()); if (UNLIKELY(throwScope.exception())) { - return JSC::JSValue::encode(JSC::JSValue {}); + return {}; } // Node.js: @@ -2581,8 +2848,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(result)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsageRSS, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsageRSS, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -2725,7 +2991,8 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec { JSValue nextTickQueueObject; if (!globalObject->m_nextTickQueue) { - nextTickQueueObject = Bun::JSNextTickQueue::create(globalObject); + auto nextTickQueue = Bun::JSNextTickQueue::create(globalObject); + nextTickQueueObject = nextTickQueue; globalObject->m_nextTickQueue.set(vm, globalObject, nextTickQueueObject); } else { nextTickQueueObject = jsCast(globalObject->m_nextTickQueue.get()); @@ -2754,6 +3021,17 @@ static JSValue constructProcessNextTickFn(VM& vm, JSObject* processObject) return jsCast(processObject)->constructNextTickFn(globalObject->vm(), globalObject); } +JSC_DEFINE_CUSTOM_GETTER(processNoDeprecation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) +{ + return JSValue::encode(jsBoolean(Bun__Node__ProcessNoDeprecation)); +} + +JSC_DEFINE_CUSTOM_SETTER(setProcessNoDeprecation, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName)) +{ + Bun__Node__ProcessNoDeprecation = JSC::JSValue::decode(encodedValue).toBoolean(globalObject); + return true; +} + static JSValue constructFeatures(VM& vm, JSObject* processObject) { // { @@ -2800,9 +3078,7 @@ JSC_DEFINE_CUSTOM_GETTER(processDebugPort, (JSC::JSGlobalObject * globalObject, return JSC::JSValue::encode(jsNumber(_debugPort)); } -JSC_DEFINE_CUSTOM_SETTER(setProcessDebugPort, - (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, - JSC::EncodedJSValue encodedValue, JSC::PropertyName)) +JSC_DEFINE_CUSTOM_SETTER(setProcessDebugPort, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName)) { auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2843,9 +3119,7 @@ JSC_DEFINE_CUSTOM_GETTER(processTitle, (JSC::JSGlobalObject * globalObject, JSC: #endif } -JSC_DEFINE_CUSTOM_SETTER(setProcessTitle, - (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, - JSC::EncodedJSValue value, JSC::PropertyName)) +JSC_DEFINE_CUSTOM_SETTER(setProcessTitle, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) { JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2889,14 +3163,12 @@ extern "C" EncodedJSValue Process__getCachedCwd(JSC::JSGlobalObject* globalObjec return JSValue::encode(getCachedCwd(globalObject)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { return JSValue::encode(getCachedCwd(globalObject)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); @@ -2922,13 +3194,18 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(result))); } -JSC_DEFINE_HOST_FUNCTION(Process_functionKill, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); auto pid_value = callFrame->argument(0); + + // this is mimicking `if (pid != (pid | 0)) {` int pid = pid_value.toInt32(globalObject); RETURN_IF_EXCEPTION(scope, {}); + if (!JSC::JSValue::equal(globalObject, pid_value, jsNumber(pid))) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "pid"_s, "number"_s, pid_value); + } + JSC::JSValue signalValue = callFrame->argument(1); int signal = SIGTERM; if (signalValue.isNumber()) { @@ -3022,6 +3299,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu argv constructArgv PropertyCallback argv0 constructArgv0 PropertyCallback assert Process_functionAssert Function 1 + availableMemory Process_availableMemory Function 0 binding Process_functionBinding Function 1 browser constructBrowser PropertyCallback chdir Process_functionChdir Function 1 @@ -3039,7 +3317,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu execArgv constructExecArgv PropertyCallback execPath constructExecPath PropertyCallback exit Process_functionExit Function 1 - exitCode processExitCode CustomAccessor + exitCode processExitCode CustomAccessor|DontDelete features constructFeatures PropertyCallback getActiveResourcesInfo Process_stubFunctionReturningArray Function 0 hasUncaughtExceptionCaptureCallback Process_hasUncaughtExceptionCaptureCallback Function 0 @@ -3050,6 +3328,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu memoryUsage constructMemoryUsage PropertyCallback moduleLoadList Process_stubEmptyArray PropertyCallback nextTick constructProcessNextTickFn PropertyCallback + noDeprecation processNoDeprecation CustomAccessor openStdin Process_functionOpenStdin Function 0 pid constructPid PropertyCallback platform constructPlatform PropertyCallback @@ -3064,6 +3343,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu stderr constructStderr PropertyCallback stdin constructStdin PropertyCallback stdout constructStdout PropertyCallback + throwDeprecation processThrowDeprecation CustomAccessor title processTitle CustomAccessor umask Process_functionUmask Function 1 uptime Process_functionUptime Function 1 @@ -3081,12 +3361,19 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu _stopProfilerIdleNotifier Process_stubEmptyFunction Function 0 _tickCallback Process_stubEmptyFunction Function 0 _kill Process_functionReallyKill Function 2 + #if !OS(WINDOWS) getegid Process_functiongetegid Function 0 geteuid Process_functiongeteuid Function 0 getgid Process_functiongetgid Function 0 getgroups Process_functiongetgroups Function 0 getuid Process_functiongetuid Function 0 + + setegid Process_functionsetegid Function 1 + seteuid Process_functionseteuid Function 1 + setgid Process_functionsetgid Function 1 + setgroups Process_functionsetgroups Function 1 + setuid Process_functionsetuid Function 1 #endif @end */ diff --git a/src/bun.js/bindings/BunProcess.h b/src/bun.js/bindings/BunProcess.h index 368d93ae8b..3fbbfd0142 100644 --- a/src/bun.js/bindings/BunProcess.h +++ b/src/bun.js/bindings/BunProcess.h @@ -36,6 +36,7 @@ public: } DECLARE_EXPORT_INFO; + bool m_reportOnUncaughtException = false; static void destroy(JSC::JSCell* cell) { diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index bb7c91195c..1e5d8e720c 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -214,7 +214,7 @@ WTF::String JSValueToStringSafe(JSC::JSGlobalObject* globalObject, JSValue arg) return makeString("[Function: "_s, name, ']'); } - return "[Function: (anonymous)]"_s; + return "[Function (anonymous)]"_s; break; } @@ -279,7 +279,7 @@ WTF::String determineSpecificType(JSC::JSGlobalObject* globalObject, JSValue val if (!name.isNull() && name.length() > 0) { return makeString("function "_s, name); } - return String("function"_s); + return String("function "_s); } if (cell->isString()) { auto str = value.toString(globalObject)->getString(globalObject); @@ -405,7 +405,7 @@ namespace ERR { JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value) { - auto arg_kind = arg_name.startsWith("options."_s) ? "property"_s : "argument"_s; + auto arg_kind = arg_name.contains('.') ? "property"_s : "argument"_s; auto ty_first_char = expected_type[0]; auto ty_kind = ty_first_char >= 'A' && ty_first_char <= 'Z' ? "an instance of"_s : "of type"_s; @@ -420,7 +420,7 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO { auto arg_name = val_arg_name.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - auto arg_kind = arg_name.startsWith("options."_s) ? "property"_s : "argument"_s; + auto arg_kind = arg_name.contains('.') ? "property"_s : "argument"_s; auto ty_first_char = expected_type[0]; auto ty_kind = ty_first_char >= 'A' && ty_first_char <= 'Z' ? "an instance of"_s : "of type"_s; @@ -500,7 +500,7 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason) { - ASCIILiteral type = String(name).find('.') != notFound ? "property"_s : "argument"_s; + ASCIILiteral type = String(name).contains('.') ? "property"_s : "argument"_s; auto value_string = JSValueToStringSafe(globalObject, value); RETURN_IF_EXCEPTION(throwScope, {}); @@ -509,6 +509,20 @@ JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobal throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, message)); return {}; } +JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason) +{ + ASCIILiteral type = String(name).contains('.') ? "property"_s : "argument"_s; + + auto value_string = JSValueToStringSafe(globalObject, value); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto& vm = globalObject->vm(); + auto message = makeString("The "_s, type, " '"_s, name, "' "_s, reason, ". Received "_s, value_string); + auto* structure = createErrorStructure(vm, globalObject, ErrorType::RangeError, "RangeError"_s, "ERR_INVALID_ARG_VALUE"_s); + auto error = JSC::ErrorInstance::create(vm, structure, message, jsUndefined(), nullptr, JSC::RuntimeType::TypeNothing, ErrorType::RangeError, true); + throwScope.throwException(globalObject, error); + return {}; +} JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason) { auto name_string = JSValueToStringSafe(globalObject, name); @@ -551,7 +565,7 @@ JSC::EncodedJSValue BUFFER_OUT_OF_BOUNDS(JSC::ThrowScope& throwScope, JSC::JSGlo JSC::EncodedJSValue UNKNOWN_SIGNAL(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue signal, bool triedUppercase) { - auto signal_string = JSValueToStringSafe(globalObject, signal); + auto signal_string = signal.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); auto message_extra = triedUppercase ? " (signals must use all capital letters)"_s : ""_s; @@ -574,6 +588,28 @@ JSC::EncodedJSValue SOCKET_BAD_PORT(JSC::ThrowScope& throwScope, JSC::JSGlobalOb return {}; } +JSC::EncodedJSValue UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject) +{ + auto message = "`process.setupUncaughtExceptionCapture()` was called while a capture callback was already active"_s; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET, message)); + return {}; +} + +JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue msg) +{ + auto msg_string = msg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + auto message = msg_string; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_ASSERTION, message)); + return {}; +} +JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral msg) +{ + auto message = msg; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_ASSERTION, message)); + return {}; +} + } static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue arg0, JSValue arg1, JSValue arg2) @@ -596,6 +632,25 @@ static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalOb return createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, msg); } +static JSValue ERR_INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, JSC::JSValue reason) +{ + ASSERT(name.isString()); + auto name_string = name.toWTFString(globalObject); + ASCIILiteral type = name_string.contains('.') ? "property"_s : "argument"_s; + + auto value_string = JSValueToStringSafe(globalObject, value); + RETURN_IF_EXCEPTION(throwScope, {}); + + ASSERT(reason.isUndefined() || reason.isString()); + if (reason.isUndefined()) { + auto message = makeString("The "_s, type, " '"_s, name_string, "' is invalid. Received "_s, value_string); + return createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, message); + } + auto reason_string = reason.toWTFString(globalObject); + auto message = makeString("The "_s, type, " '"_s, name_string, "' "_s, reason_string, ". Received "_s, value_string); + return createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, message); +} + JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_OUT_OF_RANGE, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); @@ -608,31 +663,11 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_OUT_OF_RANGE, (JSC::JSGlobalObject * glo return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_OUT_OF_RANGE, message)); } -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_IPC_DISCONNECTED, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s)); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_SERVER_NOT_RUNNING, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SERVER_NOT_RUNNING, "Server is not running."_s)); -} - extern "C" JSC::EncodedJSValue Bun__createErrorWithCode(JSC::JSGlobalObject* globalObject, ErrorCode code, BunString* message) { return JSValue::encode(createError(globalObject, code, message->toWTFString(BunString::ZeroCopy))); } -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_IPC_CHANNEL_CLOSED, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_CHANNEL_CLOSED, "Channel closed."_s)); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_SOCKET_BAD_TYPE, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_BAD_TYPE, "Bad socket type specified. Valid types are: udp4, udp6"_s)); -} - JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_INVALID_PROTOCOL, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); @@ -650,19 +685,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_INVALID_PROTOCOL, (JSC::JSGlobalObject * return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_PROTOCOL, message)); } -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_INVALID_ARG_TYPE, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - JSC::VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - EXPECT_ARG_COUNT(3); - - auto arg_name = callFrame->argument(0); - auto expected_type = callFrame->argument(1); - auto actual_value = callFrame->argument(2); - return JSValue::encode(ERR_INVALID_ARG_TYPE(scope, globalObject, arg_name, expected_type, actual_value)); -} - JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_BROTLI_INVALID_PARAM, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); @@ -691,16 +713,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_BUFFER_TOO_LARGE, (JSC::JSGlobalObject * return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_BUFFER_TOO_LARGE, message)); } -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_ZLIB_INITIALIZATION_FAILED, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_ZLIB_INITIALIZATION_FAILED, "Initialization failed"_s)); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_BUFFER_OUT_OF_BOUNDS, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) -{ - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_BUFFER_OUT_OF_BOUNDS, "Attempt to access memory outside buffer bounds"_s)); -} - JSC_DEFINE_HOST_FUNCTION(jsFunction_ERR_UNHANDLED_ERROR, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); @@ -780,7 +792,7 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - EXPECT_ARG_COUNT(2); + EXPECT_ARG_COUNT(1); JSC::JSValue codeValue = callFrame->argument(0); RETURN_IF_EXCEPTION(scope, {}); @@ -808,9 +820,43 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject JSValue arg0 = callFrame->argument(1); JSValue arg1 = callFrame->argument(2); JSValue arg2 = callFrame->argument(3); - return JSValue::encode(ERR_INVALID_ARG_TYPE(scope, globalObject, arg0, arg1, arg2)); } + + case Bun::ErrorCode::ERR_INVALID_ARG_VALUE: { + JSValue arg0 = callFrame->argument(1); + JSValue arg1 = callFrame->argument(2); + JSValue arg2 = callFrame->argument(3); + return JSValue::encode(ERR_INVALID_ARG_VALUE(scope, globalObject, arg0, arg1, arg2)); + } + + case ErrorCode::ERR_IPC_DISCONNECTED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s)); + case ErrorCode::ERR_SERVER_NOT_RUNNING: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SERVER_NOT_RUNNING, "Server is not running."_s)); + case ErrorCode::ERR_IPC_CHANNEL_CLOSED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_CHANNEL_CLOSED, "Channel closed."_s)); + case ErrorCode::ERR_SOCKET_BAD_TYPE: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_BAD_TYPE, "Bad socket type specified. Valid types are: udp4, udp6"_s)); + case ErrorCode::ERR_ZLIB_INITIALIZATION_FAILED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_ZLIB_INITIALIZATION_FAILED, "Initialization failed"_s)); + case ErrorCode::ERR_BUFFER_OUT_OF_BOUNDS: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_BUFFER_OUT_OF_BOUNDS, "Attempt to access memory outside buffer bounds"_s)); + case ErrorCode::ERR_IPC_ONE_PIPE: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_ONE_PIPE, "Child process can have only one IPC pipe"_s)); + case ErrorCode::ERR_SOCKET_ALREADY_BOUND: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_ALREADY_BOUND, "Socket is already bound"_s)); + case ErrorCode::ERR_SOCKET_BAD_BUFFER_SIZE: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_BAD_BUFFER_SIZE, "Buffer size must be a positive integer"_s)); + case ErrorCode::ERR_SOCKET_DGRAM_IS_CONNECTED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_IS_CONNECTED, "Already connected"_s)); + case ErrorCode::ERR_SOCKET_DGRAM_NOT_CONNECTED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_CONNECTED, "Not connected"_s)); + case ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING, "Not running"_s)); + case ErrorCode::ERR_INVALID_CURSOR_POS: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_CURSOR_POS, "Cannot set cursor row without setting its column"_s)); + default: { break; } diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 9c288f9b06..d06bb8e4a2 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -55,18 +55,11 @@ JSC::JSValue toJS(JSC::JSGlobalObject*, ErrorCode); JSObject* createInvalidThisError(JSGlobalObject* globalObject, JSValue thisValue, const ASCIILiteral typeName); JSObject* createInvalidThisError(JSGlobalObject* globalObject, const String& message); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_INVALID_ARG_TYPE); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_OUT_OF_RANGE); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_IPC_DISCONNECTED); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_SERVER_NOT_RUNNING); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_IPC_CHANNEL_CLOSED); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_SOCKET_BAD_TYPE); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_INVALID_PROTOCOL); JSC_DECLARE_HOST_FUNCTION(jsFunctionMakeErrorWithCode); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_BROTLI_INVALID_PARAM); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_BUFFER_TOO_LARGE); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_ZLIB_INITIALIZATION_FAILED); -JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_BUFFER_OUT_OF_BOUNDS); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_UNHANDLED_ERROR); enum Bound { @@ -84,6 +77,7 @@ JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObjec JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); +JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); @@ -91,6 +85,9 @@ JSC::EncodedJSValue STRING_TOO_LONG(JSC::ThrowScope& throwScope, JSC::JSGlobalOb JSC::EncodedJSValue BUFFER_OUT_OF_BOUNDS(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue UNKNOWN_SIGNAL(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue signal, bool triedUppercase = false); JSC::EncodedJSValue SOCKET_BAD_PORT(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue port, bool allowZero); +JSC::EncodedJSValue UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); +JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue msg); +JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral msg); } diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index bc9b2bfe28..3c7b465a24 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -52,6 +52,17 @@ export default [ ["ERR_SCRIPT_EXECUTION_TIMEOUT", Error, "Error"], ["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error, "Error"], ["ERR_UNHANDLED_ERROR", Error], + ["ERR_UNKNOWN_CREDENTIAL", Error], + ["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error], + ["ERR_DLOPEN_FAILED", Error], + ["ERR_ASSERTION", Error], + ["ERR_IPC_ONE_PIPE", Error], + ["ERR_SOCKET_ALREADY_BOUND", Error], + ["ERR_SOCKET_BAD_BUFFER_SIZE", TypeError], + ["ERR_SOCKET_DGRAM_IS_CONNECTED", Error], + ["ERR_SOCKET_DGRAM_NOT_CONNECTED", Error], + ["ERR_SOCKET_DGRAM_NOT_RUNNING", Error], + ["ERR_INVALID_CURSOR_POS", TypeError], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError], diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 561ae2d213..0d922f5577 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -205,7 +205,6 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSync(JSC::JSGlobalObje JSC::JSValue isESMValue = callFrame->argument(2); if (isESMValue.isBoolean()) { isESM = isESMValue.toBoolean(globalObject); - RETURN_IF_EXCEPTION(scope, {}); } } @@ -223,7 +222,6 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSync(JSC::JSGlobalObje } else if (fromValue.isBoolean()) { isESM = fromValue.toBoolean(globalObject); - RETURN_IF_EXCEPTION(scope, {}); fromValue = JSC::jsUndefined(); } diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 0476e797bb..f3cdd036b8 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -425,7 +425,7 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_allocUnsafeBody(JS VM& vm = lexicalGlobalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); JSValue lengthValue = callFrame->argument(0); - Bun::V::validateNumber(throwScope, lexicalGlobalObject, lengthValue, jsString(vm, String("size"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateNumber(throwScope, lexicalGlobalObject, lengthValue, "size"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); size_t length = lengthValue.toLength(lexicalGlobalObject); auto result = allocBufferUnsafe(lexicalGlobalObject, length); @@ -550,7 +550,7 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_allocBody(JSC::JSG auto scope = DECLARE_THROW_SCOPE(vm); JSValue lengthValue = callFrame->argument(0); - Bun::V::validateNumber(scope, lexicalGlobalObject, lengthValue, jsString(vm, String("size"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateNumber(scope, lexicalGlobalObject, lengthValue, "size"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(scope, {}); size_t length = lengthValue.toLength(lexicalGlobalObject); @@ -886,7 +886,7 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_copyBytesFromBody( if (!offsetValue.isUndefined() || !lengthValue.isUndefined()) { if (!offsetValue.isUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, offsetValue, jsString(vm, String("offset"_s)), jsNumber(0), jsUndefined()); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, offsetValue, "offset"_s, jsNumber(0), jsUndefined()); RETURN_IF_EXCEPTION(throwScope, {}); offset = offsetValue.asNumber(); if (offset >= viewLength) return JSValue::encode(createEmptyBuffer(lexicalGlobalObject)); @@ -896,7 +896,7 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_copyBytesFromBody( double end = 0; if (!lengthValue.isUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, lengthValue, jsString(vm, String("length"_s)), jsNumber(0), jsUndefined()); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, lengthValue, "length"_s, jsNumber(0), jsUndefined()); RETURN_IF_EXCEPTION(throwScope, {}); length = lengthValue.asNumber(); end = offset + length; @@ -998,7 +998,7 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSG default: sourceEndValue = callFrame->uncheckedArgument(4); if (sourceEndValue != jsUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceEndValue, jsString(vm, String("sourceEnd"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceEndValue, "sourceEnd"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); sourceEnd = sourceEndValue.asNumber(); } @@ -1007,7 +1007,7 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSG case 4: sourceStartValue = callFrame->uncheckedArgument(3); if (sourceStartValue != jsUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceStartValue, jsString(vm, String("sourceStart"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceStartValue, "sourceStart"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); sourceStart = sourceStartValue.asNumber(); } @@ -1016,7 +1016,7 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSG case 3: targetEndValue = callFrame->uncheckedArgument(2); if (targetEndValue != jsUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetEndValue, jsString(vm, String("targetEnd"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetEndValue, "targetEnd"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); targetEnd = targetEndValue.asNumber(); } @@ -1025,7 +1025,7 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSG case 2: targetStartValue = callFrame->uncheckedArgument(1); if (targetStartValue != jsUndefined()) { - Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetStartValue, jsString(vm, String("targetStart"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetStartValue, "targetStart"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); targetStart = targetStartValue.asNumber(); } @@ -1225,12 +1225,12 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_fillBody(JSC::JSGlob // https://github.com/nodejs/node/blob/v22.9.0/lib/buffer.js#L1066-L1079 // https://github.com/nodejs/node/blob/v22.9.0/lib/buffer.js#L122 if (!offsetValue.isUndefined()) { - Bun::V::validateNumber(scope, lexicalGlobalObject, offsetValue, jsString(vm, String("offset"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateNumber(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(scope, {}); offset = offsetValue.toLength(lexicalGlobalObject); } if (!endValue.isUndefined()) { - Bun::V::validateNumber(scope, lexicalGlobalObject, endValue, jsString(vm, String("end"_s)), jsNumber(0), jsNumber(limit)); + Bun::V::validateNumber(scope, lexicalGlobalObject, endValue, "end"_s, jsNumber(0), jsNumber(limit)); RETURN_IF_EXCEPTION(scope, {}); end = endValue.toLength(lexicalGlobalObject); } @@ -2373,7 +2373,7 @@ static inline JSC::EncodedJSValue createJSBufferFromJS(JSC::JSGlobalObject* lexi return JSBuffer__bufferFromLength(lexicalGlobalObject, distinguishingArg.asAnyInt()); } else if (distinguishingArg.isNumber()) { JSValue lengthValue = distinguishingArg; - Bun::V::validateNumber(throwScope, lexicalGlobalObject, lengthValue, jsString(vm, String("size"_s)), jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); + Bun::V::validateNumber(throwScope, lexicalGlobalObject, lengthValue, "size"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength)); RETURN_IF_EXCEPTION(throwScope, {}); size_t length = lengthValue.toLength(lexicalGlobalObject); return JSBuffer__bufferFromLength(lexicalGlobalObject, length); diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 5140505d04..0facb458fd 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -1122,7 +1122,6 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockName, (JSC::JSGlobalObject * globalOb // https://github.com/jestjs/jest/blob/bd1c6db7c15c23788ca3e09c919138e48dd3b28a/packages/jest-mock/src/index.ts#L849-L856 if (callframe->argument(0).toBoolean(globalObject)) { - RETURN_IF_EXCEPTION(scope, {}); WTF::String name = callframe->argument(0).toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); thisObject->setName(name); diff --git a/src/bun.js/bindings/NodeValidator.cpp b/src/bun.js/bindings/NodeValidator.cpp index c259609ef6..dfd177bc3c 100644 --- a/src/bun.js/bindings/NodeValidator.cpp +++ b/src/bun.js/bindings/NodeValidator.cpp @@ -51,6 +51,24 @@ JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObje return JSValue::encode(jsUndefined()); } +JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max) +{ + if (!value.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "number"_s, value); + if (min.isUndefined()) min = jsDoubleNumber(JSC::minSafeInteger()); + if (max.isUndefined()) max = jsDoubleNumber(JSC::maxSafeInteger()); + + auto value_num = value.asNumber(); + auto min_num = min.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto max_num = max.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + max_num = std::max(min_num, max_num); + + if (std::fmod(value_num, 1.0) != 0) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, "an integer"_s, value); + if (value_num < min_num || value_num > max_num) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, min_num, max_num, value); + + return JSValue::encode(jsUndefined()); +} JSC_DEFINE_HOST_FUNCTION(jsFunction_validateNumber, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -85,6 +103,28 @@ JSC::EncodedJSValue V::validateNumber(JSC::ThrowScope& scope, JSC::JSGlobalObjec return JSValue::encode(jsUndefined()); } +JSC::EncodedJSValue V::validateNumber(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue min, JSValue max) +{ + if (!value.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "number"_s, value); + + auto value_num = value.asNumber(); + auto min_num = min.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto max_num = max.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + auto min_isnonnull = !min.isUndefinedOrNull(); + auto max_isnonnull = !max.isUndefinedOrNull(); + + if ((min_isnonnull && value_num < min_num) || (max_isnonnull && value_num > max_num) || ((min_isnonnull || max_isnonnull) && std::isnan(value_num))) { + if (min_isnonnull && max_isnonnull) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, min_num, max_num, value); + if (min_isnonnull) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, min_num, Bun::LOWER, value); + if (max_isnonnull) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, max_num, Bun::UPPER, value); + return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, ""_s, value); + } + + return JSValue::encode(jsUndefined()); +} JSC_DEFINE_HOST_FUNCTION(jsFunction_validateString, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -211,8 +251,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validatePort, (JSC::JSGlobalObject * globalO if (allowZero.isUndefined()) allowZero = jsBoolean(true); auto allowZero_b = allowZero.toBoolean(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - if (!port.isNumber() && !port.isString()) return Bun::ERR::SOCKET_BAD_PORT(scope, globalObject, name, port, allowZero_b); if (port.isString()) { @@ -297,6 +335,30 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateArray, (JSC::JSGlobalObject * global auto value = callFrame->argument(0); auto name = callFrame->argument(1); auto minLength = callFrame->argument(2); + return V::validateArray(scope, globalObject, value, name, minLength); +} +JSC::EncodedJSValue V::validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue minLength) +{ + JSC::VM& vm = globalObject->vm(); + + if (minLength.isUndefined()) minLength = jsNumber(0); + + if (!JSC::isArray(globalObject, value)) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Array"_s, value); + + auto length = value.get(globalObject, Identifier::fromString(vm, "length"_s)); + RETURN_IF_EXCEPTION(scope, {}); + auto length_num = length.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto minLength_num = minLength.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (length_num < minLength_num) { + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, name, value, makeString("must be longer than "_s, minLength_num)); + } + return JSValue::encode(jsUndefined()); +} +JSC::EncodedJSValue V::validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue minLength) +{ + JSC::VM& vm = globalObject->vm(); if (minLength.isUndefined()) minLength = jsNumber(0); @@ -348,7 +410,25 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateUint32, (JSC::JSGlobalObject * globa auto value = callFrame->argument(0); auto name = callFrame->argument(1); auto positive = callFrame->argument(2); - + return V::validateUint32(scope, globalObject, value, name, positive); +} +JSC::EncodedJSValue V::validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue positive) +{ + if (!value.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "number"_s, value); + if (positive.isUndefined()) positive = jsBoolean(false); + + auto value_num = value.asNumber(); + if (std::fmod(value_num, 1.0) != 0) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, "an integer"_s, value); + + auto positive_b = positive.toBoolean(globalObject); + auto min = positive_b ? 1 : 0; + auto max = std::numeric_limits().max(); + if (value_num < min || value_num > max) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, min, max, value); + + return JSValue::encode(jsUndefined()); +} +JSC::EncodedJSValue V::validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue positive) +{ if (!value.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "number"_s, value); if (positive.isUndefined()) positive = jsBoolean(false); @@ -356,7 +436,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateUint32, (JSC::JSGlobalObject * globa if (std::fmod(value_num, 1.0) != 0) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, "an integer"_s, value); auto positive_b = positive.toBoolean(globalObject); - RETURN_IF_EXCEPTION(scope, {}); auto min = positive_b ? 1 : 0; auto max = std::numeric_limits().max(); if (value_num < min || value_num > max) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, name, min, max, value); @@ -463,4 +542,5 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateBuffer, (JSC::JSGlobalObject * globa } return JSValue::encode(jsUndefined()); } + } diff --git a/src/bun.js/bindings/NodeValidator.h b/src/bun.js/bindings/NodeValidator.h index b691c7f5e0..2557c8a422 100644 --- a/src/bun.js/bindings/NodeValidator.h +++ b/src/bun.js/bindings/NodeValidator.h @@ -27,10 +27,16 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateBuffer, (JSC::JSGlobalObject * globa namespace V { JSC::EncodedJSValue validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, JSC::JSValue name, JSC::JSValue min, JSC::JSValue max); +JSC::EncodedJSValue validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max); JSC::EncodedJSValue validateNumber(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, JSC::JSValue name, JSC::JSValue min, JSC::JSValue max); +JSC::EncodedJSValue validateNumber(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue min, JSValue max); JSC::EncodedJSValue validateFiniteNumber(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue number, JSC::JSValue name); JSC::EncodedJSValue validateString(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name); JSC::EncodedJSValue validateString(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name); +JSC::EncodedJSValue validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue minLength); +JSC::EncodedJSValue validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue minLength); +JSC::EncodedJSValue validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue positive); +JSC::EncodedJSValue validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue positive); } diff --git a/src/bun.js/bindings/OsBinding.cpp b/src/bun.js/bindings/OsBinding.cpp index c994fe8d98..dfe6713d6e 100644 --- a/src/bun.js/bindings/OsBinding.cpp +++ b/src/bun.js/bindings/OsBinding.cpp @@ -14,12 +14,31 @@ extern "C" uint64_t Bun__Os__getFreeMemory(void) vm_statistics_data_t info; mach_msg_type_number_t count = sizeof(info) / sizeof(integer_t); - if (host_statistics(mach_host_self(), HOST_VM_INFO, - (host_info_t)&info, &count) - != KERN_SUCCESS) { + if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&info, &count) != KERN_SUCCESS) { return 0; } - return (uint64_t)info.free_count * sysconf(_SC_PAGESIZE); } #endif + +#if OS(LINUX) +#include + +extern "C" uint64_t Bun__Os__getFreeMemory(void) +{ + struct sysinfo info; + if (sysinfo(&info) == 0) { + return info.freeram * info.mem_unit; + } + return 0; +} +#endif + +#if OS(WINDOWS) +extern "C" uint64_t uv_get_available_memory(void); + +extern "C" uint64_t Bun__Os__getFreeMemory(void) +{ + return uv_get_available_memory(); +} +#endif diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 358e84d9da..16402e965b 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2795,7 +2795,6 @@ JSC_DEFINE_CUSTOM_SETTER(moduleNamespacePrototypeSetESModuleMarker, (JSGlobalObj auto scope = DECLARE_THROW_SCOPE(vm); JSValue value = JSValue::decode(encodedValue); WTF::TriState triState = value.toBoolean(globalObject) ? WTF::TriState::True : WTF::TriState::False; - RETURN_IF_EXCEPTION(scope, false); moduleNamespaceObject->m_hasESModuleMarker = triState; return true; } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4bb4689510..d8b9c3bbc5 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1949,24 +1949,21 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC::JSObject* result = JSC::ErrorInstance::create(globalObject, globalObject->errorStructureWithErrorType(), message, options); + auto clientData = WebCore::clientData(vm); + if (err.code.tag != BunStringTag::Empty) { JSC::JSValue code = Bun::toJS(globalObject, err.code); - result->putDirect(vm, names.codePublicName(), code, - JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | 0); - - result->putDirect(vm, vm.propertyNames->name, code, JSC::PropertyAttribute::DontEnum | 0); - } else { - auto* domGlobalObject = defaultGlobalObject(globalObject); - result->putDirect( - vm, vm.propertyNames->name, - JSC::JSValue(domGlobalObject->commonStrings().SystemErrorString(domGlobalObject)), - JSC::PropertyAttribute::DontEnum | 0); + result->putDirect(vm, clientData->builtinNames().codePublicName(), code, JSC::PropertyAttribute::DontDelete | 0); } if (err.path.tag != BunStringTag::Empty) { JSC::JSValue path = Bun::toJS(globalObject, err.path); - result->putDirect(vm, names.pathPublicName(), path, - JSC::PropertyAttribute::DontDelete | 0); + result->putDirect(vm, clientData->builtinNames().pathPublicName(), path, JSC::PropertyAttribute::DontDelete | 0); + } + + if (err.dest.tag != BunStringTag::Empty) { + JSC::JSValue dest = Bun::toJS(globalObject, err.dest); + result->putDirect(vm, clientData->builtinNames().destPublicName(), dest, JSC::PropertyAttribute::DontDelete | 0); } if (err.fd != -1) { diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index eecc319413..75911433c4 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1732,6 +1732,7 @@ pub const SystemError = extern struct { path: String = String.empty, syscall: String = String.empty, fd: bun.FileDescriptor = bun.toFD(-1), + dest: String = String.empty, pub fn Maybe(comptime Result: type) type { return union(enum) { @@ -1755,6 +1756,7 @@ pub const SystemError = extern struct { this.code.deref(); this.message.deref(); this.syscall.deref(); + this.dest.deref(); } pub fn ref(this: *SystemError) void { @@ -1762,6 +1764,7 @@ pub const SystemError = extern struct { this.code.ref(); this.message.ref(); this.syscall.ref(); + this.dest.ref(); } pub fn toErrorInstance(this: *const SystemError, global: *JSGlobalObject) JSValue { @@ -1770,6 +1773,7 @@ pub const SystemError = extern struct { this.code.deref(); this.message.deref(); this.syscall.deref(); + this.dest.deref(); } return shim.cppFn("toErrorInstance", .{ this, global }); @@ -1800,6 +1804,7 @@ pub const SystemError = extern struct { this.code.deref(); this.message.deref(); this.syscall.deref(); + this.dest.deref(); } return SystemError__toErrorInstanceWithInfoObject(this, global); diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index a581958296..f652c77575 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -362,15 +362,22 @@ pub const Process = extern struct { pub const shim = Shimmer("Bun", "Process", @This()); pub const name = "Process"; pub const namespace = shim.namespace; - const _bun: string = "bun"; + var title_mutex = std.Thread.Mutex{}; pub fn getTitle(_: *JSGlobalObject, title: *ZigString) callconv(.C) void { - title.* = ZigString.init(_bun); + title_mutex.lock(); + defer title_mutex.unlock(); + const str = bun.CLI.Bun__Node__ProcessTitle; + title.* = ZigString.init(str orelse "bun"); } // TODO: https://github.com/nodejs/node/blob/master/deps/uv/src/unix/darwin-proctitle.c - pub fn setTitle(globalObject: *JSGlobalObject, _: *ZigString) callconv(.C) JSValue { - return ZigString.init(_bun).toJS(globalObject); + pub fn setTitle(globalObject: *JSGlobalObject, newvalue: *ZigString) callconv(.C) JSValue { + title_mutex.lock(); + defer title_mutex.unlock(); + if (bun.CLI.Bun__Node__ProcessTitle) |_| bun.default_allocator.free(bun.CLI.Bun__Node__ProcessTitle.?); + bun.CLI.Bun__Node__ProcessTitle = newvalue.dupe(bun.default_allocator) catch bun.outOfMemory(); + return newvalue.toJS(globalObject); } pub const getArgv = JSC.Node.Process.getArgv; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 8c7b6802a5..97080b4802 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -125,6 +125,7 @@ typedef struct SystemError { BunString path; BunString syscall; int fd; + BunString dest; } SystemError; typedef void* ArrayBufferSink; diff --git a/src/bun.js/bindings/helpers.cpp b/src/bun.js/bindings/helpers.cpp index 6aa6a6096a..834bd0a36b 100644 --- a/src/bun.js/bindings/helpers.cpp +++ b/src/bun.js/bindings/helpers.cpp @@ -1,6 +1,7 @@ #include "root.h" #include "helpers.h" #include "BunClientData.h" +#include JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral message, ASCIILiteral syscall, int err) { @@ -15,11 +16,13 @@ JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral message JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral syscall, int err) { - auto* instance = JSC::createError(global, makeString(String(syscall), "() failed"_s)); + auto errstr = String::fromLatin1(Bun__errnoName(err)); + auto* instance = JSC::createError(global, makeString(syscall, "() failed: "_s, errstr, ": "_s, String::fromLatin1(strerror(err)))); auto& vm = global->vm(); auto& builtinNames = WebCore::builtinNames(vm); instance->putDirect(vm, builtinNames.syscallPublicName(), jsString(vm, String(syscall)), 0); instance->putDirect(vm, builtinNames.errnoPublicName(), JSC::jsNumber(err), 0); instance->putDirect(vm, vm.propertyNames->name, jsString(vm, String("SystemError"_s)), JSC::PropertyAttribute::DontEnum | 0); + instance->putDirect(vm, builtinNames.codePublicName(), jsString(vm, errstr)); return instance; } diff --git a/src/bun.js/bindings/helpers.h b/src/bun.js/bindings/helpers.h index cabb787ce1..2653138c79 100644 --- a/src/bun.js/bindings/helpers.h +++ b/src/bun.js/bindings/helpers.h @@ -26,6 +26,7 @@ class GlobalObject; #pragma clang diagnostic ignored "-Wunused-function" extern "C" size_t Bun__stringSyntheticAllocationLimit; +extern "C" const char* Bun__errnoName(int); namespace Zig { diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index e27d4159fe..fd4d1ce63c 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -1670,7 +1670,6 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj } bool shouldThrowOnError = (throwOnError.isEmpty() || throwOnError.isUndefined()) ? false : throwOnError.toBoolean(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); sqlite3* db = databases()[dbIndex]->db; // no-op if already closed @@ -2368,7 +2367,6 @@ JSC_DEFINE_CUSTOM_SETTER(jsSqlStatementSetSafeIntegers, (JSGlobalObject * lexica CHECK_PREPARED bool value = JSValue::decode(encodedValue).toBoolean(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, false); castedThis->useBigInt64 = value; return true; diff --git a/src/bun.js/bindings/webcore/EventEmitter.cpp b/src/bun.js/bindings/webcore/EventEmitter.cpp index 021edf1fca..8db15a8e5b 100644 --- a/src/bun.js/bindings/webcore/EventEmitter.cpp +++ b/src/bun.js/bindings/webcore/EventEmitter.cpp @@ -119,9 +119,9 @@ bool EventEmitter::emitForBindings(const Identifier& eventType, const MarkedArgu return true; } -void EventEmitter::emit(const Identifier& eventType, const MarkedArgumentBuffer& arguments) +bool EventEmitter::emit(const Identifier& eventType, const MarkedArgumentBuffer& arguments) { - fireEventListeners(eventType, arguments); + return fireEventListeners(eventType, arguments); } void EventEmitter::uncaughtExceptionInEventHandler() @@ -175,12 +175,12 @@ Vector EventEmitter::getListeners(const Identifier& eventType) } // https://dom.spec.whatwg.org/#concept-event-listener-invoke -void EventEmitter::fireEventListeners(const Identifier& eventType, const MarkedArgumentBuffer& arguments) +bool EventEmitter::fireEventListeners(const Identifier& eventType, const MarkedArgumentBuffer& arguments) { auto* data = eventTargetData(); if (!data) - return; + return false; auto* listenersVector = data->eventListenerMap.find(eventType); if (UNLIKELY(!listenersVector)) { @@ -188,24 +188,25 @@ void EventEmitter::fireEventListeners(const Identifier& eventType, const MarkedA Ref protectedThis(*this); auto* thisObject = protectedThis->m_thisObject.get(); if (!thisObject) - return; + return false; Bun__reportUnhandledError(thisObject->globalObject(), JSValue::encode(arguments.at(0))); - return; + return false; } - return; + return false; } bool prevFiringEventListeners = data->isFiringEventListeners; data->isFiringEventListeners = true; - innerInvokeEventListeners(eventType, *listenersVector, arguments); + auto fired = innerInvokeEventListeners(eventType, *listenersVector, arguments); data->isFiringEventListeners = prevFiringEventListeners; + return fired; } // Intentionally creates a copy of the listeners vector to avoid event listeners added after this point from being run. // Note that removal still has an effect due to the removed field in RegisteredEventListener. // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke -void EventEmitter::innerInvokeEventListeners(const Identifier& eventType, SimpleEventListenerVector listeners, const MarkedArgumentBuffer& arguments) +bool EventEmitter::innerInvokeEventListeners(const Identifier& eventType, SimpleEventListenerVector listeners, const MarkedArgumentBuffer& arguments) { Ref protectedThis(*this); ASSERT(!listeners.isEmpty()); @@ -216,6 +217,7 @@ void EventEmitter::innerInvokeEventListeners(const Identifier& eventType, Simple auto* thisObject = protectedThis->m_thisObject.get(); JSC::JSValue thisValue = thisObject ? JSC::JSValue(thisObject) : JSC::jsUndefined(); + auto fired = false; for (auto& registeredListener : listeners) { // The below code used to be in here, but it's WRONG. Even if a listener is removed, @@ -244,6 +246,7 @@ void EventEmitter::innerInvokeEventListeners(const Identifier& eventType, Simple if (UNLIKELY(callData.type == JSC::CallData::Type::None)) continue; + fired = true; WTF::NakedPtr exceptionPtr; call(lexicalGlobalObject, jsFunction, callData, thisValue, arguments, exceptionPtr); auto* exception = exceptionPtr.get(); @@ -265,6 +268,8 @@ void EventEmitter::innerInvokeEventListeners(const Identifier& eventType, Simple } } } + + return fired; } Vector EventEmitter::eventTypes() diff --git a/src/bun.js/bindings/webcore/EventEmitter.h b/src/bun.js/bindings/webcore/EventEmitter.h index 23687d43a1..e9f6aa167d 100644 --- a/src/bun.js/bindings/webcore/EventEmitter.h +++ b/src/bun.js/bindings/webcore/EventEmitter.h @@ -55,7 +55,7 @@ public: WEBCORE_EXPORT bool removeListener(const Identifier& eventType, EventListener&); WEBCORE_EXPORT bool removeAllListeners(const Identifier& eventType); - WEBCORE_EXPORT void emit(const Identifier&, const MarkedArgumentBuffer&); + WEBCORE_EXPORT bool emit(const Identifier&, const MarkedArgumentBuffer&); WEBCORE_EXPORT void uncaughtExceptionInEventHandler(); WEBCORE_EXPORT Vector getEventNames(); @@ -76,7 +76,7 @@ public: Vector eventTypes(); const SimpleEventListenerVector& eventListeners(const Identifier& eventType); - void fireEventListeners(const Identifier& eventName, const MarkedArgumentBuffer& arguments); + bool fireEventListeners(const Identifier& eventName, const MarkedArgumentBuffer& arguments); bool isFiringEventListeners() const; void invalidateJSEventListeners(JSC::JSObject*); @@ -109,7 +109,7 @@ private: { } - void innerInvokeEventListeners(const Identifier&, SimpleEventListenerVector, const MarkedArgumentBuffer& arguments); + bool innerInvokeEventListeners(const Identifier&, SimpleEventListenerVector, const MarkedArgumentBuffer& arguments); void invalidateEventListenerRegions(); EventEmitterData m_eventTargetData; diff --git a/src/bun.js/bindings/webcore/JSDOMConstructorCallable.h b/src/bun.js/bindings/webcore/JSDOMConstructorCallable.h new file mode 100644 index 0000000000..8a2650aa3c --- /dev/null +++ b/src/bun.js/bindings/webcore/JSDOMConstructorCallable.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015, 2016 Canon Inc. All rights reserved. + * Copyright (C) 2016-2021 Apple Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "JSDOMConstructorBase.h" + +namespace WebCore { + +template class JSDOMConstructorCallable final : public JSDOMConstructorBase { +public: + using Base = JSDOMConstructorBase; + + static JSDOMConstructorCallable* create(JSC::VM&, JSC::Structure*, JSDOMGlobalObject&); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject&, JSC::JSValue prototype); + + DECLARE_INFO; + + // Must be defined for each specialization class. + static JSC::JSValue prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); + + // Must be defined for each specialization class. + static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*); + static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*); + +private: + JSDOMConstructorCallable(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, construct, call) + { + } + + void finishCreation(JSC::VM&, JSDOMGlobalObject&); + + // Usually defined for each specialization class. + void initializeProperties(JSC::VM&, JSDOMGlobalObject&) {} +}; + +template inline JSDOMConstructorCallable* JSDOMConstructorCallable::create(JSC::VM& vm, JSC::Structure* structure, JSDOMGlobalObject& globalObject) +{ + JSDOMConstructorCallable* constructor = new (NotNull, JSC::allocateCell(vm)) JSDOMConstructorCallable(vm, structure); + constructor->finishCreation(vm, globalObject); + return constructor; +} + +template inline JSC::Structure* JSDOMConstructorCallable::createStructure(JSC::VM& vm, JSC::JSGlobalObject& globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, &globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); +} + +template inline void JSDOMConstructorCallable::finishCreation(JSC::VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + initializeProperties(vm, globalObject); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp index a112593d68..d091d8959c 100644 --- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp +++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp @@ -7,7 +7,7 @@ #include "IDLTypes.h" #include "JSAddEventListenerOptions.h" #include "JSDOMBinding.h" -#include "JSDOMConstructor.h" +#include "JSDOMConstructorCallable.h" #include "JSDOMConvertBase.h" #include "JSDOMConvertBoolean.h" #include "JSDOMConvertDictionary.h" @@ -23,6 +23,7 @@ #include "JSEvent.h" #include "JSEventListener.h" #include "JSEventListenerOptions.h" +#include "JavaScriptCore/JSCJSValue.h" #include "ScriptExecutionContext.h" #include "WebCoreJSClientData.h" #include @@ -94,7 +95,7 @@ public: }; STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSEventEmitterPrototype, JSEventEmitterPrototype::Base); -using JSEventEmitterDOMConstructor = JSDOMConstructor; +using JSEventEmitterDOMConstructor = JSDOMConstructorCallable; template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSEventEmitterDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) { @@ -124,6 +125,38 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSEventEmitterDOMConstru } JSC_ANNOTATE_HOST_FUNCTION(JSEventEmitterDOMConstructorConstruct, JSEventEmitterDOMConstructor::construct); +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSEventEmitterDOMConstructor::call(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + VM& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast(callFrame->jsCallee()); + ASSERT(castedThis); + auto* context = castedThis->scriptExecutionContext(); + if (UNLIKELY(!context)) { + return throwConstructorScriptExecutionContextUnavailableError(*lexicalGlobalObject, throwScope, "EventEmitter"_s); + } + const auto object = EventEmitter::create(*context); + if constexpr (IsExceptionOr) { + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue maxListeners = castedThis->getIfPropertyExists(lexicalGlobalObject, JSC::Identifier::fromString(vm, "defaultMaxListeners"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + if (maxListeners && maxListeners.isUInt32()) { + object->setMaxListeners(maxListeners.toUInt32(lexicalGlobalObject)); + } + static_assert(TypeOrExceptionOrUnderlyingType::isRef); + auto jsValue = toJSNewlyCreated>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, object.copyRef()); + if constexpr (IsExceptionOr) { + RETURN_IF_EXCEPTION(throwScope, {}); + } + Structure* structure = JSEventEmitter::createStructure(vm, lexicalGlobalObject, jsValue); + JSEventEmitter* instance + = JSEventEmitter::create(structure, reinterpret_cast(lexicalGlobalObject), object.copyRef()); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, JSValue::encode(instance)); +} +JSC_ANNOTATE_HOST_FUNCTION(JSEventEmitterDOMConstructorCall, JSEventEmitterDOMConstructor::call); + template<> const ClassInfo JSEventEmitterDOMConstructor::s_info = { "EventEmitter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSEventEmitterDOMConstructor) }; template<> JSValue JSEventEmitterDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) diff --git a/src/bun.js/bindings/webcore/JSWorker.cpp b/src/bun.js/bindings/webcore/JSWorker.cpp index 37f1674202..57e9dd062b 100644 --- a/src/bun.js/bindings/webcore/JSWorker.cpp +++ b/src/bun.js/bindings/webcore/JSWorker.cpp @@ -140,12 +140,10 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: if (auto miniModeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s))) { options.bun.mini = miniModeValue.toBoolean(lexicalGlobalObject); - RETURN_IF_EXCEPTION(throwScope, {}); } if (auto ref = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "ref"_s))) { options.bun.unref = !ref.toBoolean(lexicalGlobalObject); - RETURN_IF_EXCEPTION(throwScope, {}); } if (auto preloadModulesValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "preload"_s))) { diff --git a/src/bun.js/bindings/webcore/JSWorkerOptions.cpp b/src/bun.js/bindings/webcore/JSWorkerOptions.cpp index 8103d23053..cb46c6f204 100644 --- a/src/bun.js/bindings/webcore/JSWorkerOptions.cpp +++ b/src/bun.js/bindings/webcore/JSWorkerOptions.cpp @@ -69,7 +69,7 @@ template<> WorkerOptions convertDictionary(JSGlobalObject& lexica // if (isNullOrUndefined) // typeValue = jsUndefined(); // else { - // typeValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "type"_s)); + // typeValue = object->get(&lexicalGlobalObject, vm.propertyNames->type); // RETURN_IF_EXCEPTION(throwScope, { }); // } // if (!typeValue.isUndefined()) { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 404558d102..4029ab0fbb 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -885,6 +885,7 @@ pub const VirtualMachine = struct { onUnhandledRejectionExceptionList: ?*ExceptionList = null, unhandled_error_counter: usize = 0, is_handling_uncaught_exception: bool = false, + exit_on_uncaught_exception: bool = false, modules: ModuleLoader.AsyncModule.Queue = .{}, aggressive_garbage_collection: GCLevel = GCLevel.none, @@ -1190,6 +1191,10 @@ pub const VirtualMachine = struct { extern fn Bun__handleUnhandledRejection(*JSC.JSGlobalObject, reason: JSC.JSValue, promise: JSC.JSValue) c_int; extern fn Bun__Process__exit(*JSC.JSGlobalObject, code: c_int) noreturn; + export fn Bun__VirtualMachine__exitDuringUncaughtException(this: *JSC.VirtualMachine) void { + this.exit_on_uncaught_exception = true; + } + pub fn unhandledRejection(this: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, reason: JSC.JSValue, promise: JSC.JSValue) bool { if (this.isShuttingDown()) { Output.debugWarn("unhandledRejection during shutdown.", .{}); @@ -1227,6 +1232,11 @@ pub const VirtualMachine = struct { Bun__Process__exit(globalObject, 7); @panic("Uncaught exception while handling uncaught exception"); } + if (this.exit_on_uncaught_exception) { + this.runErrorHandler(err, null); + Bun__Process__exit(globalObject, 1); + @panic("made it past Bun__Process__exit"); + } this.is_handling_uncaught_exception = true; defer this.is_handling_uncaught_exception = false; const handled = Bun__handleUncaughtException(globalObject, err.toError() orelse err, if (is_rejection) 1 else 0) > 0; @@ -4093,11 +4103,7 @@ pub const VirtualMachine = struct { fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { if (!name.isEmpty() and !message.isEmpty()) { const display_name: String = if (name.eqlComptime("Error")) String.init("error") else name; - - try writer.print(comptime Output.prettyFmt("{}: {s}\n", allow_ansi_color), .{ - display_name, - message, - }); + try writer.print(comptime Output.prettyFmt("{}: {s}\n", allow_ansi_color), .{ display_name, message }); } else if (!name.isEmpty()) { if (!name.hasPrefixComptime("error")) { try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{name}); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index b316a1d764..009c0142a4 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2531,6 +2531,7 @@ pub const ModuleLoader = struct { .@"node:stream/consumers" => return jsSyntheticModule(.@"node:stream/consumers", specifier), .@"node:stream/promises" => return jsSyntheticModule(.@"node:stream/promises", specifier), .@"node:stream/web" => return jsSyntheticModule(.@"node:stream/web", specifier), + .@"node:test" => return jsSyntheticModule(.@"node:test", specifier), .@"node:timers" => return jsSyntheticModule(.@"node:timers", specifier), .@"node:timers/promises" => return jsSyntheticModule(.@"node:timers/promises", specifier), .@"node:tls" => return jsSyntheticModule(.@"node:tls", specifier), @@ -2731,6 +2732,7 @@ pub const HardcodedModule = enum { @"node:stream/promises", @"node:stream/web", @"node:string_decoder", + @"node:test", @"node:timers", @"node:timers/promises", @"node:tls", @@ -2781,6 +2783,8 @@ pub const HardcodedModule = enum { .{ "node-fetch", HardcodedModule.@"node-fetch" }, .{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" }, + .{ "node:test", HardcodedModule.@"node:test" }, + .{ "assert", HardcodedModule.@"node:assert" }, .{ "assert/strict", HardcodedModule.@"node:assert/strict" }, .{ "async_hooks", HardcodedModule.@"node:async_hooks" }, @@ -2852,7 +2856,7 @@ pub const HardcodedModule = enum { pub const Aliases = struct { // Used by both Bun and Node. - const common_alias_kvs = .{ + const common_alias_kvs = [_]struct { string, Alias }{ .{ "node:assert", .{ .path = "assert" } }, .{ "node:assert/strict", .{ .path = "assert/strict" } }, .{ "node:async_hooks", .{ .path = "async_hooks" } }, @@ -2892,6 +2896,7 @@ pub const HardcodedModule = enum { .{ "node:stream/promises", .{ .path = "stream/promises" } }, .{ "node:stream/web", .{ .path = "stream/web" } }, .{ "node:string_decoder", .{ .path = "string_decoder" } }, + .{ "node:test", .{ .path = "node:test" } }, .{ "node:timers", .{ .path = "timers" } }, .{ "node:timers/promises", .{ .path = "timers/promises" } }, .{ "node:tls", .{ .path = "tls" } }, @@ -2906,6 +2911,22 @@ pub const HardcodedModule = enum { .{ "node:worker_threads", .{ .path = "worker_threads" } }, .{ "node:zlib", .{ .path = "zlib" } }, + // These are returned in builtinModules, but probably not many packages use them so we will just alias them. + .{ "node:_http_agent", .{ .path = "http" } }, + .{ "node:_http_client", .{ .path = "http" } }, + .{ "node:_http_common", .{ .path = "http" } }, + .{ "node:_http_incoming", .{ .path = "http" } }, + .{ "node:_http_outgoing", .{ .path = "http" } }, + .{ "node:_http_server", .{ .path = "http" } }, + .{ "node:_stream_duplex", .{ .path = "stream" } }, + .{ "node:_stream_passthrough", .{ .path = "stream" } }, + .{ "node:_stream_readable", .{ .path = "stream" } }, + .{ "node:_stream_transform", .{ .path = "stream" } }, + .{ "node:_stream_writable", .{ .path = "stream" } }, + .{ "node:_stream_wrap", .{ .path = "stream" } }, + .{ "node:_tls_wrap", .{ .path = "tls" } }, + .{ "node:_tls_common", .{ .path = "tls" } }, + .{ "assert", .{ .path = "assert" } }, .{ "assert/strict", .{ .path = "assert/strict" } }, .{ "async_hooks", .{ .path = "async_hooks" } }, @@ -2945,6 +2966,7 @@ pub const HardcodedModule = enum { .{ "stream/promises", .{ .path = "stream/promises" } }, .{ "stream/web", .{ .path = "stream/web" } }, .{ "string_decoder", .{ .path = "string_decoder" } }, + // .{ "test", .{ .path = "test" } }, .{ "timers", .{ .path = "timers" } }, .{ "timers/promises", .{ .path = "timers/promises" } }, .{ "tls", .{ .path = "tls" } }, @@ -2987,7 +3009,7 @@ pub const HardcodedModule = enum { .{ "internal/test/binding", .{ .path = "internal/test/binding" } }, }; - const bun_extra_alias_kvs = .{ + const bun_extra_alias_kvs = [_]struct { string, Alias }{ .{ "bun", .{ .path = "bun", .tag = .bun } }, .{ "bun:test", .{ .path = "bun:test", .tag = .bun_test } }, .{ "bun:ffi", .{ .path = "bun:ffi" } }, @@ -3017,10 +3039,9 @@ pub const HardcodedModule = enum { .{ "abort-controller/polyfill", .{ .path = "abort-controller" } }, }; - const node_alias_kvs = .{ + const node_alias_kvs = [_]struct { string, Alias }{ .{ "inspector/promises", .{ .path = "inspector/promises" } }, .{ "node:inspector/promises", .{ .path = "inspector/promises" } }, - .{ "node:test", .{ .path = "node:test" } }, }; const NodeAliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_alias_kvs); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 1a7440ebe8..302e11fd5e 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -231,14 +231,14 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn getErrno(this: @This()) posix.E { + pub fn getErrno(this: @This()) posix.E { return switch (this) { .result => posix.E.SUCCESS, .err => |e| @enumFromInt(e.errno), }; } - pub inline fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { + pub fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { if (comptime Environment.isWindows) { if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { if (rc != 0) return null; @@ -256,7 +256,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errno(err: anytype, syscall: Syscall.Tag) @This() { + pub fn errno(err: anytype, syscall: Syscall.Tag) @This() { return @This(){ // always truncate .err = .{ @@ -266,7 +266,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { + pub fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { if (comptime Environment.isWindows) { if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { if (rc != 0) return null; @@ -285,7 +285,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }; } - pub inline fn errnoSysP(rc: anytype, syscall: Syscall.Tag, path: anytype) ?@This() { + pub fn errnoSysP(rc: anytype, syscall: Syscall.Tag, path: anytype) ?@This() { if (bun.meta.Item(@TypeOf(path)) == u16) { @compileError("Do not pass WString path to errnoSysP, it needs the path encoded as utf8"); } @@ -306,6 +306,49 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { }, }; } + + pub fn errnoSysFP(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor, path: anytype) ?@This() { + if (comptime Environment.isWindows) { + if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { + if (rc != 0) return null; + } + } + return switch (Syscall.getErrno(rc)) { + .SUCCESS => null, + else => |e| @This(){ + // Always truncate + .err = .{ + .errno = translateToErrInt(e), + .syscall = syscall, + .fd = fd, + .path = bun.asByteSlice(path), + }, + }, + }; + } + + pub fn errnoSysPD(rc: anytype, syscall: Syscall.Tag, path: anytype, dest: anytype) ?@This() { + if (bun.meta.Item(@TypeOf(path)) == u16) { + @compileError("Do not pass WString path to errnoSysPD, it needs the path encoded as utf8"); + } + if (comptime Environment.isWindows) { + if (comptime @TypeOf(rc) == std.os.windows.NTSTATUS) {} else { + if (rc != 0) return null; + } + } + return switch (Syscall.getErrno(rc)) { + .SUCCESS => null, + else => |e| @This(){ + // Always truncate + .err = .{ + .errno = translateToErrInt(e), + .syscall = syscall, + .path = bun.asByteSlice(path), + .dest = bun.asByteSlice(dest), + }, + }, + }; + } }; } @@ -2081,34 +2124,34 @@ pub const Process = struct { if (to.len == 0) { return globalObject.throwInvalidArguments("Expected path to be a non-empty string", .{}); } + const vm = globalObject.bunVM(); + const fs = vm.transpiler.fs; var buf: bun.PathBuffer = undefined; - const slice = to.sliceZBuf(&buf) catch { - return globalObject.throw("Invalid path", .{}); - }; + const slice = to.sliceZBuf(&buf) catch return globalObject.throw("Invalid path", .{}); - switch (Syscall.chdir(slice)) { + switch (Syscall.chdir(fs.top_level_dir, slice)) { .result => { // When we update the cwd from JS, we have to update the bundler's version as well // However, this might be called many times in a row, so we use a pre-allocated buffer // that way we don't have to worry about garbage collector - const fs = JSC.VirtualMachine.get().transpiler.fs; const into_cwd_buf = switch (bun.sys.getcwd(&buf)) { .result => |r| r, .err => |err| { - _ = Syscall.chdir(@as([:0]const u8, @ptrCast(fs.top_level_dir))); + _ = Syscall.chdir(fs.top_level_dir, fs.top_level_dir); return globalObject.throwValue(err.toJSC(globalObject)); }, }; @memcpy(fs.top_level_dir_buf[0..into_cwd_buf.len], into_cwd_buf); - fs.top_level_dir = fs.top_level_dir_buf[0..into_cwd_buf.len]; + fs.top_level_dir_buf[into_cwd_buf.len] = 0; + fs.top_level_dir = fs.top_level_dir_buf[0..into_cwd_buf.len :0]; const len = fs.top_level_dir.len; // Ensure the path ends with a slash if (fs.top_level_dir_buf[len - 1] != std.fs.path.sep) { fs.top_level_dir_buf[len] = std.fs.path.sep; fs.top_level_dir_buf[len + 1] = 0; - fs.top_level_dir = fs.top_level_dir_buf[0 .. len + 1]; + fs.top_level_dir = fs.top_level_dir_buf[0 .. len + 1 :0]; } const withoutTrailingSlash = if (Environment.isWindows) strings.withoutTrailingSlashWindowsPath else strings.withoutTrailingSlash; var str = bun.String.createUTF8(withoutTrailingSlash(fs.top_level_dir)); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index ae4b6b2478..f90c8bbd15 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -4664,7 +4664,6 @@ pub const Expect = struct { if (result.isObject()) { if (try result.get(globalThis, "pass")) |pass_value| { pass = pass_value.toBoolean(); - if (globalThis.hasException()) return false; if (result.fastGet(globalThis, .message)) |message_value| { if (!message_value.isString() and !message_value.isCallable(globalThis.vm())) { diff --git a/src/bun.zig b/src/bun.zig index bd611f9276..6bb9b890a5 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1399,10 +1399,10 @@ fn getFdPathViaCWD(fd: std.posix.fd_t, buf: *[@This().MAX_PATH_BYTES]u8) ![]u8 { pub const getcwd = std.posix.getcwd; -pub fn getcwdAlloc(allocator: std.mem.Allocator) ![]u8 { +pub fn getcwdAlloc(allocator: std.mem.Allocator) ![:0]u8 { var temp: PathBuffer = undefined; const temp_slice = try getcwd(&temp); - return allocator.dupe(u8, temp_slice); + return allocator.dupeZ(u8, temp_slice); } /// Get the absolute path to a file descriptor. diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index e7cccd04b3..660cff180d 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1655,7 +1655,7 @@ pub const BundleV2 = struct { .entry_points = config.entry_points.keys(), .target = config.target.toAPI(), .absolute_working_dir = if (config.dir.list.items.len > 0) - config.dir.slice() + config.dir.sliceWithSentinel() else null, .inject = &.{}, diff --git a/src/c.zig b/src/c.zig index 03ee097e08..1541b9872e 100644 --- a/src/c.zig +++ b/src/c.zig @@ -499,3 +499,7 @@ pub extern fn strlen(ptr: [*c]const u8) usize; pub const passwd = translated.passwd; pub const geteuid = translated.geteuid; pub const getpwuid_r = translated.getpwuid_r; + +export fn Bun__errnoName(err: c_int) ?[*:0]const u8 { + return @tagName(bun.C.SystemErrno.init(err) orelse return null); +} diff --git a/src/cli.zig b/src/cli.zig index 792ff2012b..73a936ff1d 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -47,6 +47,11 @@ pub var start_time: i128 = undefined; const Bunfig = @import("./bunfig.zig").Bunfig; const OOM = bun.OOM; +export var Bun__Node__ProcessNoDeprecation = false; +export var Bun__Node__ProcessThrowDeprecation = false; + +pub var Bun__Node__ProcessTitle: ?string = null; + pub const Cli = struct { pub const CompileTarget = @import("./compile_target.zig"); var wait_group: sync.WaitGroup = undefined; @@ -232,6 +237,9 @@ pub const Arguments = struct { clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, clap.parseParam("--expose-internals Expose internals used for testing Bun itself. Usage of these APIs are completely unsupported.") catch unreachable, + clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, + clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable, + clap.parseParam("--title Set the process title") catch unreachable, }; const auto_or_run_params = [_]ParamType{ @@ -412,7 +420,7 @@ pub const Arguments = struct { var secondbuf: bun.PathBuffer = undefined; const cwd = bun.getcwd(&secondbuf) catch return; - ctx.args.absolute_working_dir = try allocator.dupe(u8, cwd); + ctx.args.absolute_working_dir = try allocator.dupeZ(u8, cwd); } var parts = [_]string{ ctx.args.absolute_working_dir.?, config_path_ }; @@ -487,16 +495,16 @@ pub const Arguments = struct { } } - var cwd: []u8 = undefined; + var cwd: [:0]u8 = undefined; if (args.option("--cwd")) |cwd_arg| { cwd = brk: { var outbuf: bun.PathBuffer = undefined; const out = bun.path.joinAbs(try bun.getcwd(&outbuf), .loose, cwd_arg); - bun.sys.chdir(out).unwrap() catch |err| { + bun.sys.chdir("", out).unwrap() catch |err| { Output.err(err, "Could not change directory to \"{s}\"\n", .{cwd_arg}); Global.exit(1); }; - break :brk try allocator.dupe(u8, out); + break :brk try allocator.dupeZ(u8, out); }; } else { cwd = try bun.getcwdAlloc(allocator); @@ -795,6 +803,15 @@ pub const Arguments = struct { if (args.flag("--expose-internals")) { bun.JSC.ModuleLoader.is_allowed_to_use_internal_testing_apis = true; } + if (args.flag("--no-deprecation")) { + Bun__Node__ProcessNoDeprecation = true; + } + if (args.flag("--throw-deprecation")) { + Bun__Node__ProcessThrowDeprecation = true; + } + if (args.option("--title")) |title| { + Bun__Node__ProcessTitle = title; + } } if (opts.port != null and opts.origin == null) { diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index fb822fa4e9..1ce8301f15 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -581,7 +581,7 @@ pub const UpgradeCommand = struct { tmpdir_path_buf[tmpdir_path.len] = 0; const tmpdir_z = tmpdir_path_buf[0..tmpdir_path.len :0]; - _ = bun.sys.chdir(tmpdir_z); + _ = bun.sys.chdir("", tmpdir_z); const tmpname = "bun.zip"; const exe = diff --git a/src/codegen/generate-node-errors.ts b/src/codegen/generate-node-errors.ts index debbb07fc5..e4c807be70 100644 --- a/src/codegen/generate-node-errors.ts +++ b/src/codegen/generate-node-errors.ts @@ -15,6 +15,8 @@ enumHeader = ` // Generated by: src/codegen/generate-node-errors.ts #pragma once +#include + namespace Bun { static constexpr size_t NODE_ERROR_COUNT = ${NodeErrors.length}; enum class ErrorCode : uint8_t { @@ -25,6 +27,8 @@ listHeader = ` // Generated by: src/codegen/generate-node-errors.ts #pragma once +#include + struct ErrorCodeData { JSC::ErrorType type; WTF::ASCIILiteral name; diff --git a/src/fs.zig b/src/fs.zig index 9f8cd22f24..b1feeadd4b 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -37,7 +37,7 @@ pub const Preallocate = struct { }; pub const FileSystem = struct { - top_level_dir: string, + top_level_dir: stringZ, // used on subsequent updates top_level_dir_buf: bun.PathBuffer = undefined, @@ -108,22 +108,14 @@ pub const FileSystem = struct { ENOTDIR, }; - pub fn init(top_level_dir: ?string) !*FileSystem { + pub fn init(top_level_dir: ?stringZ) !*FileSystem { return initWithForce(top_level_dir, false); } - pub fn initWithForce(top_level_dir_: ?string, comptime force: bool) !*FileSystem { + pub fn initWithForce(top_level_dir_: ?stringZ, comptime force: bool) !*FileSystem { const allocator = bun.fs_allocator; var top_level_dir = top_level_dir_ orelse (if (Environment.isBrowser) "/project/" else try bun.getcwdAlloc(allocator)); - - // Ensure there's a trailing separator in the top level directory - // This makes path resolution more reliable - if (!bun.path.isSepAny(top_level_dir[top_level_dir.len - 1])) { - const tld = try allocator.alloc(u8, top_level_dir.len + 1); - bun.copy(u8, tld, top_level_dir); - tld[tld.len - 1] = std.fs.path.sep; - top_level_dir = tld; - } + _ = &top_level_dir; if (!instance_loaded or force) { instance = FileSystem{ diff --git a/src/install/install.zig b/src/install/install.zig index d7bed546cc..546dbf2ef4 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -8843,7 +8843,7 @@ pub const PackageManager = struct { } else child_path; if (strings.eqlLong(maybe_workspace_path, path, true)) { - fs.top_level_dir = parent; + fs.top_level_dir = try bun.default_allocator.dupeZ(u8, parent); found = true; child_json.close(); if (comptime Environment.isWindows) { @@ -8860,16 +8860,15 @@ pub const PackageManager = struct { } } - fs.top_level_dir = child_cwd; + fs.top_level_dir = try bun.default_allocator.dupeZ(u8, child_cwd); break :root_package_json_file child_json; }; - try bun.sys.chdir(fs.top_level_dir).unwrap(); + try bun.sys.chdir(fs.top_level_dir, fs.top_level_dir).unwrap(); try BunArguments.loadConfig(ctx.allocator, cli.config, ctx, .InstallCommand); bun.copy(u8, &cwd_buf, fs.top_level_dir); - cwd_buf[fs.top_level_dir.len] = std.fs.path.sep; - cwd_buf[fs.top_level_dir.len + 1] = 0; - fs.top_level_dir = cwd_buf[0 .. fs.top_level_dir.len + 1]; + cwd_buf[fs.top_level_dir.len] = 0; + fs.top_level_dir = cwd_buf[0..fs.top_level_dir.len :0]; package_json_cwd = try bun.getFdPath(root_package_json_file.handle, &package_json_cwd_buf); const entries_option = try fs.fs.readDirectory(fs.top_level_dir, null, 0, true); @@ -10213,7 +10212,7 @@ pub const PackageManager = struct { buf[cwd_.len] = 0; final_path = buf[0..cwd_.len :0]; } - bun.sys.chdir(final_path).unwrap() catch |err| { + bun.sys.chdir("", final_path).unwrap() catch |err| { Output.errGeneric("failed to change directory to \"{s}\": {s}\n", .{ final_path, @errorName(err) }); Global.crash(); }; diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 47f4c6d8ac..dad7851954 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1681,7 +1681,7 @@ pub const Printer = struct { } if (lockfile_path.len > 0 and lockfile_path[0] == std.fs.path.sep) - _ = bun.sys.chdir(std.fs.path.dirname(lockfile_path) orelse std.fs.path.sep_str); + _ = bun.sys.chdir("", std.fs.path.dirname(lockfile_path) orelse std.fs.path.sep_str); _ = try FileSystem.init(null); diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 2c07c8aa0e..d8df1e8938 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -549,6 +549,22 @@ declare interface Error { */ declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedType: string, actualValue: string): TypeError; declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedTypes: any[], actualValue: string): TypeError; +declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; + +declare function $ERR_IPC_DISCONNECTED(): Error; +declare function $ERR_SERVER_NOT_RUNNING(): Error; +declare function $ERR_IPC_CHANNEL_CLOSED(): Error; +declare function $ERR_SOCKET_BAD_TYPE(): Error; +declare function $ERR_ZLIB_INITIALIZATION_FAILED(): Error; +declare function $ERR_BUFFER_OUT_OF_BOUNDS(): Error; +declare function $ERR_IPC_ONE_PIPE(): Error; +declare function $ERR_SOCKET_ALREADY_BOUND(): Error; +declare function $ERR_SOCKET_BAD_BUFFER_SIZE(): Error; +declare function $ERR_SOCKET_DGRAM_IS_CONNECTED(): Error; +declare function $ERR_SOCKET_DGRAM_NOT_CONNECTED(): Error; +declare function $ERR_SOCKET_DGRAM_NOT_RUNNING(): Error; +declare function $ERR_INVALID_CURSOR_POS(): Error; + /** * Convert a function to a class-like object. * diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index b3cb19b816..b7017f0154 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -71,6 +71,7 @@ using namespace JSC; macro(dataView) \ macro(decode) \ macro(delimiter) \ + macro(dest) \ macro(destroy) \ macro(dir) \ macro(direct) \ @@ -244,6 +245,7 @@ using namespace JSC; macro(version) \ macro(versions) \ macro(view) \ + macro(warning) \ macro(writable) \ macro(WritableStream) \ macro(WritableStreamDefaultController) \ diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index ee9c6c6cf7..7377e5710c 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -142,7 +142,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { const { inspect, formatWithOptions, stripVTControlCharacters } = require("node:util"); const { isBuffer } = require("node:buffer"); - const { validateObject, validateInteger, validateArray } = require("internal/validators"); + const { validateObject, validateInteger, validateArray, validateOneOf } = require("internal/validators"); const kMaxGroupIndentation = 1000; const StringPrototypeIncludes = String.prototype.includes; @@ -298,11 +298,7 @@ export function createConsoleConstructor(console: typeof globalThis.console) { throw $ERR_CONSOLE_WRITABLE_STREAM("stderr is not a writable stream"); } - if (typeof colorMode !== "boolean" && colorMode !== "auto") { - throw $ERR_INVALID_ARG_VALUE( - "The argument 'colorMode' must be one of: 'auto', true, false. Received " + inspect(colorMode), - ); - } + validateOneOf(colorMode, "colorMode", ["auto", true, false]); if (groupIndentation !== undefined) { validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); diff --git a/src/js/internal/errors.ts b/src/js/internal/errors.ts index 739958bf03..f12e6a067c 100644 --- a/src/js/internal/errors.ts +++ b/src/js/internal/errors.ts @@ -1,14 +1,7 @@ export default { - ERR_INVALID_ARG_TYPE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_INVALID_ARG_TYPE", 3), ERR_OUT_OF_RANGE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_OUT_OF_RANGE", 3), - ERR_IPC_DISCONNECTED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_IPC_DISCONNECTED", 0), - ERR_SERVER_NOT_RUNNING: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_SERVER_NOT_RUNNING", 0), - ERR_IPC_CHANNEL_CLOSED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_IPC_CHANNEL_CLOSED", 0), - ERR_SOCKET_BAD_TYPE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_SOCKET_BAD_TYPE", 0), ERR_INVALID_PROTOCOL: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_INVALID_PROTOCOL", 0), ERR_BROTLI_INVALID_PARAM: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BROTLI_INVALID_PARAM", 0), ERR_BUFFER_TOO_LARGE: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BUFFER_TOO_LARGE", 0), - ERR_ZLIB_INITIALIZATION_FAILED: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_ZLIB_INITIALIZATION_FAILED", 0), - ERR_BUFFER_OUT_OF_BOUNDS: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_BUFFER_OUT_OF_BOUNDS", 0), ERR_UNHANDLED_ERROR: $newCppFunction("ErrorCode.cpp", "jsFunction_ERR_UNHANDLED_ERROR", 0), }; diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index df0f652ee9..dc1dcd93e0 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -1,10 +1,11 @@ class NotImplementedError extends Error { code: string; - constructor(feature: string, issue?: number) { + constructor(feature: string, issue?: number, extra?: string) { super( feature + " is not yet implemented in Bun." + - (issue ? " Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/" + issue : ""), + (issue ? " Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/" + issue : "") + + (!!extra ? ". " + extra : ""), ); this.name = "NotImplementedError"; this.code = "ERR_NOT_IMPLEMENTED"; @@ -14,11 +15,11 @@ class NotImplementedError extends Error { } } -function throwNotImplemented(feature: string, issue?: number): never { +function throwNotImplemented(feature: string, issue?: number, extra?: string): never { // in the definition so that it isn't bundled unless used hideFromStack(throwNotImplemented); - throw new NotImplementedError(feature, issue); + throw new NotImplementedError(feature, issue, extra); } function hideFromStack(...fns) { diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index a6612d6db0..414b943e7f 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -1,6 +1,10 @@ const { hideFromStack } = require("internal/shared"); const { ArrayIsArray } = require("internal/primordials"); + const RegExpPrototypeExec = RegExp.prototype.exec; +const ArrayPrototypeIncludes = Array.prototype.includes; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypeMap = Array.prototype.map; const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; /** @@ -65,6 +69,18 @@ function validateObject(value, name) { } hideFromStack(validateObject); +function validateOneOf(value, name, oneOf) { + if (!ArrayPrototypeIncludes.$call(oneOf, value)) { + const allowed = ArrayPrototypeJoin.$call( + ArrayPrototypeMap.$call(oneOf, v => (typeof v === "string" ? `'${v}'` : String(v))), + ", ", + ); + const reason = "must be one of: " + allowed; + throw $ERR_INVALID_ARG_VALUE(name, value, reason); + } +} +hideFromStack(validateOneOf); + export default { validateObject: validateObject, validateLinkHeaderValue: validateLinkHeaderValue, @@ -103,4 +119,6 @@ export default { validateUndefined: $newCppFunction("NodeValidator.cpp", "jsFunction_validateUndefined", 0), /** `(buffer, name = 'buffer')` */ validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0), + /** `(value, name, oneOf)` */ + validateOneOf, }; diff --git a/src/js/node/assert.ts b/src/js/node/assert.ts index e5343bd18e..daf536b8da 100644 --- a/src/js/node/assert.ts +++ b/src/js/node/assert.ts @@ -139,19 +139,6 @@ var require_errors = __commonJS({ }, TypeError, ); - createErrorType( - "ERR_INVALID_ARG_VALUE", - function (name, value) { - var reason = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : "is invalid"; - var inspected = util.inspect(value); - return ( - inspected.length > 128 && (inspected = "".concat(inspected.slice(0, 128), "...")), - "The argument '".concat(name, "' ").concat(reason, ". Received ").concat(inspected) - ); - }, - TypeError, - RangeError, - ); createErrorType( "ERR_INVALID_RETURN_VALUE", function (input, name, value) { @@ -835,7 +822,6 @@ var require_assert = __commonJS({ _require$codes = _require.codes, ERR_AMBIGUOUS_ARGUMENT = _require$codes.ERR_AMBIGUOUS_ARGUMENT, ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE = _require$codes.ERR_INVALID_ARG_VALUE, ERR_INVALID_RETURN_VALUE = _require$codes.ERR_INVALID_RETURN_VALUE, ERR_MISSING_ARGS = _require$codes.ERR_MISSING_ARGS, AssertionError = require_assertion_error(), @@ -1065,7 +1051,7 @@ var require_assert = __commonJS({ } var keys = Object.keys(expected); if (expected instanceof Error) keys.push("name", "message"); - else if (keys.length === 0) throw new ERR_INVALID_ARG_VALUE("error", expected, "may not be an empty object"); + else if (keys.length === 0) throw $ERR_INVALID_ARG_VALUE("error", expected, "may not be an empty object"); return ( keys.forEach(function (key) { return ( diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index 30e2077b80..d8a12370b4 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -2,7 +2,6 @@ const EventEmitter = require("node:events"); const StreamModule = require("node:stream"); const OsModule = require("node:os"); -const { ERR_INVALID_ARG_TYPE, ERR_IPC_DISCONNECTED } = require("internal/errors"); const { kHandle } = require("internal/shared"); const { validateBoolean, @@ -76,9 +75,7 @@ var ReadableFromWeb; // TODO: Add these params after support added in Bun.spawn // uid Sets the user identity of the process (see setuid(2)). // gid Sets the group identity of the process (see setgid(2)). -// detached Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). -// TODO: Add support for ipc option, verify only one IPC channel in array // stdio | Child's stdio configuration (see options.stdio). // Support wrapped ipc types (e.g. net.Socket, dgram.Socket, TTY, etc.) // IPC FD passing support @@ -556,7 +553,7 @@ function spawnSync(file, args, options) { } else if (typeof input === "string") { bunStdio[0] = Buffer.from(input, encoding || "utf8"); } else { - throw ERR_INVALID_ARG_TYPE(`options.stdio[0]`, ["Buffer", "TypedArray", "DataView", "string"], input); + throw $ERR_INVALID_ARG_TYPE(`options.stdio[0]`, ["Buffer", "TypedArray", "DataView", "string"], input); } } @@ -699,7 +696,7 @@ function stdioStringToArray(stdio, channel) { options = [0, 1, 2]; break; default: - throw ERR_INVALID_ARG_VALUE("stdio", stdio); + throw $ERR_INVALID_ARG_VALUE("stdio", stdio); } if (channel) $arrayPush(options, channel); @@ -797,7 +794,7 @@ function sanitizeKillSignal(killSignal) { if (typeof killSignal === "string" || typeof killSignal === "number") { return convertToValidSignal(killSignal); } else if (killSignal != null) { - throw ERR_INVALID_ARG_TYPE("options.killSignal", ["string", "number"], killSignal); + throw $ERR_INVALID_ARG_TYPE("options.killSignal", ["string", "number"], killSignal); } } @@ -877,14 +874,14 @@ function normalizeSpawnArguments(file, args, options) { validateString(file, "file"); validateArgumentNullCheck(file, "file"); - if (file.length === 0) throw ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); + if (file.length === 0) throw $ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); if ($isJSArray(args)) { args = ArrayPrototypeSlice.$call(args); } else if (args == null) { args = []; } else if (typeof args !== "object") { - throw ERR_INVALID_ARG_TYPE("args", "object", args); + throw $ERR_INVALID_ARG_TYPE("args", "object", args); } else { options = args; args = []; @@ -909,17 +906,17 @@ function normalizeSpawnArguments(file, args, options) { // Validate the uid, if present. if (options.uid != null && !isInt32(options.uid)) { - throw ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); + throw $ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); } // Validate the gid, if present. if (options.gid != null && !isInt32(options.gid)) { - throw ERR_INVALID_ARG_TYPE("options.gid", "int32", options.gid); + throw $ERR_INVALID_ARG_TYPE("options.gid", "int32", options.gid); } // Validate the shell, if present. if (options.shell != null && typeof options.shell !== "boolean" && typeof options.shell !== "string") { - throw ERR_INVALID_ARG_TYPE("options.shell", ["boolean", "string"], options.shell); + throw $ERR_INVALID_ARG_TYPE("options.shell", ["boolean", "string"], options.shell); } // Validate argv0, if present. @@ -1340,7 +1337,7 @@ class ChildProcess extends EventEmitter { options = undefined; } else if (options !== undefined) { if (typeof options !== "object" || options === null) { - throw ERR_INVALID_ARG_TYPE("options", "object", options); + throw $ERR_INVALID_ARG_TYPE("options", "object", options); } } @@ -1373,7 +1370,7 @@ class ChildProcess extends EventEmitter { $assert(this.connected); this.#handle.disconnect(); } else if (!ok) { - this.emit("error", ERR_IPC_DISCONNECTED()); + this.emit("error", $ERR_IPC_DISCONNECTED()); return; } this.#handle.disconnect(); @@ -1432,7 +1429,7 @@ const nodeToBunLookup = { ipc: "ipc", }; -function nodeToBun(item, index) { +function nodeToBun(item: string, index: number): string | number | null { // If not defined, use the default. // For stdin/stdout/stderr, it's pipe. For others, it's ignore. if (item == null) { @@ -1501,6 +1498,7 @@ function fdToStdioName(fd) { function getBunStdioFromOptions(stdio) { const normalizedStdio = normalizeStdio(stdio); + if (normalizedStdio.filter(v => v === "ipc").length > 1) throw $ERR_IPC_ONE_PIPE(); // Node options: // pipe: just a pipe // ipc = can only be one in array @@ -1527,7 +1525,7 @@ function getBunStdioFromOptions(stdio) { return bunStdio; } -function normalizeStdio(stdio) { +function normalizeStdio(stdio): string[] { if (typeof stdio === "string") { switch (stdio) { case "ignore": @@ -1616,7 +1614,7 @@ function validateMaxBuffer(maxBuffer) { function validateArgumentNullCheck(arg, propName) { if (typeof arg === "string" && StringPrototypeIncludes.$call(arg, "\u0000")) { - throw ERR_INVALID_ARG_VALUE(propName, arg, "must be a string without null bytes"); + throw $ERR_INVALID_ARG_VALUE(propName, arg, "must be a string without null bytes"); } } @@ -1649,7 +1647,7 @@ const validateOneOf = (value, name, oneOf) => { ", ", ); const reason = "must be one of: " + allowed; - throw ERR_INVALID_ARG_VALUE(name, value, reason); + throw $ERR_INVALID_ARG_VALUE(name, value, reason); } }; @@ -1675,7 +1673,7 @@ const validateObject = (value, name, options = null) => { (!allowArray && $isJSArray(value)) || (typeof value !== "object" && (!allowFunction || typeof value !== "function")) ) { - throw ERR_INVALID_ARG_TYPE(name, "object", value); + throw $ERR_INVALID_ARG_TYPE(name, "object", value); } }; @@ -1696,7 +1694,7 @@ function nullCheck(path, propName, throwError = true) { return; } - const err = ERR_INVALID_ARG_VALUE(propName, path, "must be a string or Uint8Array without null bytes"); + const err = $ERR_INVALID_ARG_VALUE(propName, path, "must be a string or Uint8Array without null bytes"); if (throwError) { throw err; } @@ -1705,7 +1703,7 @@ function nullCheck(path, propName, throwError = true) { function validatePath(path, propName = "path") { if (typeof path !== "string" && !isUint8Array(path)) { - throw ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], path); + throw $ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], path); } const err = nullCheck(path, propName, false); @@ -1751,7 +1749,7 @@ class AbortError extends Error { name = "AbortError"; constructor(message = "The operation was aborted", options = undefined) { if (options !== undefined && typeof options !== "object") { - throw ERR_INVALID_ARG_TYPE("options", "object", options); + throw $ERR_INVALID_ARG_TYPE("options", "object", options); } super(message, options); } @@ -1937,12 +1935,6 @@ function ERR_INVALID_OPT_VALUE(name, value) { return err; } -function ERR_INVALID_ARG_VALUE(name, value, reason) { - const err = new Error(`The value "${value}" is invalid for argument '${name}'. Reason: ${reason}`); - err.code = "ERR_INVALID_ARG_VALUE"; - return err; -} - function ERR_CHILD_PROCESS_IPC_REQUIRED(name) { const err = new TypeError(`Forked processes must have an IPC channel, missing value 'ipc' in ${name}`); err.code = "ERR_CHILD_PROCESS_IPC_REQUIRED"; diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index b83c64bafd..ed652132e5 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -34,7 +34,6 @@ const kStateSymbol = Symbol("state symbol"); const async_id_symbol = Symbol("async_id_symbol"); const { hideFromStack, throwNotImplemented } = require("internal/shared"); -const { ERR_SOCKET_BAD_TYPE } = require("internal/errors"); const { validateString, validateNumber, @@ -54,48 +53,6 @@ const { const EventEmitter = require("node:events"); -class ERR_OUT_OF_RANGE extends Error { - constructor(argumentName, range, received) { - super(`The value of "${argumentName}" is out of range. It must be ${range}. Received ${received}`); - this.code = "ERR_OUT_OF_RANGE"; - } -} - -class ERR_BUFFER_OUT_OF_BOUNDS extends Error { - constructor() { - super("Buffer offset or length is out of bounds"); - this.code = "ERR_BUFFER_OUT_OF_BOUNDS"; - } -} - -class ERR_INVALID_ARG_TYPE extends Error { - constructor(argName, expected, actual) { - super(`The "${argName}" argument must be of type ${expected}. Received type ${typeof actual}`); - this.code = "ERR_INVALID_ARG_TYPE"; - } -} - -class ERR_MISSING_ARGS extends Error { - constructor(argName) { - super(`The "${argName}" argument is required`); - this.code = "ERR_MISSING_ARGS"; - } -} - -class ERR_SOCKET_ALREADY_BOUND extends Error { - constructor() { - super("Socket is already bound"); - this.code = "ERR_SOCKET_ALREADY_BOUND"; - } -} - -class ERR_SOCKET_BAD_BUFFER_SIZE extends Error { - constructor() { - super("Buffer size must be a number"); - this.code = "ERR_SOCKET_BAD_BUFFER_SIZE"; - } -} - class ERR_SOCKET_BUFFER_SIZE extends Error { constructor(ctx) { super(`Invalid buffer size: ${ctx}`); @@ -103,34 +60,6 @@ class ERR_SOCKET_BUFFER_SIZE extends Error { } } -class ERR_SOCKET_DGRAM_IS_CONNECTED extends Error { - constructor() { - super("Socket is connected"); - this.code = "ERR_SOCKET_DGRAM_IS_CONNECTED"; - } -} - -class ERR_SOCKET_DGRAM_NOT_CONNECTED extends Error { - constructor() { - super("Socket is not connected"); - this.code = "ERR_SOCKET_DGRAM_NOT_CONNECTED"; - } -} - -class ERR_SOCKET_BAD_PORT extends Error { - constructor(name, port, allowZero) { - super(`Invalid ${name}: ${port}. Ports must be >= 0 and <= 65535. ${allowZero ? "0" : ""}`); - this.code = "ERR_SOCKET_BAD_PORT"; - } -} - -class ERR_SOCKET_DGRAM_NOT_RUNNING extends Error { - constructor() { - super("Socket is not running"); - this.code = "ERR_SOCKET_DGRAM_NOT_RUNNING"; - } -} - function isInt32(value) { return value === (value | 0); } @@ -167,7 +96,7 @@ function newHandle(type, lookup) { } else if (type === "udp6") { handle.lookup = FunctionPrototypeBind(lookup6, handle, lookup); } else { - throw new ERR_SOCKET_BAD_TYPE(); + throw $ERR_SOCKET_BAD_TYPE(); } return handle; @@ -241,7 +170,7 @@ function createSocket(type, listener) { } function bufferSize(self, size, buffer) { - if (size >>> 0 !== size) throw new ERR_SOCKET_BAD_BUFFER_SIZE(); + if (size >>> 0 !== size) throw $ERR_SOCKET_BAD_BUFFER_SIZE(); const ctx = {}; // const ret = self[kStateSymbol].handle.bufferSize(size, buffer, ctx); @@ -257,7 +186,7 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { const state = this[kStateSymbol]; - if (state.bindState !== BIND_STATE_UNBOUND) throw new ERR_SOCKET_ALREADY_BOUND(); + if (state.bindState !== BIND_STATE_UNBOUND) throw $ERR_SOCKET_ALREADY_BOUND(); state.bindState = BIND_STATE_BINDING; @@ -393,7 +322,7 @@ Socket.prototype.connect = function (port, address, callback) { const state = this[kStateSymbol]; - if (state.connectState !== CONNECT_STATE_DISCONNECTED) throw new ERR_SOCKET_DGRAM_IS_CONNECTED(); + if (state.connectState !== CONNECT_STATE_DISCONNECTED) throw $ERR_SOCKET_DGRAM_IS_CONNECTED(); state.connectState = CONNECT_STATE_CONNECTING; if (state.bindState === BIND_STATE_UNBOUND) this.bind({ port: 0, exclusive: true }, null); @@ -451,7 +380,7 @@ const disconnectFn = $newZigFunction("udp_socket.zig", "UDPSocket.jsDisconnect", Socket.prototype.disconnect = function () { const state = this[kStateSymbol]; - if (state.connectState !== CONNECT_STATE_CONNECTED) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (state.connectState !== CONNECT_STATE_CONNECTED) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); disconnectFn.$call(state.handle.socket); state.connectState = CONNECT_STATE_DISCONNECTED; @@ -471,17 +400,17 @@ function sliceBuffer(buffer, offset, length) { if (typeof buffer === "string") { buffer = Buffer.from(buffer); } else if (!ArrayBuffer.isView(buffer)) { - throw new ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); } offset = offset >>> 0; length = length >>> 0; if (offset > buffer.byteLength) { - throw new ERR_BUFFER_OUT_OF_BOUNDS("offset"); + throw $ERR_BUFFER_OUT_OF_BOUNDS("offset"); } if (offset + length > buffer.byteLength) { - throw new ERR_BUFFER_OUT_OF_BOUNDS("length"); + throw $ERR_BUFFER_OUT_OF_BOUNDS("length"); } return Buffer.from(buffer.buffer, buffer.byteOffset + offset, length); @@ -570,19 +499,19 @@ Socket.prototype.send = function (buffer, offset, length, port, address, callbac callback = offset; } - if (port || address) throw new ERR_SOCKET_DGRAM_IS_CONNECTED(); + if (port || address) throw $ERR_SOCKET_DGRAM_IS_CONNECTED(); } if (!Array.isArray(buffer)) { if (typeof buffer === "string") { list = [Buffer.from(buffer)]; } else if (!ArrayBuffer.isView(buffer)) { - throw new ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", ["Buffer", "TypedArray", "DataView", "string"], buffer); } else { list = [buffer]; } } else if (!(list = fixBufferList(buffer))) { - throw new ERR_INVALID_ARG_TYPE("buffer list arguments", ["Buffer", "TypedArray", "DataView", "string"], buffer); + throw $ERR_INVALID_ARG_TYPE("buffer list arguments", ["Buffer", "TypedArray", "DataView", "string"], buffer); } if (!connected) port = validatePort(port, "Port", false); @@ -747,7 +676,7 @@ function socketCloseNT(self) { Socket.prototype.address = function () { const addr = this[kStateSymbol].handle.socket?.address; - if (!addr) throw new ERR_SOCKET_DGRAM_NOT_RUNNING(); + if (!addr) throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); return addr; }; @@ -755,11 +684,11 @@ Socket.prototype.remoteAddress = function () { const state = this[kStateSymbol]; const socket = state.handle.socket; - if (!socket) throw new ERR_SOCKET_DGRAM_NOT_RUNNING(); + if (!socket) throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); - if (state.connectState !== CONNECT_STATE_CONNECTED) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (state.connectState !== CONNECT_STATE_CONNECTED) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); - if (!socket.remoteAddress) throw new ERR_SOCKET_DGRAM_NOT_CONNECTED(); + if (!socket.remoteAddress) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED(); return socket.remoteAddress; }; diff --git a/src/js/node/diagnostics_channel.ts b/src/js/node/diagnostics_channel.ts index 57722725e7..2aa78dbb12 100644 --- a/src/js/node/diagnostics_channel.ts +++ b/src/js/node/diagnostics_channel.ts @@ -2,7 +2,6 @@ // Reference: https://github.com/nodejs/node/blob/fb47afc335ef78a8cef7eac52b8ee7f045300696/lib/diagnostics_channel.js const { validateFunction } = require("internal/validators"); -const { ERR_INVALID_ARG_TYPE } = require("internal/errors"); const SafeMap = Map; const SafeFinalizationRegistry = FinalizationRegistry; @@ -212,7 +211,7 @@ function channel(name) { if (channel) return channel; if (typeof name !== "string" && typeof name !== "symbol") { - throw ERR_INVALID_ARG_TYPE("channel", "string or symbol", name); + throw $ERR_INVALID_ARG_TYPE("channel", "string or symbol", name); } return new Channel(name); @@ -237,7 +236,7 @@ const traceEvents = ["start", "end", "asyncStart", "asyncEnd", "error"]; function assertChannel(value, name) { if (!(value instanceof Channel)) { - throw ERR_INVALID_ARG_TYPE(name, ["Channel"], value); + throw $ERR_INVALID_ARG_TYPE(name, ["Channel"], value); } } @@ -264,7 +263,7 @@ class TracingChannel { this.asyncEnd = asyncEnd; this.error = error; } else { - throw ERR_INVALID_ARG_TYPE("nameOrChannels", ["string, object, or Channel"], nameOrChannels); + throw $ERR_INVALID_ARG_TYPE("nameOrChannels", ["string, object, or Channel"], nameOrChannels); } } diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 85a5c7707c..64a14f8edb 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -23,7 +23,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const { ERR_INVALID_ARG_TYPE, ERR_UNHANDLED_ERROR } = require("internal/errors"); +const { ERR_UNHANDLED_ERROR } = require("internal/errors"); const { validateObject, validateInteger, @@ -55,7 +55,7 @@ const kEmptyObject = Object.freeze({ __proto__: null }); var defaultMaxListeners = 10; // EventEmitter must be a standard function because some old code will do weird tricks like `EventEmitter.$apply(this)`. -const EventEmitter = function EventEmitter(opts) { +function EventEmitter(opts) { if (this._events === undefined || this._events === this.__proto__._events) { this._events = { __proto__: null }; this._eventsCount = 0; @@ -65,13 +65,10 @@ const EventEmitter = function EventEmitter(opts) { if ((this[kCapture] = opts?.captureRejections ? Boolean(opts?.captureRejections) : EventEmitterPrototype[kCapture])) { this.emit = emitWithRejectionCapture; } -}; +} Object.defineProperty(EventEmitter, "name", { value: "EventEmitter", configurable: true }); const EventEmitterPrototype = (EventEmitter.prototype = {}); -EventEmitterPrototype._events = undefined; -EventEmitterPrototype._eventsCount = 0; -EventEmitterPrototype._maxListeners = undefined; EventEmitterPrototype.setMaxListeners = function setMaxListeners(n) { validateNumber(n, "setMaxListeners", 0); this._maxListeners = n; @@ -530,7 +527,7 @@ function on(emitter, event, options = kEmptyObject) { throw(err) { if (!err || !(err instanceof Error)) { - throw ERR_INVALID_ARG_TYPE("EventEmitter.AsyncIterator", "Error", err); + throw $ERR_INVALID_ARG_TYPE("EventEmitter.AsyncIterator", "Error", err); } errorHandler(err); }, @@ -661,7 +658,7 @@ function setMaxListeners(n = defaultMaxListeners, ...eventTargets) { } else if (typeof target.setMaxListeners === "function") { target.setMaxListeners(n); } else { - throw ERR_INVALID_ARG_TYPE("eventTargets", ["EventEmitter", "EventTarget"], target); + throw $ERR_INVALID_ARG_TYPE("eventTargets", ["EventEmitter", "EventTarget"], target); } } } @@ -689,7 +686,7 @@ function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { } else if (typeof emitter.removeEventListener === "function") { emitter.removeEventListener(name, listener, flags); } else { - throw ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + throw $ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); } } @@ -703,14 +700,14 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) { } else if (typeof emitter.addEventListener === "function") { emitter.addEventListener(name, listener, flags); } else { - throw ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + throw $ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); } } class AbortError extends Error { constructor(message = "The operation was aborted", options = undefined) { if (options !== undefined && typeof options !== "object") { - throw ERR_INVALID_ARG_TYPE("options", "object", options); + throw $ERR_INVALID_ARG_TYPE("options", "object", options); } super(message, options); this.code = "ABORT_ERR"; @@ -718,12 +715,6 @@ class AbortError extends Error { } } -function ERR_OUT_OF_RANGE(name, range, value) { - const err = new RangeError(`The "${name}" argument is out of range. It must be ${range}. Received ${value}`); - err.code = "ERR_OUT_OF_RANGE"; - return err; -} - function checkListener(listener) { validateFunction(listener, "listener"); } @@ -741,19 +732,19 @@ function getMaxListeners(emitterOrTarget) { emitterOrTarget[kMaxEventTargetListeners] ??= defaultMaxListeners; return emitterOrTarget[kMaxEventTargetListeners]; } - throw ERR_INVALID_ARG_TYPE("emitter", ["EventEmitter", "EventTarget"], emitterOrTarget); + throw $ERR_INVALID_ARG_TYPE("emitter", ["EventEmitter", "EventTarget"], emitterOrTarget); } Object.defineProperty(getMaxListeners, "name", { value: "getMaxListeners" }); // Copy-pasta from Node.js source code function addAbortListener(signal, listener) { if (signal === undefined) { - throw ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); } validateAbortSignal(signal, "signal"); if (typeof listener !== "function") { - throw ERR_INVALID_ARG_TYPE("listener", "function", listener); + throw $ERR_INVALID_ARG_TYPE("listener", "function", listener); } let removeEventListener; diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 08cd434ade..46c29c17ba 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -2,7 +2,7 @@ const EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); const { Duplex, Readable, Writable } = require("node:stream"); -const { ERR_INVALID_ARG_TYPE, ERR_INVALID_PROTOCOL } = require("internal/errors"); +const { ERR_INVALID_PROTOCOL } = require("internal/errors"); const { isPrimary } = require("internal/cluster/isPrimary"); const { kAutoDestroyed } = require("internal/shared"); const { urlToHttpOptions } = require("internal/url"); @@ -126,7 +126,7 @@ function isValidTLSArray(obj) { function validateMsecs(numberlike: any, field: string) { if (typeof numberlike !== "number" || numberlike < 0) { - throw ERR_INVALID_ARG_TYPE(field, "number", numberlike); + throw $ERR_INVALID_ARG_TYPE(field, "number", numberlike); } return numberlike; @@ -1806,7 +1806,7 @@ class ClientRequest extends OutgoingMessage { } else if (agent == null) { agent = defaultAgent; } else if (typeof agent.addRequest !== "function") { - throw ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); + throw $ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); } this.#agent = agent; @@ -1852,8 +1852,7 @@ class ClientRequest extends OutgoingMessage { let method = options.method; const methodIsString = typeof method === "string"; if (method !== null && method !== undefined && !methodIsString) { - // throw ERR_INVALID_ARG_TYPE("options.method", "string", method); - throw new Error("ERR_INVALID_ARG_TYPE: options.method"); + throw $ERR_INVALID_ARG_TYPE("options.method", "string", method); } if (methodIsString && method) { @@ -2088,12 +2087,7 @@ class ClientRequest extends OutgoingMessage { function validateHost(host, name) { if (host !== null && host !== undefined && typeof host !== "string") { - // throw ERR_INVALID_ARG_TYPE( - // `options.${name}`, - // ["string", "undefined", "null"], - // host, - // ); - throw new Error("Invalid arg type in options"); + throw $ERR_INVALID_ARG_TYPE(`options.${name}`, ["string", "undefined", "null"], host); } return host; } diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 277900dd99..1fad065074 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -24,7 +24,6 @@ const { Duplex } = require("node:stream"); const EventEmitter = require("node:events"); const { addServerName, upgradeDuplexToTLS, isNamedPipeSocket } = require("../internal/net"); const { ExceptionWithHostPort } = require("internal/shared"); -const { ERR_SERVER_NOT_RUNNING } = require("internal/errors"); // IPv4 Segment const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; @@ -1122,7 +1121,7 @@ class Server extends EventEmitter { if (typeof callback === "function") { if (!this._handle) { this.once("close", function close() { - callback(ERR_SERVER_NOT_RUNNING()); + callback($ERR_SERVER_NOT_RUNNING()); }); } else { this.once("close", callback); diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts index 9db2708f43..8cd6e67421 100644 --- a/src/js/node/readline.ts +++ b/src/js/node/readline.ts @@ -270,30 +270,6 @@ var NodeError = getNodeErrorByName("Error"); var NodeTypeError = getNodeErrorByName("TypeError"); var NodeRangeError = getNodeErrorByName("RangeError"); -class ERR_INVALID_ARG_VALUE extends NodeTypeError { - constructor(name, value, reason = "not specified") { - super(`The value "${String(value)}" is invalid for argument '${name}'. Reason: ${reason}`, { - code: "ERR_INVALID_ARG_VALUE", - }); - } -} - -class ERR_INVALID_CURSOR_POS extends NodeTypeError { - constructor() { - super("Cannot set cursor row without setting its column", { - code: "ERR_INVALID_CURSOR_POS", - }); - } -} - -class ERR_OUT_OF_RANGE extends NodeRangeError { - constructor(name, range, received) { - super(`The value of "${name}" is out of range. It must be ${range}. Received ${received}`, { - code: "ERR_OUT_OF_RANGE", - }); - } -} - class ERR_USE_AFTER_CLOSE extends NodeError { constructor() { super("This socket has been ended by the other party", { @@ -881,15 +857,15 @@ function cursorTo(stream, x, y, callback) { y = undefined; } - if (NumberIsNaN(x)) throw new ERR_INVALID_ARG_VALUE("x", x); - if (NumberIsNaN(y)) throw new ERR_INVALID_ARG_VALUE("y", y); + if (NumberIsNaN(x)) throw $ERR_INVALID_ARG_VALUE("x", x); + if (NumberIsNaN(y)) throw $ERR_INVALID_ARG_VALUE("y", y); if (stream == null || (typeof x !== "number" && typeof y !== "number")) { if (typeof callback === "function") process.nextTick(callback, null); return true; } - if (typeof x !== "number") throw new ERR_INVALID_CURSOR_POS(); + if (typeof x !== "number") throw $ERR_INVALID_CURSOR_POS(); var data = typeof y !== "number" ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; return stream.write(data, callback); @@ -1286,7 +1262,7 @@ function InterfaceConstructor(input, output, completer, terminal) { if (NumberIsFinite(inputEscapeCodeTimeout)) { this.escapeCodeTimeout = inputEscapeCodeTimeout; } else { - throw new ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); + throw $ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); } } @@ -1299,7 +1275,7 @@ function InterfaceConstructor(input, output, completer, terminal) { } if (completer !== undefined && typeof completer !== "function") { - throw new ERR_INVALID_ARG_VALUE("completer", completer); + throw $ERR_INVALID_ARG_VALUE("completer", completer); } if (history === undefined) { @@ -1313,7 +1289,7 @@ function InterfaceConstructor(input, output, completer, terminal) { } if (typeof historySize !== "number" || NumberIsNaN(historySize) || historySize < 0) { - throw new ERR_INVALID_ARG_VALUE("historySize", historySize); + throw $ERR_INVALID_ARG_VALUE("historySize", historySize); } // Backwards compat; check the isTTY prop of the output stream diff --git a/src/js/node/stream.ts b/src/js/node/stream.ts index 26ba3188a6..458ec766a8 100644 --- a/src/js/node/stream.ts +++ b/src/js/node/stream.ts @@ -31,24 +31,12 @@ const transferToNativeReadable = $newCppFunction("ReadableStream.cpp", "jsFuncti const { kAutoDestroyed } = require("internal/shared"); const { validateBoolean, - validateString, - validateNumber, - validateSignalName, - validateEncoding, - validatePort, validateInteger, validateInt32, - validateUint32, - validateArray, - validateBuffer, validateAbortSignal, validateFunction, - validatePlainFunction, - validateUndefined, } = require("internal/validators"); -const ObjectSetPrototypeOf = Object.setPrototypeOf; - const ProcessNextTick = process.nextTick; const EE = require("node:events").EventEmitter; @@ -70,14 +58,6 @@ $debug("node:stream loaded"); // Node error polyfills //------------------------------------------------------------------------------ -function ERR_INVALID_ARG_TYPE(name, type, value) { - return new Error(`The argument '${name}' is invalid. Received '${value}' for type '${type}'`); -} - -function ERR_INVALID_ARG_VALUE(name, value, reason) { - return new Error(`The value '${value}' is invalid for argument '${name}'. Reason: ${reason}`); -} - // node_modules/readable-stream/lib/ours/primordials.js var require_primordials = __commonJS({ "node_modules/readable-stream/lib/ours/primordials.js"(exports, module) { @@ -516,18 +496,6 @@ var require_errors = __commonJS({ }, TypeError, ); - E( - "ERR_INVALID_ARG_VALUE", - (name, value, reason = "is invalid") => { - let inspected = inspect(value); - if (inspected.length > 128) { - inspected = inspected.slice(0, 128) + "..."; - } - const type = name.includes(".") ? "property" : "argument"; - return `The ${type} '${name}' ${reason}. Received ${inspected}`; - }, - TypeError, - ); E( "ERR_INVALID_RETURN_VALUE", (input, name, value) => { @@ -623,10 +591,8 @@ var require_validators = __commonJS({ } = require_primordials(); var { hideStackFrames, - codes: { ERR_SOCKET_BAD_PORT, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE, ERR_UNKNOWN_SIGNAL }, + codes: { ERR_INVALID_ARG_TYPE }, } = require_errors(); - var { normalizeEncoding } = require_util(); - var { isAsyncFunction, isArrayBufferView } = require_util().types; var signals = {}; function isInt32(value) { return value === (value | 0); @@ -642,7 +608,7 @@ var require_validators = __commonJS({ } if (typeof value === "string") { if (!RegExpPrototypeTest(octalReg, value)) { - throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc); + throw $ERR_INVALID_ARG_VALUE(name, value, modeDesc); } value = NumberParseInt(value, 8); } @@ -656,7 +622,7 @@ var require_validators = __commonJS({ ", ", ); const reason = "must be one of: " + allowed; - throw new ERR_INVALID_ARG_VALUE(name, value, reason); + throw $ERR_INVALID_ARG_VALUE(name, value, reason); } }); var validateObject = hideStackFrames((value, name, options) => { @@ -2018,7 +1984,7 @@ function getHighWaterMark(state, options, duplexKey, isDuplex) { if (hwm != null) { if (!NumberIsInteger(hwm) || hwm < 0) { const name = isDuplex ? `options.${duplexKey}` : "options.highWaterMark"; - throw new ERR_INVALID_ARG_VALUE(name, hwm); + throw $ERR_INVALID_ARG_VALUE(name, hwm); } return MathFloor(hwm); } @@ -2467,7 +2433,7 @@ var require_readable = __commonJS({ } = options; if (encoding !== undefined && !Buffer.isEncoding(encoding)) - throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); + throw $ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); validateBoolean(objectMode, "options.objectMode"); // validateBoolean(native, "options.native"); @@ -5103,10 +5069,10 @@ var require_compose = __commonJS({ continue; } if (n < streams.length - 1 && !isReadable(streams[n])) { - throw new ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be readable"); + throw $ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be readable"); } if (n > 0 && !isWritable(streams[n])) { - throw new ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be writable"); + throw $ERR_INVALID_ARG_VALUE(`streams[${n}]`, orgStreams[n], "must be writable"); } } let ondrain; diff --git a/src/js/node/test.ts b/src/js/node/test.ts new file mode 100644 index 0000000000..01470f3c9c --- /dev/null +++ b/src/js/node/test.ts @@ -0,0 +1,38 @@ +// Hardcoded module "node:test" + +const { throwNotImplemented } = require("internal/shared"); + +function suite() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function test() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function before() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function after() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function beforeEach() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +function afterEach() { + throwNotImplemented("node:test", 5090, "bun:test in available in the interim."); +} + +export default { + suite, + test, + describe: suite, + it: test, + before, + after, + beforeEach, + afterEach, +}; diff --git a/src/js/node/timers.promises.ts b/src/js/node/timers.promises.ts index 68ac1fa3f6..6d011ec78a 100644 --- a/src/js/node/timers.promises.ts +++ b/src/js/node/timers.promises.ts @@ -1,17 +1,10 @@ // Hardcoded module "node:timers/promises" // https://github.com/niksy/isomorphic-timers-promises/blob/master/index.js -const { validateBoolean, validateAbortSignal } = require("internal/validators"); +const { validateBoolean, validateAbortSignal, validateObject } = require("internal/validators"); const symbolAsyncIterator = Symbol.asyncIterator; -class ERR_INVALID_ARG_TYPE extends Error { - constructor(name, expected, actual) { - super(`${name} must be ${expected}, ${typeof actual} given`); - this.code = "ERR_INVALID_ARG_TYPE"; - } -} - class AbortError extends Error { constructor() { super("The operation was aborted"); @@ -19,12 +12,6 @@ class AbortError extends Error { } } -function validateObject(object, name) { - if (object === null || typeof object !== "object") { - throw new ERR_INVALID_ARG_TYPE(name, "Object", object); - } -} - function asyncIterator({ next: nextFunction, return: returnFunction }) { const result = {}; if (typeof nextFunction === "function") { diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 562aad9d15..a9ddd44135 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -2,12 +2,14 @@ const types = require("node:util/types"); /** @type {import('node-inspect-extracted')} */ const utl = require("internal/util/inspect"); -const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); +const { ERR_OUT_OF_RANGE } = require("internal/errors"); const { promisify } = require("internal/promisify"); +const { validateString, validateOneOf } = require("internal/validators"); const internalErrorName = $newZigFunction("node_util_binding.zig", "internalErrorName", 1); const NumberIsSafeInteger = Number.isSafeInteger; +const ObjectKeys = Object.keys; var cjs_exports; @@ -137,15 +139,15 @@ var log = function log() { }; var inherits = function inherits(ctor, superCtor) { if (ctor === undefined || ctor === null) { - throw ERR_INVALID_ARG_TYPE("ctor", "function", ctor); + throw $ERR_INVALID_ARG_TYPE("ctor", "function", ctor); } if (superCtor === undefined || superCtor === null) { - throw ERR_INVALID_ARG_TYPE("superCtor", "function", superCtor); + throw $ERR_INVALID_ARG_TYPE("superCtor", "function", superCtor); } if (superCtor.prototype === undefined) { - throw ERR_INVALID_ARG_TYPE("superCtor.prototype", "object", superCtor.prototype); + throw $ERR_INVALID_ARG_TYPE("superCtor.prototype", "object", superCtor.prototype); } ctor.super_ = superCtor; Object.setPrototypeOf(ctor.prototype, superCtor.prototype); @@ -201,11 +203,7 @@ var toUSVString = input => { }; function styleText(format, text) { - if (typeof text !== "string") { - const e = new Error(`The text argument must be of type string. Received type ${typeof text}`); - e.code = "ERR_INVALID_ARG_TYPE"; - throw e; - } + validateString(text, "text"); if ($isJSArray(format)) { let left = ""; @@ -213,11 +211,7 @@ function styleText(format, text) { for (const key of format) { const formatCodes = inspect.colors[key]; if (formatCodes == null) { - const e = new Error( - `The value "${typeof key === "symbol" ? key.description : key}" is invalid for argument 'format'. Reason: must be one of: ${Object.keys(inspect.colors).join(", ")}`, - ); - e.code = "ERR_INVALID_ARG_VALUE"; - throw e; + validateOneOf(key, "format", ObjectKeys(inspect.colors)); } left += `\u001b[${formatCodes[0]}m`; right = `\u001b[${formatCodes[1]}m${right}`; @@ -229,17 +223,13 @@ function styleText(format, text) { let formatCodes = inspect.colors[format]; if (formatCodes == null) { - const e = new Error( - `The value "${typeof format === "symbol" ? format.description : format}" is invalid for argument 'format'. Reason: must be one of: ${Object.keys(inspect.colors).join(", ")}`, - ); - e.code = "ERR_INVALID_ARG_VALUE"; - throw e; + validateOneOf(format, "format", ObjectKeys(inspect.colors)); } return `\u001b[${formatCodes[0]}m${text}\u001b[${formatCodes[1]}m`; } function getSystemErrorName(err: any) { - if (typeof err !== "number") throw ERR_INVALID_ARG_TYPE("err", "number", err); + if (typeof err !== "number") throw $ERR_INVALID_ARG_TYPE("err", "number", err); if (err >= 0 || !NumberIsSafeInteger(err)) throw ERR_OUT_OF_RANGE("err", "a negative integer", err); return internalErrorName(err); } @@ -256,11 +246,11 @@ function onAbortedCallback(resolveFn: Function) { function aborted(signal: AbortSignal, resource: object) { if (!$isObject(signal) || !(signal instanceof AbortSignal)) { - throw ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); } if (!$isObject(resource)) { - throw ERR_INVALID_ARG_TYPE("resource", "object", resource); + throw $ERR_INVALID_ARG_TYPE("resource", "object", resource); } if (signal.aborted) { diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index 77651013eb..b8bd4feadc 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -24,13 +24,7 @@ const isArrayBufferView = ArrayBufferIsView; const isAnyArrayBuffer = b => b instanceof ArrayBuffer || b instanceof SharedArrayBuffer; const kMaxLength = $requireMap.$get("buffer")?.exports.kMaxLength ?? BufferModule.kMaxLength; -const { - ERR_BROTLI_INVALID_PARAM, - ERR_BUFFER_TOO_LARGE, - ERR_INVALID_ARG_TYPE, - ERR_OUT_OF_RANGE, - ERR_ZLIB_INITIALIZATION_FAILED, -} = require("internal/errors"); +const { ERR_BROTLI_INVALID_PARAM, ERR_BUFFER_TOO_LARGE, ERR_OUT_OF_RANGE } = require("internal/errors"); const { Transform, finished } = require("node:stream"); const owner_symbol = Symbol("owner_symbol"); const { @@ -126,7 +120,7 @@ function zlibBufferSync(engine, buffer) { if (isAnyArrayBuffer(buffer)) { buffer = Buffer.from(buffer); } else { - throw ERR_INVALID_ARG_TYPE("buffer", "string, Buffer, TypedArray, DataView, or ArrayBuffer", buffer); + throw $ERR_INVALID_ARG_TYPE("buffer", "string, Buffer, TypedArray, DataView, or ArrayBuffer", buffer); } } buffer = processChunkSync(engine, buffer, engine._finishFlushFlag); @@ -562,7 +556,7 @@ function Zlib(opts, mode) { if (isAnyArrayBuffer(dictionary)) { dictionary = Buffer.from(dictionary); } else { - throw ERR_INVALID_ARG_TYPE("options.dictionary", "Buffer, TypedArray, DataView, or ArrayBuffer", dictionary); + throw $ERR_INVALID_ARG_TYPE("options.dictionary", "Buffer, TypedArray, DataView, or ArrayBuffer", dictionary); } } } @@ -686,7 +680,7 @@ function Brotli(opts, mode) { const value = opts.params[origKey]; if (typeof value !== "number" && typeof value !== "boolean") { - throw ERR_INVALID_ARG_TYPE("options.params[key]", "number", opts.params[origKey]); + throw $ERR_INVALID_ARG_TYPE("options.params[key]", "number", opts.params[origKey]); } brotliInitParamsArray[key] = value; }); @@ -696,7 +690,7 @@ function Brotli(opts, mode) { this._writeState = new Uint32Array(2); if (!handle.init(brotliInitParamsArray, this._writeState, processCallback)) { - throw ERR_ZLIB_INITIALIZATION_FAILED(); + throw $ERR_ZLIB_INITIALIZATION_FAILED(); } ZlibBase.$apply(this, [opts, mode, handle, brotliDefaultOpts]); diff --git a/src/sys.zig b/src/sys.zig index 2327c3bc7e..fa7d22c4df 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -296,10 +296,12 @@ pub const Error = struct { from_libuv: if (Environment.isWindows) bool else void = if (Environment.isWindows) false else undefined, path: []const u8 = "", syscall: Syscall.Tag = Syscall.Tag.TODO, + dest: []const u8 = "", pub fn clone(this: *const Error, allocator: std.mem.Allocator) !Error { var copy = this.*; copy.path = try allocator.dupe(u8, copy.path); + copy.dest = try allocator.dupe(u8, copy.dest); return copy; } @@ -426,6 +428,10 @@ pub const Error = struct { err.path = bun.String.createUTF8(this.path); } + if (this.dest.len > 0) { + err.dest = bun.String.createUTF8(this.dest); + } + if (this.fd != bun.invalid_fd) { err.fd = this.fd; } @@ -469,7 +475,7 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { var wbuf = bun.WPathBufferPool.get(); defer bun.WPathBufferPool.put(wbuf); const len: windows.DWORD = kernel32.GetCurrentDirectoryW(wbuf.len, wbuf); - if (Result.errnoSys(len, .getcwd)) |err| return err; + if (Result.errnoSysP(len, .getcwd, buf)) |err| return err; return Result{ .result = bun.strings.fromWPath(buf, wbuf[0..len]) }; } @@ -477,7 +483,7 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { return if (rc != null) Result{ .result = rc.?[0..std.mem.len(rc.?) :0] } else - Result.errnoSys(@as(c_int, 0), .getcwd).?; + Result.errnoSysP(@as(c_int, 0), .getcwd, buf).?; } pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { @@ -485,14 +491,14 @@ pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { return sys_uv.fchmod(fd, mode); } - return Maybe(void).errnoSys(C.fchmod(fd.cast(), mode), .fchmod) orelse + return Maybe(void).errnoSysFd(C.fchmod(fd.cast(), mode), .fchmod, fd) orelse Maybe(void).success; } pub fn fchmodat(fd: bun.FileDescriptor, path: [:0]const u8, mode: bun.Mode, flags: i32) Maybe(void) { if (comptime Environment.isWindows) @compileError("Use fchmod instead"); - return Maybe(void).errnoSys(C.fchmodat(fd.cast(), path.ptr, mode, flags), .fchmodat) orelse + return Maybe(void).errnoSysFd(C.fchmodat(fd.cast(), path.ptr, mode, flags), .fchmodat, fd) orelse Maybe(void).success; } @@ -505,19 +511,21 @@ pub fn chmod(path: [:0]const u8, mode: bun.Mode) Maybe(void) { Maybe(void).success; } -pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { +pub fn chdirOSPath(path: bun.stringZ, destination: if (Environment.isPosix) bun.stringZ else bun.string) Maybe(void) { if (comptime Environment.isPosix) { const rc = syscall.chdir(destination); - return Maybe(void).errnoSys(rc, .chdir) orelse Maybe(void).success; + return Maybe(void).errnoSysPD(rc, .chdir, path, destination) orelse Maybe(void).success; } if (comptime Environment.isWindows) { - if (kernel32.SetCurrentDirectory(destination) == windows.FALSE) { - log("SetCurrentDirectory({}) = {d}", .{ bun.fmt.utf16(destination), kernel32.GetLastError() }); - return Maybe(void).errnoSys(0, .chdir) orelse Maybe(void).success; + const wbuf = bun.WPathBufferPool.get(); + defer bun.WPathBufferPool.put(wbuf); + if (kernel32.SetCurrentDirectory(bun.strings.toWDirPath(wbuf, destination)) == windows.FALSE) { + log("SetCurrentDirectory({s}) = {d}", .{ destination, kernel32.GetLastError() }); + return Maybe(void).errnoSysPD(0, .chdir, path, destination) orelse Maybe(void).success; } - log("SetCurrentDirectory({}) = {d}", .{ bun.fmt.utf16(destination), 0 }); + log("SetCurrentDirectory({s}) = {d}", .{ destination, 0 }); return Maybe(void).success; } @@ -525,12 +533,16 @@ pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { @compileError("Not implemented yet"); } -pub fn chdir(destination: anytype) Maybe(void) { +pub fn chdir(path: anytype, destination: anytype) Maybe(void) { const Type = @TypeOf(destination); if (comptime Environment.isPosix) { if (comptime Type == []u8 or Type == []const u8) { return chdirOSPath( + &(std.posix.toPosixPath(path) catch return .{ .err = .{ + .errno = @intFromEnum(bun.C.SystemErrno.EINVAL), + .syscall = .chdir, + } }), &(std.posix.toPosixPath(destination) catch return .{ .err = .{ .errno = @intFromEnum(bun.C.SystemErrno.EINVAL), .syscall = .chdir, @@ -538,25 +550,23 @@ pub fn chdir(destination: anytype) Maybe(void) { ); } - return chdirOSPath(destination); + return chdirOSPath(path, destination); } if (comptime Environment.isWindows) { if (comptime Type == *[*:0]u16) { if (kernel32.SetCurrentDirectory(destination) != 0) { - return Maybe(void).errnoSys(0, .chdir) orelse Maybe(void).success; + return Maybe(void).errnoSysPD(0, .chdir, path, destination) orelse Maybe(void).success; } return Maybe(void).success; } if (comptime Type == bun.OSPathSliceZ or Type == [:0]u16) { - return chdirOSPath(@as(bun.OSPathSliceZ, destination)); + return chdirOSPath(path, @as(bun.OSPathSliceZ, destination)); } - const wbuf = bun.WPathBufferPool.get(); - defer bun.WPathBufferPool.put(wbuf); - return chdirOSPath(bun.strings.toWDirPath(wbuf, destination)); + return chdirOSPath(path, destination); } return Maybe(void).todo(); @@ -590,7 +600,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { if (comptime Environment.allow_assert) log("stat({s}) = {d}", .{ bun.asByteSlice(path), rc }); - if (Maybe(bun.Stat).errnoSys(rc, .stat)) |err| return err; + if (Maybe(bun.Stat).errnoSysP(rc, .stat, path)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } } @@ -600,7 +610,7 @@ pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { return sys_uv.lstat(path); } else { var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSys(C.lstat(path, &stat_), .lstat)) |err| return err; + if (Maybe(bun.Stat).errnoSysP(C.lstat(path, &stat_), .lstat, path)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } } @@ -621,7 +631,7 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { if (comptime Environment.allow_assert) log("fstat({}) = {d}", .{ fd, rc }); - if (Maybe(bun.Stat).errnoSys(rc, .fstat)) |err| return err; + if (Maybe(bun.Stat).errnoSysFd(rc, .fstat, fd)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } @@ -674,7 +684,7 @@ pub fn fstatat(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) { }; } var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSys(syscall.fstatat(fd.int(), path, &stat_, 0), .fstatat)) |err| { + if (Maybe(bun.Stat).errnoSysFP(syscall.fstatat(fd.int(), path, &stat_, 0), .fstatat, fd, path)) |err| { log("fstatat({}, {s}) = {s}", .{ fd, path, @tagName(err.getErrno()) }); return err; } @@ -758,7 +768,7 @@ const fnctl_int = if (Environment.isLinux) usize else c_int; pub fn fcntl(fd: bun.FileDescriptor, cmd: i32, arg: fnctl_int) Maybe(fnctl_int) { while (true) { const result = fcntl_symbol(fd.cast(), cmd, arg); - if (Maybe(fnctl_int).errnoSys(result, .fcntl)) |err| { + if (Maybe(fnctl_int).errnoSysFd(result, .fcntl, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1278,7 +1288,7 @@ pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flag if (comptime Environment.allow_assert) log("openat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(file_path, 0), rc }); - return Maybe(bun.FileDescriptor).errnoSys(rc, .open) orelse .{ .result = bun.toFD(rc) }; + return Maybe(bun.FileDescriptor).errnoSysFP(rc, .open, dirfd, file_path) orelse .{ .result = bun.toFD(rc) }; } else if (comptime Environment.isWindows) { return openatWindowsT(bun.OSPathChar, dirfd, file_path, flags); } @@ -1620,7 +1630,7 @@ pub fn pread(fd: bun.FileDescriptor, buf: []u8, offset: i64) Maybe(usize) { const ioffset = @as(i64, @bitCast(offset)); // the OS treats this as unsigned while (true) { const rc = pread_sym(fd.cast(), buf.ptr, adjusted_len, ioffset); - if (Maybe(usize).errnoSys(rc, .pread)) |err| { + if (Maybe(usize).errnoSysFd(rc, .pread, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1732,7 +1742,7 @@ pub fn recv(fd: bun.FileDescriptor, buf: []u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { const rc = syscall.@"recvfrom$NOCANCEL"(fd.cast(), buf.ptr, adjusted_len, flag, null, null); - if (Maybe(usize).errnoSys(rc, .recv)) |err| { + if (Maybe(usize).errnoSysFd(rc, .recv, fd)) |err| { log("recv({}, {d}) = {s} {}", .{ fd, adjusted_len, err.err.name(), debug_timer }); return err; } @@ -1763,7 +1773,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { const rc = syscall.@"sendto$NOCANCEL"(fd.cast(), buf.ptr, buf.len, flag, null, 0); - if (Maybe(usize).errnoSys(rc, .send)) |err| { + if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; } @@ -1775,7 +1785,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { while (true) { const rc = linux.sendto(fd.cast(), buf.ptr, buf.len, flag, null, 0); - if (Maybe(usize).errnoSys(rc, .send)) |err| { + if (Maybe(usize).errnoSysFd(rc, .send, fd)) |err| { if (err.getErrno() == .INTR) continue; syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; @@ -1790,7 +1800,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { pub fn lseek(fd: bun.FileDescriptor, offset: i64, whence: usize) Maybe(usize) { while (true) { const rc = syscall.lseek(fd.cast(), offset, whence); - if (Maybe(usize).errnoSys(rc, .lseek)) |err| { + if (Maybe(usize).errnoSysFd(rc, .lseek, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1807,7 +1817,7 @@ pub fn readlink(in: [:0]const u8, buf: []u8) Maybe([:0]u8) { while (true) { const rc = syscall.readlink(in, buf.ptr, buf.len); - if (Maybe([:0]u8).errnoSys(rc, .readlink)) |err| { + if (Maybe([:0]u8).errnoSysP(rc, .readlink, in)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1820,7 +1830,7 @@ pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe([:0 while (true) { const rc = syscall.readlinkat(fd.cast(), in, buf.ptr, buf.len); - if (Maybe([:0]u8).errnoSys(rc, .readlink)) |err| { + if (Maybe([:0]u8).errnoSysFP(rc, .readlink, fd, in)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -1832,14 +1842,14 @@ pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe([:0 pub fn ftruncate(fd: bun.FileDescriptor, size: isize) Maybe(void) { if (comptime Environment.isWindows) { if (kernel32.SetFileValidData(fd.cast(), size) == 0) { - return Maybe(void).errnoSys(0, .ftruncate) orelse Maybe(void).success; + return Maybe(void).errnoSysFd(0, .ftruncate, fd) orelse Maybe(void).success; } return Maybe(void).success; } return while (true) { - if (Maybe(void).errnoSys(syscall.ftruncate(fd.cast(), size), .ftruncate)) |err| { + if (Maybe(void).errnoSysFd(syscall.ftruncate(fd.cast(), size), .ftruncate, fd)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2013,7 +2023,7 @@ pub fn renameat(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.Fi pub fn chown(path: [:0]const u8, uid: posix.uid_t, gid: posix.gid_t) Maybe(void) { while (true) { - if (Maybe(void).errnoSys(C.chown(path, uid, gid), .chown)) |err| { + if (Maybe(void).errnoSysP(C.chown(path, uid, gid), .chown, path)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2023,7 +2033,7 @@ pub fn chown(path: [:0]const u8, uid: posix.uid_t, gid: posix.gid_t) Maybe(void) pub fn symlink(target: [:0]const u8, dest: [:0]const u8) Maybe(void) { while (true) { - if (Maybe(void).errnoSys(syscall.symlink(target, dest), .symlink)) |err| { + if (Maybe(void).errnoSysPD(syscall.symlink(target, dest), .symlink, target, dest)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2184,7 +2194,7 @@ pub fn unlink(from: [:0]const u8) Maybe(void) { } while (true) { - if (Maybe(void).errnoSys(syscall.unlink(from), .unlink)) |err| { + if (Maybe(void).errnoSysP(syscall.unlink(from), .unlink, from)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2213,7 +2223,7 @@ pub fn unlinkatWithFlags(dirfd: bun.FileDescriptor, to: anytype, flags: c_uint) } while (true) { - if (Maybe(void).errnoSys(syscall.unlinkat(dirfd.cast(), to, flags), .unlink)) |err| { + if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, flags), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); @@ -2231,7 +2241,7 @@ pub fn unlinkat(dirfd: bun.FileDescriptor, to: anytype) Maybe(void) { return unlinkatWithFlags(dirfd, to, 0); } while (true) { - if (Maybe(void).errnoSys(syscall.unlinkat(dirfd.cast(), to, 0), .unlink)) |err| { + if (Maybe(void).errnoSysFP(syscall.unlinkat(dirfd.cast(), to, 0), .unlink, dirfd, to)) |err| { if (err.getErrno() == .INTR) continue; if (comptime Environment.allow_assert) log("unlinkat({}, {s}) = {d}", .{ dirfd, bun.sliceTo(to, 0), @intFromEnum(err.getErrno()) }); @@ -2351,7 +2361,7 @@ pub fn setCloseOnExec(fd: bun.FileDescriptor) Maybe(void) { pub fn setsockopt(fd: bun.FileDescriptor, level: c_int, optname: u32, value: i32) Maybe(i32) { while (true) { const rc = syscall.setsockopt(fd.cast(), level, optname, &value, @sizeOf(i32)); - if (Maybe(i32).errnoSys(rc, .setsockopt)) |err| { + if (Maybe(i32).errnoSysFd(rc, .setsockopt, fd)) |err| { if (err.getErrno() == .INTR) continue; log("setsockopt() = {d} {s}", .{ err.err.errno, err.err.name() }); return err; @@ -2497,12 +2507,12 @@ pub fn setPipeCapacityOnLinux(fd: bun.FileDescriptor, capacity: usize) Maybe(usi // We don't use glibc here // It didn't work. Always returned 0. const pipe_len = std.os.linux.fcntl(fd.cast(), F_GETPIPE_SZ, 0); - if (Maybe(usize).errnoSys(pipe_len, .fcntl)) |err| return err; + if (Maybe(usize).errnoSysFd(pipe_len, .fcntl, fd)) |err| return err; if (pipe_len == 0) return Maybe(usize){ .result = 0 }; if (pipe_len >= capacity) return Maybe(usize){ .result = pipe_len }; const new_pipe_len = std.os.linux.fcntl(fd.cast(), F_SETPIPE_SZ, capacity); - if (Maybe(usize).errnoSys(new_pipe_len, .fcntl)) |err| return err; + if (Maybe(usize).errnoSysFd(new_pipe_len, .fcntl, fd)) |err| return err; return Maybe(usize){ .result = new_pipe_len }; } @@ -2892,7 +2902,7 @@ pub fn setFileOffset(fd: bun.FileDescriptor, offset: usize) Maybe(void) { windows.FILE_BEGIN, ); if (rc == windows.FALSE) { - return Maybe(void).errnoSys(0, .lseek) orelse Maybe(void).success; + return Maybe(void).errnoSysFd(0, .lseek, fd) orelse Maybe(void).success; } return Maybe(void).success; } @@ -2903,7 +2913,7 @@ pub fn setFileOffsetToEndWindows(fd: bun.FileDescriptor) Maybe(usize) { var new_ptr: std.os.windows.LARGE_INTEGER = undefined; const rc = kernel32.SetFilePointerEx(fd.cast(), 0, &new_ptr, windows.FILE_END); if (rc == windows.FALSE) { - return Maybe(usize).errnoSys(0, .lseek) orelse Maybe(usize){ .result = 0 }; + return Maybe(usize).errnoSysFd(0, .lseek, fd) orelse Maybe(usize){ .result = 0 }; } return Maybe(usize){ .result = @intCast(new_ptr) }; } @@ -2922,10 +2932,7 @@ pub fn pipe() Maybe([2]bun.FileDescriptor) { var fds: [2]i32 = undefined; const rc = syscall.pipe(&fds); - if (Maybe([2]bun.FileDescriptor).errnoSys( - rc, - .pipe, - )) |err| { + if (Maybe([2]bun.FileDescriptor).errnoSys(rc, .pipe)) |err| { return err; } log("pipe() = [{d}, {d}]", .{ fds[0], fds[1] }); diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index d4c3610527..3949d409ea 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -73,7 +73,7 @@ describe.todoIf(isFlaky && isWindows)("bundler", () => { import {rmSync} from 'fs'; // Verify we're not just importing from the filesystem rmSync("./worker.ts", {force: true}); - + console.log("Hello, world!"); new Worker("./worker"); `, diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 9b53bb733c..8453a87dde 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun"; +import { spawn, stderr } from "bun"; import { beforeEach, expect, it } from "bun:test"; import { copyFileSync, cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isDebug, tmpdirSync, waitForFileToExist } from "harness"; @@ -450,7 +450,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { @@ -530,7 +530,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { diff --git a/test/harness.ts b/test/harness.ts index d83a0372cc..5fe4cf1a18 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1389,9 +1389,9 @@ Object.defineProperty(globalThis, "gc", { configurable: true, }); -export function waitForFileToExist(path: string, interval: number) { +export function waitForFileToExist(path: string, interval_ms: number) { while (!fs.existsSync(path)) { - sleepSync(interval); + sleepSync(interval_ms); } } diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index e3735148f3..693ce9e808 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -220,7 +220,8 @@ it("should reject on connection error, calling both connectError() and rejecting expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); + expect(error.code).toBe("ECONNREFUSED"); expect(error.message).toBe("Failed to connect"); }, data() { @@ -246,7 +247,8 @@ it("should reject on connection error, calling both connectError() and rejecting () => done(new Error("Promise should reject instead")), err => { expect(err).toBeDefined(); - expect(err.name).toBe("ECONNREFUSED"); + expect(err.name).toBe("Error"); + expect(err.code).toBe("ECONNREFUSED"); expect(err.message).toBe("Failed to connect"); done(); @@ -293,7 +295,7 @@ it("should handle connection error", done => { expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); expect(error.message).toBe("Failed to connect"); expect((error as any).code).toBe("ECONNREFUSED"); done(); @@ -595,6 +597,7 @@ it("should not call drain before handshake", async () => { }); it("upgradeTLS handles errors", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); @@ -699,6 +702,7 @@ it("upgradeTLS handles errors", async () => { }); it("should be able to upgrade to TLS", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index fc014b9faf..d211fd4c19 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -533,7 +533,7 @@ describe("inline snapshots", () => { (\r ${v("", bad, '`"12"`')})\r ; - expect("13").toMatchInlineSnapshot(${v("", bad, '`"13"`')}); expect("14").toMatchInlineSnapshot(${v("", bad, '`"14"`')}); expect("15").toMatchInlineSnapshot(${v("", bad, '`"15"`')}); + expect("13").toMatchInlineSnapshot(${v("", bad, '`"13"`')}); expect("14").toMatchInlineSnapshot(${v("", bad, '`"14"`')}); expect("15").toMatchInlineSnapshot(${v("", bad, '`"15"`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v(",", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index dd32267d49..3b0c3a6061 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -113,7 +113,8 @@ test("throwing inside an error suppresses the error and continues printing prope const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith(`ENOENT: No such file or directory + expect(stderr.toString().trim()).toStartWith(`error: No such file or directory + code: "ENOENT", path: "this-file-path-is-bad", syscall: "open", errno: -2, diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index a6a949433d..eff7103964 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -2,7 +2,7 @@ exports[`error.cause 1`] = ` "1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); 5 | const err2 = new Error("error 2", { cause: err }); @@ -11,7 +11,7 @@ error: error 2 at [dir]/inspect-error.test.js:5:16 1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); ^ @@ -24,7 +24,7 @@ exports[`Error 1`] = ` " 9 | .replaceAll("//", "/"), 10 | ).toMatchSnapshot(); 11 | }); -12 | +12 | 13 | test("Error", () => { 14 | const err = new Error("my message"); ^ @@ -65,7 +65,7 @@ exports[`Error inside minified file (color) 1`] = ` 23 | arguments);c=b;c.s=1;return c.v=g}catch(h){throw g=b,g.s=2,g.v=h,h;}}}; 24 | exports.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var f=C({},a.props),d=a.key,e=a.ref,g=a._owner;if(null!=b){void 0!==b.ref&&(e=b.ref,g=K.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var h=a.type.defaultProps;for(k in b)J.call(b,k)&&!L.hasOwnProperty(k)&&(f[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)f.children=c;else if(1 { readdirSync(import.meta.path); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); @@ -1126,7 +1127,8 @@ it("readdirSync throws when given a path that doesn't exist", () => { } catch (exception: any) { // the correct error to return in this case is actually ENOENT (which we do on windows), // but on posix we return ENOTDIR - expect(exception.name).toMatch(/ENOTDIR|ENOENT/); + expect(exception.name).toBe("Error"); + expect(exception.code).toMatch(/ENOTDIR|ENOENT/); } }); @@ -1135,7 +1137,8 @@ it("readdirSync throws when given a file path with trailing slash", () => { readdirSync(import.meta.path + "/"); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 70034749ed..0572567901 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -285,7 +285,8 @@ describe("net.createServer listen", () => { expect(err).not.toBeNull(); expect(err!.message).toBe("Failed to connect"); - expect(err!.name).toBe("ECONNREFUSED"); + expect(err!.name).toBe("Error"); + expect(err!.code).toBe("ECONNREFUSED"); server.close(); done(); diff --git a/test/js/node/process/call-constructor.test.js b/test/js/node/process/call-constructor.test.js new file mode 100644 index 0000000000..7522966572 --- /dev/null +++ b/test/js/node/process/call-constructor.test.js @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; +import process from "process"; + +test("the constructor of process can be called", () => { + let obj = process.constructor.call({ ...process }); + expect(Object.getPrototypeOf(obj)).toEqual(Object.getPrototypeOf(process)); +}); + +test("#14346", () => { + process.__proto__.constructor.call({}); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 7055550847..965105f56b 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2,7 +2,7 @@ import { spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; -import { basename, join, resolve } from "path"; +import path, { basename, join, resolve } from "path"; import { familySync } from "detect-libc"; expect.extend({ @@ -236,12 +236,16 @@ it("process.uptime()", () => { }); it("process.umask()", () => { - let notNumbers = [265n, "string", true, false, null, {}, [], () => {}, Symbol("symbol"), BigInt(1)]; - for (let notNumber of notNumbers) { - expect(() => { - process.umask(notNumber); - }).toThrow('The "mask" argument must be of type number'); - } + expect(() => process.umask(265n)).toThrow('The "mask" argument must be of type number. Received type bigint (265n)'); + expect(() => process.umask("string")).toThrow(`The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received "string"`); // prettier-ignore + expect(() => process.umask(true)).toThrow('The "mask" argument must be of type number. Received type boolean (true)'); + expect(() => process.umask(false)).toThrow('The "mask" argument must be of type number. Received type boolean (false)'); // prettier-ignore + expect(() => process.umask(null)).toThrow('The "mask" argument must be of type number. Received null'); + expect(() => process.umask({})).toThrow('The "mask" argument must be of type number. Received an instance of Object'); + expect(() => process.umask([])).toThrow('The "mask" argument must be of type number. Received an instance of Array'); + expect(() => process.umask(() => {})).toThrow('The "mask" argument must be of type number. Received function '); + expect(() => process.umask(Symbol("symbol"))).toThrow('The "mask" argument must be of type number. Received type symbol (Symbol(symbol))'); // prettier-ignore + expect(() => process.umask(BigInt(1))).toThrow('The "mask" argument must be of type number. Received type bigint (1n)'); // prettier-ignore let rangeErrors = [NaN, -1.4, Infinity, -Infinity, -1, 1.3, 4294967296]; for (let rangeError of rangeErrors) { @@ -310,20 +314,6 @@ it("process.config", () => { }); }); -it("process.emitWarning", () => { - process.emitWarning("-- Testing process.emitWarning --"); - var called = 0; - process.on("warning", err => { - called++; - expect(err.message).toBe("-- Testing process.on('warning') --"); - }); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); - expect(process.off("warning")).toBe(process); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); -}); - it("process.execArgv", () => { expect(process.execArgv instanceof Array).toBe(true); }); @@ -342,11 +332,21 @@ it("process.argv in testing", () => { describe("process.exitCode", () => { it("validates int", () => { - expect(() => (process.exitCode = "potato")).toThrow(`exitCode must be an integer`); - expect(() => (process.exitCode = 1.2)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = NaN)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = Infinity)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = -Infinity)).toThrow("exitCode must be an integer"); + expect(() => (process.exitCode = "potato")).toThrow( + `The "code" argument must be of type number. Received type string ("potato")`, + ); + expect(() => (process.exitCode = 1.2)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received 1.2`, + ); + expect(() => (process.exitCode = NaN)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received NaN`, + ); + expect(() => (process.exitCode = Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received Infinity`, + ); + expect(() => (process.exitCode = -Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received -Infinity`, + ); }); it("works with implicit process.exit", () => { @@ -458,13 +458,13 @@ describe("process.cpuUsage", () => { user: -1, system: 100, }), - ).toThrow("The 'user' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.user' is invalid. Received -1"); expect(() => process.cpuUsage({ user: 100, system: -1, }), - ).toThrow("The 'system' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.system' is invalid. Received -1"); }); // Skipped on Windows because it seems UV returns { user: 15000, system: 0 } constantly @@ -684,13 +684,7 @@ it("dlopen accepts file: URLs", () => { }); it("process.constrainedMemory()", () => { - if (process.platform === "linux") { - // On Linux, it returns 0 if the kernel doesn't support it - expect(process.constrainedMemory() >= 0).toBe(true); - } else { - // On unsupported platforms, it returns undefined - expect(process.constrainedMemory()).toBeUndefined(); - } + expect(process.constrainedMemory() >= 0).toBe(true); }); it("process.report", () => { diff --git a/test/js/node/readline/readline.node.test.ts b/test/js/node/readline/readline.node.test.ts index caa38dcfa5..fecce0f34d 100644 --- a/test/js/node/readline/readline.node.test.ts +++ b/test/js/node/readline/readline.node.test.ts @@ -306,15 +306,15 @@ describe("readline.cursorTo()", () => { // Verify that cursorTo() throws if x or y is NaN. assert.throws(() => { readline.cursorTo(writable, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); assert.throws(() => { readline.cursorTo(writable, 1, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); assert.throws(() => { readline.cursorTo(writable, NaN, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); }); }); diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 6b5d1079ff..40d0639a00 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -132,11 +132,9 @@ if (process.argv.length === 2 && const options = { encoding: 'utf8', stdio: 'inherit' }; const result = spawnSync(process.execPath, args, options); if (result.signal) { - process.kill(0, result.signal); + process.kill(process.pid, result.signal); } else { - // Ensure we don't call the "exit" callbacks, as that will cause the - // test to fail when it may have passed in the child process. - process.kill(process.pid, result.status); + process.exit(result.status); } } } @@ -900,6 +898,7 @@ function invalidArgTypeHelper(input) { let inspected = inspect(input, { colors: false }); if (inspected.length > 28) { inspected = `${inspected.slice(inspected, 0, 25)}...`; } + if (inspected.startsWith("'") && inspected.endsWith("'")) inspected = `"${inspected.slice(1, inspected.length - 1)}"`; // BUN: util.inspect uses ' but bun uses " for strings return ` Received type ${typeof input} (${inspected})`; } @@ -1218,5 +1217,3 @@ module.exports = new Proxy(common, { return obj[prop]; }, }); - - diff --git a/test/js/node/test/parallel/test-child-process-stdio.js b/test/js/node/test/parallel/test-child-process-stdio.js new file mode 100644 index 0000000000..15c2770aa2 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// Test stdio piping. +{ + const child = spawn(...common.pwdCommand, { stdio: ['pipe'] }); + assert.notStrictEqual(child.stdout, null); + assert.notStrictEqual(child.stderr, null); +} + +// Test stdio ignoring. +{ + const child = spawn(...common.pwdCommand, { stdio: 'ignore' }); + assert.strictEqual(child.stdout, null); + assert.strictEqual(child.stderr, null); +} + +// Asset options invariance. +{ + const options = { stdio: 'ignore' }; + spawn(...common.pwdCommand, options); + assert.deepStrictEqual(options, { stdio: 'ignore' }); +} + +// Test stdout buffering. +{ + let output = ''; + const child = spawn(...common.pwdCommand); + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', function(s) { + output += s; + }); + + child.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); + })); + + child.on('close', common.mustCall(function() { + assert.strictEqual(output.length > 1, true); + assert.strictEqual(output[output.length - 1], '\n'); + })); +} + +// Assert only one IPC pipe allowed. +assert.throws( + () => { + spawn( + ...common.pwdCommand, + { stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'ipc'] } + ); + }, + { code: 'ERR_IPC_ONE_PIPE', name: 'Error' } +); diff --git a/test/js/node/test/parallel/test-console-tty-colors.js b/test/js/node/test/parallel/test-console-tty-colors.js index 969fb53a23..63ff42935b 100644 --- a/test/js/node/test/parallel/test-console-tty-colors.js +++ b/test/js/node/test/parallel/test-console-tty-colors.js @@ -60,7 +60,21 @@ check(false, false, false); write: common.mustNotCall() }); - [0, 'true', null, {}, [], () => {}].forEach((colorMode) => { + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: 'true' + }); + }, + { + message: `The argument 'colorMode' must be one of: 'auto', true, false. Received "true"`, + code: 'ERR_INVALID_ARG_VALUE' + } + ); + + [0, null, {}, [], () => {}].forEach((colorMode) => { const received = util.inspect(colorMode); assert.throws( () => { diff --git a/test/js/node/test/parallel/test-process-assert.js b/test/js/node/test/parallel/test-process-assert.js new file mode 100644 index 0000000000..f740d3d70c --- /dev/null +++ b/test/js/node/test/parallel/test-process-assert.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.strictEqual(process.assert(1, 'error'), undefined); +assert.throws(() => { + process.assert(undefined, 'errorMessage'); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'errorMessage' +}); +assert.throws(() => { + process.assert(false); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'assertion error' +}); diff --git a/test/js/node/test/parallel/test-process-available-memory.js b/test/js/node/test/parallel/test-process-available-memory.js new file mode 100644 index 0000000000..67de5b5e0b --- /dev/null +++ b/test/js/node/test/parallel/test-process-available-memory.js @@ -0,0 +1,5 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const availableMemory = process.availableMemory(); +assert(typeof availableMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js new file mode 100644 index 0000000000..6e9d764be9 --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +common.skipIfWorker(); + +// Test that 'exit' is emitted if 'beforeExit' throws. + +process.on('exit', common.mustCall(() => { + process.exitCode = 0; +})); +process.on('beforeExit', common.mustCall(() => { + throw new Error(); +})); diff --git a/test/js/node/test/parallel/test-process-beforeexit.js b/test/js/node/test/parallel/test-process-beforeexit.js new file mode 100644 index 0000000000..e04b756cad --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +process.once('beforeExit', common.mustCall(tryImmediate)); + +function tryImmediate() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryTimer)); + })); +} + +function tryTimer() { + setTimeout(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryListen)); + }), 1); +} + +function tryListen() { + net.createServer() + .listen(0) + .on('listening', common.mustCall(function() { + this.close(); + process.once('beforeExit', common.mustCall(tryRepeatedTimer)); + })); +} + +// Test that a function invoked from the beforeExit handler can use a timer +// to keep the event loop open, which can use another timer to keep the event +// loop open, etc. +// +// After N times, call function `tryNextTick` to test behaviors of the +// `process.nextTick`. +function tryRepeatedTimer() { + const N = 5; + let n = 0; + const repeatedTimer = common.mustCall(function() { + if (++n < N) + setTimeout(repeatedTimer, 1); + else // n == N + process.once('beforeExit', common.mustCall(tryNextTickSetImmediate)); + }, N); + setTimeout(repeatedTimer, 1); +} + +// Test if the callback of `process.nextTick` can be invoked. +function tryNextTickSetImmediate() { + process.nextTick(common.mustCall(function() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryNextTick)); + })); + })); +} + +// Test that `process.nextTick` won't keep the event loop running by itself. +function tryNextTick() { + process.nextTick(common.mustCall(function() { + process.once('beforeExit', common.mustNotCall()); + })); +} diff --git a/test/js/node/test/parallel/test-process-binding-util.js b/test/js/node/test/parallel/test-process-binding-util.js new file mode 100644 index 0000000000..a834676e05 --- /dev/null +++ b/test/js/node/test/parallel/test-process-binding-util.js @@ -0,0 +1,58 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const util = require('util'); + +const utilBinding = process.binding('util'); +assert.deepStrictEqual( + Object.keys(utilBinding).sort(), + [ + 'isAnyArrayBuffer', + 'isArgumentsObject', + 'isArrayBuffer', + 'isArrayBufferView', + 'isAsyncFunction', + 'isBigInt64Array', + 'isBigIntObject', + 'isBigUint64Array', + 'isBooleanObject', + 'isBoxedPrimitive', + 'isCryptoKey', + 'isDataView', + 'isDate', + 'isEventTarget', + 'isExternal', + 'isFloat16Array', + 'isFloat32Array', + 'isFloat64Array', + 'isGeneratorFunction', + 'isGeneratorObject', + 'isInt16Array', + 'isInt32Array', + 'isInt8Array', + 'isKeyObject', + 'isMap', + 'isMapIterator', + 'isModuleNamespaceObject', + 'isNativeError', + 'isNumberObject', + 'isPromise', + 'isProxy', + 'isRegExp', + 'isSet', + 'isSetIterator', + 'isSharedArrayBuffer', + 'isStringObject', + 'isSymbolObject', + 'isTypedArray', + 'isUint16Array', + 'isUint32Array', + 'isUint8Array', + 'isUint8ClampedArray', + 'isWeakMap', + 'isWeakSet', + ]); + +for (const k of Object.keys(utilBinding)) { + assert.strictEqual(utilBinding[k], util.types[k]); +} diff --git a/test/js/node/test/parallel/test-process-chdir-errormessage.js b/test/js/node/test/parallel/test-process-chdir-errormessage.js new file mode 100644 index 0000000000..16cdf4aa1d --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir-errormessage.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); +const assert = require('assert'); + +assert.throws( + () => { + process.chdir('does-not-exist'); + }, + { + name: 'Error', + code: 'ENOENT', + // message: /ENOENT: No such file or directory, chdir .+ -> 'does-not-exist'/, + path: process.cwd(), + syscall: 'chdir', + dest: 'does-not-exist' + } +); diff --git a/test/js/node/test/parallel/test-process-chdir.js b/test/js/node/test/parallel/test-process-chdir.js new file mode 100644 index 0000000000..ee59df853b --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const tmpdir = require('../common/tmpdir'); + +process.chdir('..'); +assert.notStrictEqual(process.cwd(), __dirname); +process.chdir(__dirname); +assert.strictEqual(process.cwd(), __dirname); + +let dirName; +if (process.versions.icu) { + // ICU is available, use characters that could possibly be decomposed + dirName = 'weird \uc3a4\uc3ab\uc3af characters \u00e1\u00e2\u00e3'; +} else { + // ICU is unavailable, use characters that can't be decomposed + dirName = 'weird \ud83d\udc04 characters \ud83d\udc05'; +} +const dir = tmpdir.resolve(dirName); + +// Make sure that the tmp directory is clean +tmpdir.refresh(); + +fs.mkdirSync(dir); +process.chdir(dir); +assert.strictEqual(process.cwd().normalize(), dir.normalize()); + +process.chdir('..'); +assert.strictEqual(process.cwd().normalize(), + path.resolve(tmpdir.path).normalize()); + +const err = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "directory" argument must be of type string/ +}; +assert.throws(function() { process.chdir({}); }, err); +assert.throws(function() { process.chdir(); }, err); diff --git a/test/js/node/test/parallel/test-process-config.js b/test/js/node/test/parallel/test-process-config.js new file mode 100644 index 0000000000..20ebc36a99 --- /dev/null +++ b/test/js/node/test/parallel/test-process-config.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); + +// Checks that the internal process.config is equivalent to the config.gypi file +// created when we run configure. + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +// Check for existence of `process.config`. +assert(Object.hasOwn(process, 'config')); + +// Ensure that `process.config` is an Object. +assert.strictEqual(Object(process.config), process.config); + +// Ensure that you can't change config values +assert.throws(() => { process.config.variables = 42; }, TypeError); + +const configPath = path.resolve(__dirname, '..', '..', 'config.gypi'); + +if (!fs.existsSync(configPath)) { + common.skip('config.gypi does not exist.'); +} + +let config = fs.readFileSync(configPath, 'utf8'); + +// Clean up comment at the first line. +config = config.split('\n').slice(1).join('\n'); +config = config.replace(/"/g, '\\"'); +config = config.replace(/'/g, '"'); +config = JSON.parse(config, (key, value) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; +}); + +try { + assert.deepStrictEqual(config, process.config); +} catch (e) { + // If the assert fails, it only shows 3 lines. We need all the output to + // compare. + console.log('config:', config); + console.log('process.config:', process.config); + + throw e; +} diff --git a/test/js/node/test/parallel/test-process-constrained-memory.js b/test/js/node/test/parallel/test-process-constrained-memory.js new file mode 100644 index 0000000000..03f99b166f --- /dev/null +++ b/test/js/node/test/parallel/test-process-constrained-memory.js @@ -0,0 +1,6 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const constrainedMemory = process.constrainedMemory(); +assert.strictEqual(typeof constrainedMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-cpuUsage.js b/test/js/node/test/parallel/test-process-cpuUsage.js new file mode 100644 index 0000000000..f1580d5f09 --- /dev/null +++ b/test/js/node/test/parallel/test-process-cpuUsage.js @@ -0,0 +1,118 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const result = process.cpuUsage(); + +// Validate the result of calling with no previous value argument. +validateResult(result); + +// Validate the result of calling with a previous value argument. +validateResult(process.cpuUsage(result)); + +// Ensure the results are >= the previous. +let thisUsage; +let lastUsage = process.cpuUsage(); +for (let i = 0; i < 10; i++) { + thisUsage = process.cpuUsage(); + validateResult(thisUsage); + assert(thisUsage.user >= lastUsage.user); + assert(thisUsage.system >= lastUsage.system); + lastUsage = thisUsage; +} + +// Ensure that the diffs are >= 0. +let startUsage; +let diffUsage; +for (let i = 0; i < 10; i++) { + startUsage = process.cpuUsage(); + diffUsage = process.cpuUsage(startUsage); + validateResult(startUsage); + validateResult(diffUsage); + assert(diffUsage.user >= 0); + assert(diffUsage.system >= 0); +} + +// Ensure that an invalid shape for the previous value argument throws an error. +assert.throws( + () => process.cpuUsage(1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue" argument must be of type object. ' + + 'Received type number (1)' + } +); + +// Check invalid types. +[ + {}, + { user: 'a' }, + { user: null, system: 'c' }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.user" property must be of type number.' + + common.invalidArgTypeHelper(value.user) + } + ); +}); + +[ + { user: 3, system: 'b' }, + { user: 3, system: null }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.system" property must be of type number.' + + common.invalidArgTypeHelper(value.system) + } + ); +}); + +// Check invalid values. +[ + { user: -1, system: 2 }, + { user: Number.POSITIVE_INFINITY, system: 4 }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.user' is invalid. " + + `Received ${value.user}`, + } + ); +}); + +[ + { user: 3, system: -2 }, + { user: 5, system: Number.NEGATIVE_INFINITY }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.system' is invalid. " + + `Received ${value.system}`, + } + ); +}); + +// Ensure that the return value is the expected shape. +function validateResult(result) { + assert.notStrictEqual(result, null); + + assert(Number.isFinite(result.user)); + assert(Number.isFinite(result.system)); + + assert(result.user >= 0); + assert(result.system >= 0); +} diff --git a/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js new file mode 100644 index 0000000000..cc93e01abd --- /dev/null +++ b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js @@ -0,0 +1,47 @@ +'use strict'; + +// This is a regression test for some scenarios in which node would pass +// unsanitized user input to a printf-like formatting function when dlopen +// fails, potentially crashing the process. + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const fs = require('fs'); + +// This error message should not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, 'foo-%s.node'); +}, ({ name, code, message }) => { + assert.strictEqual(name, 'Error'); + assert.strictEqual(code, 'ERR_DLOPEN_FAILED'); + if (!common.isAIX && !common.isIBMi) { + assert.match(message, /foo-%s\.node/); + } + return true; +}); + +const notBindingDir = 'test/addons/not-a-binding'; +const notBindingPath = `${notBindingDir}/build/Release/binding.node`; +const strangeBindingPath = `${tmpdir.path}/binding-%s.node`; +// Ensure that the addon directory exists, but skip the remainder of the test if +// the addon has not been compiled. +// fs.accessSync(notBindingDir); +// try { +// fs.copyFileSync(notBindingPath, strangeBindingPath); +// } catch (err) { +// if (err.code !== 'ENOENT') throw err; +// common.skip(`addon not found: ${notBindingPath}`); +// } + +// This error message should also not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, strangeBindingPath); +}, { + name: 'Error', + code: 'ERR_DLOPEN_FAILED', + message: /binding-%s\.node/ +}); diff --git a/test/js/node/test/parallel/test-process-emitwarning.js b/test/js/node/test/parallel/test-process-emitwarning.js new file mode 100644 index 0000000000..e1c7473f8a --- /dev/null +++ b/test/js/node/test/parallel/test-process-emitwarning.js @@ -0,0 +1,81 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const testMsg = 'A Warning'; +const testCode = 'CODE001'; +const testDetail = 'Some detail'; +const testType = 'CustomWarning'; + +process.on('warning', common.mustCall((warning) => { + assert(warning); + assert.match(warning.name, /^(?:Warning|CustomWarning)/); + assert.strictEqual(warning.message, testMsg); + if (warning.code) assert.strictEqual(warning.code, testCode); + if (warning.detail) assert.strictEqual(warning.detail, testDetail); +}, 15)); + +class CustomWarning extends Error { + constructor() { + super(); + this.name = testType; + this.message = testMsg; + this.code = testCode; + Error.captureStackTrace(this, CustomWarning); + } +} + +[ + [testMsg], + [testMsg, testType], + [testMsg, CustomWarning], + [testMsg, testType, CustomWarning], + [testMsg, testType, testCode], + [testMsg, { type: testType }], + [testMsg, { type: testType, code: testCode }], + [testMsg, { type: testType, code: testCode, detail: testDetail }], + [new CustomWarning()], + // Detail will be ignored for the following. No errors thrown + [testMsg, { type: testType, code: testCode, detail: true }], + [testMsg, { type: testType, code: testCode, detail: [] }], + [testMsg, { type: testType, code: testCode, detail: null }], + [testMsg, { type: testType, code: testCode, detail: 1 }], +].forEach((args) => { + process.emitWarning(...args); +}); + +const warningNoToString = new CustomWarning(); +warningNoToString.toString = null; +process.emitWarning(warningNoToString); + +const warningThrowToString = new CustomWarning(); +warningThrowToString.toString = function() { + throw new Error('invalid toString'); +}; +process.emitWarning(warningThrowToString); + +// TypeError is thrown on invalid input +[ + [1], + [{}], + [true], + [[]], + ['', '', {}], + ['', 1], + ['', '', 1], + ['', true], + ['', '', true], + ['', []], + ['', '', []], + [], + [undefined, 'foo', 'bar'], + [undefined], +].forEach((args) => { + assert.throws( + () => process.emitWarning(...args), + { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' } + ); +}); diff --git a/test/js/node/test/parallel/test-process-euid-egid.js b/test/js/node/test/parallel/test-process-euid-egid.js new file mode 100644 index 0000000000..06854ba3f5 --- /dev/null +++ b/test/js/node/test/parallel/test-process-euid-egid.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.geteuid, undefined); + assert.strictEqual(process.getegid, undefined); + assert.strictEqual(process.seteuid, undefined); + assert.strictEqual(process.setegid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.seteuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type number or string. ' + + 'Received an instance of Object' +}); + +assert.throws(() => { + process.seteuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// IBMi does not support below operations. +if (common.isIBMi) + return; + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getegid(); + process.geteuid(); + + assert.throws(() => { + process.setegid('nobody'); + }, /(?:EPERM: .+|Group identifier does not exist: nobody)$/); + + assert.throws(() => { + process.seteuid('nobody'); + }, /(?:EPERM: .+|User identifier does not exist: nobody)$/); + + return; +} + +// If we are running as super user... +const oldgid = process.getegid(); +try { + process.setegid('nobody'); +} catch (err) { + if (err.message !== 'Group identifier does not exist: nobody') { + throw err; + } else { + process.setegid('nogroup'); + } +} +const newgid = process.getegid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.geteuid(); +process.seteuid('nobody'); +const newuid = process.geteuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-exception-capture-errors.js b/test/js/node/test/parallel/test-process-exception-capture-errors.js new file mode 100644 index 0000000000..8eb825267c --- /dev/null +++ b/test/js/node/test/parallel/test-process-exception-capture-errors.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(42), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fn" argument must be of type function or null. ' + + 'Received type number (42)' + } +); + +process.setUncaughtExceptionCaptureCallback(common.mustNotCall()); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()), + { + code: 'ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + name: 'Error', + message: /setupUncaughtExceptionCapture.*called while a capture callback/ + } +); diff --git a/test/js/node/test/parallel/test-process-exit-code-validation.js b/test/js/node/test/parallel/test-process-exit-code-validation.js new file mode 100644 index 0000000000..59934fa31d --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-code-validation.js @@ -0,0 +1,145 @@ +'use strict'; + +require('../common'); + +const invalids = [ + { + code: '', + expected: 1, + pattern: 'Received type string \\(""\\)$', + }, + { + code: '1 one', + expected: 1, + pattern: 'Received type string \\("1 one"\\)$', + }, + { + code: 'two', + expected: 1, + pattern: 'Received type string \\("two"\\)$', + }, + { + code: {}, + expected: 1, + pattern: 'Received an instance of Object$', + }, + { + code: [], + expected: 1, + pattern: 'Received an instance of Array$', + }, + { + code: true, + expected: 1, + pattern: 'Received type boolean \\(true\\)$', + }, + { + code: false, + expected: 1, + pattern: 'Received type boolean \\(false\\)$', + }, + { + code: 2n, + expected: 1, + pattern: 'Received type bigint \\(2n\\)$', + }, + { + code: 2.1, + expected: 1, + pattern: 'Received 2.1$', + }, + { + code: Infinity, + expected: 1, + pattern: 'Received Infinity$', + }, + { + code: NaN, + expected: 1, + pattern: 'Received NaN$', + }, +]; +const valids = [ + { + code: 1, + expected: 1, + }, + { + code: '2', + expected: 2, + }, + { + code: undefined, + expected: 0, + }, + { + code: null, + expected: 0, + }, + { + code: 0, + expected: 0, + }, + { + code: '0', + expected: 0, + }, +]; +const args = [...invalids, ...valids]; + +if (process.argv[2] === undefined) { + const { spawnSync } = require('node:child_process'); + const { inspect, debuglog } = require('node:util'); + const { throws, strictEqual } = require('node:assert'); + + const debug = debuglog('test'); + const node = process.execPath; + const test = (index, useProcessExitCode) => { + const { status: code } = spawnSync(node, [ + __filename, + index, + useProcessExitCode, + ]); + console.log(`actual: ${code}, ${args[index].expected} ${index} ${!!useProcessExitCode} ${args[index].code}`); + debug(`actual: ${code}, ${inspect(args[index])} ${!!useProcessExitCode}`); + strictEqual( + code, + args[index].expected, + `actual: ${code}, ${inspect(args[index])}` + ); + }; + + // Check process.exitCode + for (const arg of invalids) { + debug(`invaild code: ${inspect(arg.code)}`); + throws(() => (process.exitCode = arg.code), new RegExp(arg.pattern)); + } + for (const arg of valids) { + debug(`vaild code: ${inspect(arg.code)}`); + process.exitCode = arg.code; + } + + throws(() => { + delete process.exitCode; + // }, /Cannot delete property 'exitCode' of #/); + }, /Unable to delete property./); + process.exitCode = 0; + + // Check process.exit([code]) + for (const index of args.keys()) { + test(index); + test(index, true); + } +} else { + const index = parseInt(process.argv[2]); + const useProcessExitCode = process.argv[3] !== 'undefined'; + if (Number.isNaN(index)) { + return process.exit(100); + } + + if (useProcessExitCode) { + process.exitCode = args[index].code; + } else { + process.exit(args[index].code); + } +} diff --git a/test/js/node/test/parallel/test-process-hrtime.js b/test/js/node/test/parallel/test-process-hrtime.js new file mode 100644 index 0000000000..34ef514aac --- /dev/null +++ b/test/js/node/test/parallel/test-process-hrtime.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// The default behavior, return an Array "tuple" of numbers +const tuple = process.hrtime(); + +// Validate the default behavior +validateTuple(tuple); + +// Validate that passing an existing tuple returns another valid tuple +validateTuple(process.hrtime(tuple)); + +// Test that only an Array may be passed to process.hrtime() +assert.throws(() => { + process.hrtime(1); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "time" argument must be an instance of Array. Received type ' + + 'number (1)' +}); +assert.throws(() => { + process.hrtime([]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 0' +}); +assert.throws(() => { + process.hrtime([1]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 1' +}); +assert.throws(() => { + process.hrtime([1, 2, 3]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 3' +}); + +function validateTuple(tuple) { + assert(Array.isArray(tuple)); + assert.strictEqual(tuple.length, 2); + assert(Number.isInteger(tuple[0])); + assert(Number.isInteger(tuple[1])); +} + +const diff = process.hrtime([0, 1e9 - 1]); +assert(diff[1] >= 0); // https://github.com/nodejs/node/issues/4751 diff --git a/test/js/node/test/parallel/test-process-kill-pid.js b/test/js/node/test/parallel/test-process-kill-pid.js new file mode 100644 index 0000000000..1fa1d6c2ab --- /dev/null +++ b/test/js/node/test/parallel/test-process-kill-pid.js @@ -0,0 +1,116 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const assert = require('assert'); + +// Test variants of pid +// +// null: TypeError +// undefined: TypeError +// +// 'SIGTERM': TypeError +// +// String(process.pid): TypeError +// +// Nan, Infinity, -Infinity: TypeError +// +// 0, String(0): our group process +// +// process.pid, String(process.pid): ourself + +assert.throws(() => process.kill('SIGTERM'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number. Received type string ("SIGTERM")' +}); + +[null, undefined, NaN, Infinity, -Infinity].forEach((val) => { + assert.throws(() => process.kill(val), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number.' + + common.invalidArgTypeHelper(val) + }); +}); + +// Test that kill throws an error for unknown signal names +assert.throws(() => process.kill(0, 'test'), { + code: 'ERR_UNKNOWN_SIGNAL', + name: 'TypeError', + message: 'Unknown signal: test' +}); + +// Test that kill throws an error for invalid signal numbers +assert.throws(() => process.kill(0, 987), { + code: 'EINVAL', + name: 'SystemError', + message: 'kill() failed: EINVAL: Invalid argument' +}); + +// Test kill argument processing in valid cases. +// +// Monkey patch _kill so that we don't actually send any signals, particularly +// that we don't kill our process group, or try to actually send ANY signals on +// windows, which doesn't support them. +function kill(tryPid, trySig, expectPid, expectSig) { + let getPid; + let getSig; + const origKill = process._kill; + process._kill = function(pid, sig) { + getPid = pid; + getSig = sig; + + // un-monkey patch process._kill + process._kill = origKill; + }; + + process.kill(tryPid, trySig); + + assert.strictEqual(getPid.toString(), expectPid.toString()); + assert.strictEqual(getSig, expectSig); +} + +// Note that SIGHUP and SIGTERM map to 1 and 15 respectively, even on Windows +// (for Windows, libuv maps 1 and 15 to the correct behavior). + +kill(0, 'SIGHUP', 0, 1); +kill(0, undefined, 0, 15); +kill('0', 'SIGHUP', 0, 1); +kill('0', undefined, 0, 15); + +// Confirm that numeric signal arguments are supported + +kill(0, 1, 0, 1); +kill(0, 15, 0, 15); + +// Negative numbers are meaningful on unix +kill(-1, 'SIGHUP', -1, 1); +kill(-1, undefined, -1, 15); +kill('-1', 'SIGHUP', -1, 1); +kill('-1', undefined, -1, 15); + +kill(process.pid, 'SIGHUP', process.pid, 1); +kill(process.pid, undefined, process.pid, 15); +kill(String(process.pid), 'SIGHUP', process.pid, 1); +kill(String(process.pid), undefined, process.pid, 15); diff --git a/test/js/node/test/parallel/test-process-no-deprecation.js b/test/js/node/test/parallel/test-process-no-deprecation.js new file mode 100644 index 0000000000..bcda99de25 --- /dev/null +++ b/test/js/node/test/parallel/test-process-no-deprecation.js @@ -0,0 +1,32 @@ +'use strict'; +// Flags: --no-warnings + +// The --no-warnings flag only suppresses writing the warning to stderr, not the +// emission of the corresponding event. This test file can be run without it. + +const common = require('../common'); +process.noDeprecation = true; + +const assert = require('assert'); + +function listener() { + assert.fail('received unexpected warning'); +} + +process.addListener('warning', listener); + +process.emitWarning('Something is deprecated.', 'DeprecationWarning'); + +// The warning would be emitted in the next tick, so continue after that. +process.nextTick(common.mustCall(() => { + // Check that deprecations can be re-enabled. + process.noDeprecation = false; + process.removeListener('warning', listener); + + process.addListener('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'DeprecationWarning'); + assert.strictEqual(warning.message, 'Something else is deprecated.'); + })); + + process.emitWarning('Something else is deprecated.', 'DeprecationWarning'); +})); diff --git a/test/js/node/test/parallel/test-process-really-exit.js b/test/js/node/test/parallel/test-process-really-exit.js new file mode 100644 index 0000000000..8445d220ca --- /dev/null +++ b/test/js/node/test/parallel/test-process-really-exit.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +// Ensure that the reallyExit hook is executed. +// see: https://github.com/nodejs/node/issues/25650 +if (process.argv[2] === 'subprocess') { + process.reallyExit = function() { + console.info('really exited'); + }; + process.exit(); +} else { + const { spawnSync } = require('child_process'); + const out = spawnSync(process.execPath, [__filename, 'subprocess']); + const observed = out.output[1].toString('utf8').trim(); + assert.strictEqual(observed, 'really exited'); +} diff --git a/test/js/node/test/parallel/test-process-release.js b/test/js/node/test/parallel/test-process-release.js new file mode 100644 index 0000000000..98a089a8f9 --- /dev/null +++ b/test/js/node/test/parallel/test-process-release.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const versionParts = process.versions.node.split('.'); + +assert.strictEqual(process.release.name, 'node'); + +// It's expected that future LTS release lines will have additional +// branches in here +if (versionParts[0] === '4' && versionParts[1] >= 2) { + assert.strictEqual(process.release.lts, 'Argon'); +} else if (versionParts[0] === '6' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Boron'); +} else if (versionParts[0] === '8' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Carbon'); +} else if (versionParts[0] === '10' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Dubnium'); +} else if (versionParts[0] === '12' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Erbium'); +} else if (versionParts[0] === '14' && versionParts[1] >= 15) { + assert.strictEqual(process.release.lts, 'Fermium'); +} else if (versionParts[0] === '16' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Gallium'); +} else if (versionParts[0] === '18' && versionParts[1] >= 12) { + assert.strictEqual(process.release.lts, 'Hydrogen'); +} else if (versionParts[0] === '20' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Iron'); +} else { + assert.strictEqual(process.release.lts, undefined); +} diff --git a/test/js/node/test/parallel/test-process-setgroups.js b/test/js/node/test/parallel/test-process-setgroups.js new file mode 100644 index 0000000000..c26b5dbaf1 --- /dev/null +++ b/test/js/node/test/parallel/test-process-setgroups.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.setgroups, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws( + () => { + process.setgroups(); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups" argument must be an instance of Array. ' + + 'Received undefined' + } +); + +assert.throws( + () => { + process.setgroups([1, -1]); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } +); + +[undefined, null, true, {}, [], () => {}].forEach((val) => { + assert.throws( + () => { + process.setgroups([val]); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups[0]" argument must be ' + + 'of type number or string.' + + common.invalidArgTypeHelper(val) + } + ); +}); + +assert.throws(() => { + process.setgroups([1, 'fhqwhgadshgnsdhjsdbkhsdabkfabkveyb']); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'Group identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); diff --git a/test/js/node/test/parallel/test-process-title-cli.js b/test/js/node/test/parallel/test-process-title-cli.js new file mode 100644 index 0000000000..98b3da003f --- /dev/null +++ b/test/js/node/test/parallel/test-process-title-cli.js @@ -0,0 +1,17 @@ +// Flags: --title=foo +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +if (common.isSunOS) + common.skip(`Unsupported platform [${process.platform}]`); + +if (common.isIBMi) + common.skip('Unsupported platform IBMi'); + +const assert = require('assert'); + +// Verifies that the --title=foo command line flag set the process +// title on startup. +assert.strictEqual(process.title, 'foo'); diff --git a/test/js/node/test/parallel/test-process-uid-gid.js b/test/js/node/test/parallel/test-process-uid-gid.js new file mode 100644 index 0000000000..0e8e0e89a0 --- /dev/null +++ b/test/js/node/test/parallel/test-process-uid-gid.js @@ -0,0 +1,100 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); + +if (common.isWindows) { + // uid/gid functions are POSIX only. + assert.strictEqual(process.getuid, undefined); + assert.strictEqual(process.getgid, undefined); + assert.strictEqual(process.setuid, undefined); + assert.strictEqual(process.setgid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.setuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type ' + + 'number or string. Received an instance of Object' +}); + +assert.throws(() => { + process.setuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// Passing -0 shouldn't crash the process +// Refs: https://github.com/nodejs/node/issues/32750 +// And neither should values exceeding 2 ** 31 - 1. +for (const id of [-0, 2 ** 31, 2 ** 32 - 1]) { + for (const fn of [process.setuid, process.setuid, process.setgid, process.setegid]) { + try { fn(id); } catch { + // Continue regardless of error. + } + } +} + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getgid(); + process.getuid(); + + assert.throws( + () => { process.setgid('nobody'); }, + /(?:EPERM: .+|Group identifier does not exist: nobody)$/ + ); + + assert.throws( + () => { process.setuid('nobody'); }, + /(?:EPERM: .+|User identifier does not exist: nobody)$/ + ); + return; +} + +// If we are running as super user... +const oldgid = process.getgid(); +try { + process.setgid('nobody'); +} catch (err) { + if (err.code !== 'ERR_UNKNOWN_CREDENTIAL') { + throw err; + } + process.setgid('nogroup'); +} + +const newgid = process.getgid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.getuid(); +process.setuid('nobody'); +const newuid = process.getuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-umask-mask.js b/test/js/node/test/parallel/test-process-umask-mask.js new file mode 100644 index 0000000000..d599379761 --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask-mask.js @@ -0,0 +1,32 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in +// process.umask() + +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) + common.skip('Setting process.umask is not supported in Workers'); + +let mask; + +if (common.isWindows) { + mask = 0o600; +} else { + mask = 0o664; +} + +const maskToIgnore = 0o10000; + +const old = process.umask(); + +function test(input, output) { + process.umask(input); + assert.strictEqual(process.umask(), output); + + process.umask(old); +} + +test(mask | maskToIgnore, mask); +test((mask | maskToIgnore).toString(8), mask); diff --git a/test/js/node/test/parallel/test-process-umask.js b/test/js/node/test/parallel/test-process-umask.js new file mode 100644 index 0000000000..e90955f394 --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) { + assert.strictEqual(typeof process.umask(), 'number'); + assert.throws(() => { + process.umask('0664'); + }, { code: 'ERR_WORKER_UNSUPPORTED_OPERATION' }); + + common.skip('Setting process.umask is not supported in Workers'); +} + +// Note in Windows one can only set the "user" bits. +let mask; +if (common.isWindows) { + mask = '0600'; +} else { + mask = '0664'; +} + +const old = process.umask(mask); + +assert.strictEqual(process.umask(old), parseInt(mask, 8)); + +// Confirm reading the umask does not modify it. +// 1. If the test fails, this call will succeed, but the mask will be set to 0 +assert.strictEqual(process.umask(), old); +// 2. If the test fails, process.umask() will return 0 +assert.strictEqual(process.umask(), old); + +assert.throws(() => { + process.umask({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', +}); + +['123x', 'abc', '999'].forEach((value) => { + assert.throws(() => { + process.umask(value); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); +}); diff --git a/test/js/node/test/parallel/test-process-warning.js b/test/js/node/test/parallel/test-process-warning.js new file mode 100644 index 0000000000..c1fbbf775f --- /dev/null +++ b/test/js/node/test/parallel/test-process-warning.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const { + hijackStderr, + restoreStderr +} = require('../common/hijackstdio'); +const assert = require('assert'); + +function test1() { + // Output is skipped if the argument to the 'warning' event is + // not an Error object. + hijackStderr(common.mustNotCall('stderr.write must not be called')); + process.emit('warning', 'test'); + setImmediate(test2); +} + +function test2() { + // Output is skipped if it's a deprecation warning and + // process.noDeprecation = true + process.noDeprecation = true; + process.emitWarning('test', 'DeprecationWarning'); + process.noDeprecation = false; + setImmediate(test3); +} + +function test3() { + restoreStderr(); + // Type defaults to warning when the second argument is an object + process.emitWarning('test', {}); + process.once('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'Warning'); + })); + setImmediate(test4); +} + +function test4() { + // process.emitWarning will throw when process.throwDeprecation is true + // and type is `DeprecationWarning`. + process.throwDeprecation = true; + process.once('uncaughtException', (err) => { + assert.match(err.toString(), /^DeprecationWarning: test$/); + }); + try { + process.emitWarning('test', 'DeprecationWarning'); + } catch { + assert.fail('Unreachable'); + } + process.throwDeprecation = false; + setImmediate(test5); +} + +function test5() { + // Setting toString to a non-function should not cause an error + const err = new Error('test'); + err.toString = 1; + process.emitWarning(err); + setImmediate(test6); +} + +function test6() { + process.emitWarning('test', { detail: 'foo' }); + process.on('warning', (warning) => { + assert.strictEqual(warning.detail, 'foo'); + }); +} + +test1(); diff --git a/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js b/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js deleted file mode 100644 index 35b3d9fa30..0000000000 --- a/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const async_hooks = require('async_hooks'); - -// Regression test for https://github.com/nodejs/node/issues/30080: -// An uncaught exception inside a queueMicrotask callback should not lead -// to multiple after() calls for it. - -let µtaskId; -const events = []; - -async_hooks.createHook({ - init(id, type, triggerId, resource) { - if (type === 'Microtask') { - µtaskId = id; - events.push('init'); - } - }, - before(id) { - if (id === µtaskId) events.push('before'); - }, - after(id) { - if (id === µtaskId) events.push('after'); - }, - destroy(id) { - if (id === µtaskId) events.push('destroy'); - } -}).enable(); - -queueMicrotask(() => { throw new Error(); }); - -process.on('uncaughtException', common.mustCall()); -process.on('exit', () => { - assert.deepStrictEqual(events, ['init', 'after', 'before', 'destroy']); -}); diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 21a72ede53..b3414f453b 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -1209,12 +1209,15 @@ describe("fetch() with streaming", () => { expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { if (compression === "br") { - expect((err as Error).name).toBe("BrotliDecompressionError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("BrotliDecompressionError"); } else if (compression === "deflate-libdeflate") { // Since the compressed data is different, the error ends up different. - expect((err as Error).name).toBe("ShortRead"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ShortRead"); } else { - expect((err as Error).name).toBe("ZlibError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ZlibError"); } } } @@ -1306,7 +1309,8 @@ describe("fetch() with streaming", () => { gcTick(false); expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { - expect((err as Error).name).toBe("ConnectionClosed"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ConnectionClosed"); } } }); From 5e9e188edac6cb2b020b9bd547aac3fcd190ee7b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 6 Jan 2025 14:37:39 -0800 Subject: [PATCH 44/56] update workspaces.md --- docs/install/workspaces.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/install/workspaces.md b/docs/install/workspaces.md index 64d2445132..fb25a0a7db 100644 --- a/docs/install/workspaces.md +++ b/docs/install/workspaces.md @@ -53,6 +53,16 @@ Each workspace has it's own `package.json`. When referencing other packages in t } ``` +`bun install` will install dependencies for all workspaces in the monorepo, de-duplicating packages if possible. If you only want to install dependencies for specific workspaces, you can use the `--filter` flag. + +```bash +# Install dependencies for all workspaces starting with `pkg-` except for `pkg-c` +$ bun install --filter "pkg-*" --filter "!pkg-c" + +# Paths can also be used. This is equivalent to the command above. +$ bun install --filter "./packages/pkg-*" --filter "!pkg-c" # or --filter "!./packages/pkg-c" +``` + Workspaces have a couple major benefits. - **Code can be split into logical parts.** If one package relies on another, you can simply add it as a dependency in `package.json`. If package `b` depends on `a`, `bun install` will install your local `packages/a` directory into `node_modules` instead of downloading it from the npm registry. From 24a2c9b50c91a9e8cfa800f09ac3abf3c15453c9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 6 Jan 2025 15:52:56 -0800 Subject: [PATCH 45/56] Redo S3 API before we release it --- docs/api/s3.md | 278 +++++++++++++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 113 deletions(-) diff --git a/docs/api/s3.md b/docs/api/s3.md index 448baa0571..5db2210953 100644 --- a/docs/api/s3.md +++ b/docs/api/s3.md @@ -3,14 +3,11 @@ Production servers often read, upload, and write files to S3-compatible object s Bun provides fast, native bindings for interacting with S3-compatible object storage services. Bun's S3 API is designed to be simple and feel similar to fetch's `Response` and `Blob` APIs (like Bun's local filesystem APIs). ```ts -import { s3, write, S3 } from "bun"; +import { s3, write, S3Client } from "bun"; -const metadata = await s3("123.json", { - accessKeyId: "your-access-key", - secretAccessKey: "your-secret-key", - bucket: "my-bucket", - // endpoint: "https://s3.us-east-1.amazonaws.com", -}); +// Bun.s3 reads environment variables for credentials +// file() returns a lazy reference to a file on S3 +const metadata = s3.file("123.json"); // Download from S3 as JSON const data = await metadata.json(); @@ -23,6 +20,9 @@ const url = metadata.presign({ acl: "public-read", expiresIn: 60 * 60 * 24, // 1 day }); + +// Delete the file +await metadata.delete(); ``` S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. You can use Bun's S3 API with S3-compatible storage services like: @@ -38,28 +38,45 @@ S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) i There are several ways to interact with Bun's S3 API. -### Using `Bun.s3()` +### `Bun.S3Client` & `Bun.s3` -The `s3()` helper function is used to create one-off `S3File` instances for a single file. +`Bun.s3` is equivalent to `new Bun.S3Client()`, relying on environment variables for credentials. + +To explicitly set credentials, you can pass them to the `Bun.S3Client` constructor. ```ts -import { s3 } from "bun"; +import { S3Client } from "bun"; -// Using the s3() helper -const s3file = s3("my-file.txt", { +const client = new S3Client({ accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", - // endpoint: "https://s3.us-east-1.amazonaws.com", // optional + // sessionToken: "..." + // acl: "public-read", + // endpoint: "https://s3.us-east-1.amazonaws.com", // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 // endpoint: "https://.digitaloceanspaces.com", // DigitalOcean Spaces // endpoint: "http://localhost:9000", // MinIO }); + +// Bun.s3 is a global singleton that is equivalent to `new Bun.S3Client()` +Bun.s3 = client; ``` -### Reading Files +### Working with S3 Files -You can read files from S3 using similar methods to Bun's file system APIs: +The **`file`** method in `S3Client` returns a **lazy reference to a file on S3**. + +```ts +// A lazy reference to a file on S3 +const s3file: S3File = client.file("123.json"); +``` + +Like `Bun.file(path)`, the `S3Client`'s `file` method is synchronous. It does zero network requests until you call a method that depends on a network request. + +### Reading files from S3 + +If you've used the `fetch` API, you're familiar with the `Response` and `Blob` APIs. `S3File` extends `Blob`, so you can use the same methods on it as you would for a `Response` or a `Blob`. ```ts // Read an S3File as text @@ -81,14 +98,28 @@ for await (const chunk of stream) { } ``` -## Writing Files +#### Memory optimization -Writing to S3 is just as simple: +Methods like `text()`, `json()`, `bytes()`, or `arrayBuffer()` avoid duplicating the string or bytes in memory when possible. + +If the text happens to be ASCII, Bun directly transfers the string to JavaScriptCore (the engine) without transcoding and without duplicating the string in memory. When you use `.bytes()` or `.arrayBuffer()`, it will also avoid duplicating the bytes in memory. + +These helper methods not only simplify the API, they also make it faster. + +### Writing & uploading files to S3 + +Writing to S3 is just as simple. ```ts // Write a string (replacing the file) await s3file.write("Hello World!"); +// Write a Buffer (replacing the file) +await s3file.write(Buffer.from("Hello World!")); + +// Write a Response (replacing the file) +await s3file.write(new Response("Hello World!")); + // Write with content type await s3file.write(JSON.stringify({ name: "John", age: 30 }), { type: "application/json", @@ -119,7 +150,7 @@ const writer = s3file.writer({ queueSize: 10, // Upload in 5 MB chunks - partSize: 5 * 1024 * 1024, + partSize: 5, }); for (let i = 0; i < 10; i++) { await writer.write(bigFile); @@ -134,20 +165,11 @@ When your production service needs to let users upload files to your server, it' To facilitate this, you can presign URLs for S3 files. This generates a URL with a signature that allows a user to securely upload that specific file to S3, without exposing your credentials or granting them unnecessary access to your bucket. ```ts +import { s3 } from "bun"; + // Generate a presigned URL that expires in 24 hours (default) -const url = s3file.presign(); - -// Custom expiration time (in seconds) -const url2 = s3file.presign({ expiresIn: 3600 }); // 1 hour - -// Using static method -const url3 = Bun.S3.presign("my-file.txt", { - bucket: "my-bucket", - accessKeyId: "your-access-key", - secretAccessKey: "your-secret-key", - // endpoint: "https://s3.us-east-1.amazonaws.com", - // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 - expiresIn: 3600, +const url = s3.presign("my-file.txt", { + expiresIn: 3600, // 1 hour }); ``` @@ -183,6 +205,12 @@ To set an expiration time for a presigned URL, pass the `expiresIn` option. const url = s3file.presign({ // Seconds expiresIn: 3600, // 1 hour + + // access control list + acl: "public-read", + + // HTTP method + method: "PUT", }); ``` @@ -231,10 +259,10 @@ Response (0 KB) { Bun's S3 implementation works with any S3-compatible storage service. Just specify the appropriate endpoint: ```ts -import { s3 } from "bun"; +import { S3Client } from "bun"; // CloudFlare R2 -const r2file = s3("my-file.txt", { +const r2 = new S3Client({ accessKeyId: "access-key", secretAccessKey: "secret-key", bucket: "my-bucket", @@ -242,7 +270,7 @@ const r2file = s3("my-file.txt", { }); // DigitalOcean Spaces -const spacesFile = s3("my-file.txt", { +const spaces = new S3Client({ accessKeyId: "access-key", secretAccessKey: "secret-key", bucket: "my-bucket", @@ -250,7 +278,7 @@ const spacesFile = s3("my-file.txt", { }); // MinIO -const minioFile = s3("my-file.txt", { +const minio = new S3Client({ accessKeyId: "access-key", secretAccessKey: "secret-key", bucket: "my-bucket", @@ -284,16 +312,16 @@ If the `S3_*` environment variable is not set, Bun will also check for the `AWS_ These environment variables are read from [`.env` files](/docs/runtime/env) or from the process environment at initialization time (`process.env` is not used for this). -These defaults are overriden by the options you pass to `s3(credentials)`, `new Bun.S3(credentials)`, or any of the methods that accept credentials. So if, for example, you use the same credentials for different buckets, you can set the credentials once in your `.env` file and then pass `bucket: "my-bucket"` to the `s3()` helper function without having to specify all the credentials again. +These defaults are overriden by the options you pass to `s3(credentials)`, `new Bun.S3Client(credentials)`, or any of the methods that accept credentials. So if, for example, you use the same credentials for different buckets, you can set the credentials once in your `.env` file and then pass `bucket: "my-bucket"` to the `s3()` helper function without having to specify all the credentials again. -### `S3` Buckets +### `S3Client` objects -Passing around all of these credentials can be cumbersome. To make it easier, you can create a `S3` bucket instance. +When you're not using environment variables or using multiple buckets, you can create a `S3Client` object to explicitly set credentials. ```ts -import { S3 } from "bun"; +import { S3Client } from "bun"; -const bucket = new S3({ +const client = new S3Client({ accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", @@ -303,15 +331,6 @@ const bucket = new S3({ // endpoint: "http://localhost:9000", // MinIO }); -// bucket is a function that creates `S3File` instances (lazy) -const file = bucket("my-file.txt"); - -// Write to S3 -await file.write("Hello World!"); - -// Read from S3 -const text = await file.text(); - // Write using a Response await file.write(new Response("Hello World!")); @@ -322,65 +341,57 @@ const url = file.presign({ }); // Delete the file -await file.unlink(); +await file.delete(); ``` -#### Read a file from an `S3` bucket +### `S3Client.prototype.write` -The `S3` bucket instance is itself a function that creates `S3File` instances. It provides a more convenient API for interacting with S3. +You can call `.write` on the `S3Client` instance to write a file to S3. ```ts -const s3file = bucket("my-file.txt"); -const text = await s3file.text(); -const json = await s3file.json(); -const bytes = await s3file.bytes(); -const arrayBuffer = await s3file.arrayBuffer(); -``` - -#### Write a file to S3 - -To write a file to the bucket, you can use the `write` method. - -```ts -const bucket = new Bun.S3({ +const client = new Bun.S3Client({ accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", endpoint: "https://s3.us-east-1.amazonaws.com", bucket: "my-bucket", }); -await bucket.write("my-file.txt", "Hello World!"); -await bucket.write("my-file.txt", new Response("Hello World!")); +await client.write("my-file.txt", "Hello World!"); +await client.write("my-file.txt", new Response("Hello World!")); + +// equivalent to +// await client.file("my-file.txt").write("Hello World!"); ``` -You can also call `.write` on the `S3File` instance created by the `S3` bucket instance. +### `S3Client.prototype.delete` + +To delete a file from S3, you can use the `delete` method. ```ts -const s3file = bucket("my-file.txt"); -await s3file.write("Hello World!", { - type: "text/plain", -}); -await s3file.write(new Response("Hello World!")); -``` - -#### Delete a file from S3 - -To delete a file from the bucket, you can use the `delete` method. - -```ts -const bucket = new Bun.S3({ +const client = new Bun.S3Client({ accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", }); -await bucket.delete("my-file.txt"); +await client.delete("my-file.txt"); +// equivalent to +// await client.file("my-file.txt").delete(); ``` -You can also use the `unlink` method, which is an alias for `delete`. +### `S3Client.prototype.exists` + +To check if a file exists in S3, you can use the `exists` method. ```ts -// "delete" and "unlink" are aliases of each other. -await bucket.unlink("my-file.txt"); +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +const exists = await client.exists("my-file.txt"); +// equivalent to +// const exists = await client.file("my-file.txt").exists(); ``` ## `S3File` @@ -410,7 +421,18 @@ interface S3File extends Blob { options?: BlobPropertyBag, ): Promise; - readonly size: Promise; + exists(options?: S3Options): Promise; + unlink(options?: S3Options): Promise; + delete(options?: S3Options): Promise; + presign(options?: S3Options): string; + + stat(options?: S3Options): Promise; + /** + * Size is not synchronously available because it requires a network request. + * + * @deprecated Use `stat()` instead. + */ + size: NaN; // ... more omitted for brevity } @@ -428,7 +450,7 @@ Like `Bun.file()`, `S3File` extends [`Blob`](https://developer.mozilla.org/en-US That means using `S3File` instances with `fetch()`, `Response`, and other web APIs that accept `Blob` instances just works. -### Partial reads +### Partial reads with `slice` To read a partial range of a file, you can use the `slice` method. @@ -444,6 +466,17 @@ const text = await partial.text(); Internally, this works by using the HTTP `Range` header to request only the bytes you want. This `slice` method is the same as [`Blob.prototype.slice`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice). +### Deleting files from S3 + +To delete a file from S3, you can use the `delete` method. + +```ts +await s3file.delete(); +// await s3File.unlink(); +``` + +`delete` is the same as `unlink`. + ## Error codes When Bun's S3 API throws an error, it will have a `code` property that matches one of the following values: @@ -457,82 +490,101 @@ When Bun's S3 API throws an error, it will have a `code` property that matches o When the S3 Object Storage service returns an error (that is, not Bun), it will be an `S3Error` instance (an `Error` instance with the name `"S3Error"`). -## `S3` static methods +## `S3Client` static methods -The `S3` class provides several static methods for interacting with S3. +The `S3Client` class provides several static methods for interacting with S3. -### `S3.presign` +### `S3Client.presign` (static) -To generate a presigned URL for an S3 file, you can use the `S3.presign` method. +To generate a presigned URL for an S3 file, you can use the `S3Client.presign` static method. ```ts -import { S3 } from "bun"; +import { S3Client } from "bun"; -const url = S3.presign("my-file.txt", { +const credentials = { accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", - expiresIn: 3600, // endpoint: "https://s3.us-east-1.amazonaws.com", // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 +}; + +const url = S3Client.presign("my-file.txt", { + ...credentials, + expiresIn: 3600, }); ``` -This is the same as `S3File.prototype.presign` and `new S3(credentials).presign`, as a static method on the `S3` class. +This is equivalent to calling `new S3Client(credentials).presign("my-file.txt", { expiresIn: 3600 })`. -### `S3.exists` +### `S3Client.exists` (static) -To check if an S3 file exists, you can use the `S3.exists` method. +To check if an S3 file exists, you can use the `S3Client.exists` static method. ```ts -import { S3 } from "bun"; +import { S3Client } from "bun"; -const exists = await S3.exists("my-file.txt", { +const credentials = { accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", // endpoint: "https://s3.us-east-1.amazonaws.com", -}); +}; + +const exists = await S3Client.exists("my-file.txt", credentials); ``` The same method also works on `S3File` instances. ```ts const s3file = Bun.s3("my-file.txt", { - accessKeyId: "your-access-key", - secretAccessKey: "your-secret-key", - bucket: "my-bucket", + ...credentials, }); const exists = await s3file.exists(); ``` -### `S3.size` +### `S3Client.stat` (static) -To get the size of an S3 file, you can use the `S3.size` method. +To get the size, etag, and other metadata of an S3 file, you can use the `S3Client.stat` static method. ```ts -import { S3 } from "bun"; -const size = await S3.size("my-file.txt", { +import { S3Client } from "bun"; + +const credentials = { accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", // endpoint: "https://s3.us-east-1.amazonaws.com", -}); +}; + +const stat = await S3Client.stat("my-file.txt", credentials); +// { +// size: 1024, +// etag: "1234567890", +// lastModified: new Date(), +// } ``` -### `S3.unlink` +### `S3Client.delete` (static) -To delete an S3 file, you can use the `S3.unlink` method. +To delete an S3 file, you can use the `S3Client.delete` static method. ```ts -import { S3 } from "bun"; +import { S3Client } from "bun"; -await S3.unlink("my-file.txt", { +const credentials = { accessKeyId: "your-access-key", secretAccessKey: "your-secret-key", bucket: "my-bucket", // endpoint: "https://s3.us-east-1.amazonaws.com", -}); +}; + +await S3Client.delete("my-file.txt", credentials); +// equivalent to +// await new S3Client(credentials).delete("my-file.txt"); + +// S3Client.unlink is alias of S3Client.delete +await S3Client.unlink("my-file.txt", credentials); ``` ## s3:// protocol From 7f949bf8dfb80ffabde6ca04596394dc003d017b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 6 Jan 2025 16:03:51 -0800 Subject: [PATCH 46/56] Update s3.md --- docs/api/s3.md | 73 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/docs/api/s3.md b/docs/api/s3.md index 5db2210953..fe98908466 100644 --- a/docs/api/s3.md +++ b/docs/api/s3.md @@ -25,7 +25,7 @@ const url = metadata.presign({ await metadata.delete(); ``` -S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. You can use Bun's S3 API with S3-compatible storage services like: +S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. Bun's S3 API works with S3-compatible storage services like: - AWS S3 - Cloudflare R2 @@ -42,7 +42,7 @@ There are several ways to interact with Bun's S3 API. `Bun.s3` is equivalent to `new Bun.S3Client()`, relying on environment variables for credentials. -To explicitly set credentials, you can pass them to the `Bun.S3Client` constructor. +To explicitly set credentials, pass them to the `Bun.S3Client` constructor. ```ts import { S3Client } from "bun"; @@ -76,7 +76,7 @@ Like `Bun.file(path)`, the `S3Client`'s `file` method is synchronous. It does ze ### Reading files from S3 -If you've used the `fetch` API, you're familiar with the `Response` and `Blob` APIs. `S3File` extends `Blob`, so you can use the same methods on it as you would for a `Response` or a `Blob`. +If you've used the `fetch` API, you're familiar with the `Response` and `Blob` APIs. `S3File` extends `Blob`. The same methods that work on `Blob` also work on `S3File`. ```ts // Read an S3File as text @@ -231,7 +231,7 @@ const url = s3file.presign({ ### `new Response(S3File)` -To quickly redirect users to a presigned URL for an S3 file, you can pass an `S3File` instance to a `Response` object as the body. +To quickly redirect users to a presigned URL for an S3 file, pass an `S3File` instance to a `Response` object as the body. ```ts const response = new Response(s3file); @@ -258,6 +258,43 @@ Response (0 KB) { Bun's S3 implementation works with any S3-compatible storage service. Just specify the appropriate endpoint: +### Using Bun's S3Client with AWS S3 + +AWS S3 is the default. You can also pass a `region` option instead of an `endpoint` option for AWS S3. + +```ts +import { S3Client } from "bun"; + +// AWS S3 +const s3 = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // region: "us-east-1", +}); +``` + +### Using Bun's S3Client with Google Cloud Storage + +To use Bun's S3 client with [Google Cloud Storage](https://cloud.google.com/storage), set `endpoint` to `"https://storage.googleapis.com"` in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +// Google Cloud Storage +const gcs = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://storage.googleapis.com", +}); +``` + +### Using Bun's S3Client with Cloudflare R2 + +To use Bun's S3 client with [Cloudflare R2](https://developers.cloudflare.com/r2/), set `endpoint` to the R2 endpoint in the `S3Client` constructor. The R2 endpoint includes your account ID. + ```ts import { S3Client } from "bun"; @@ -268,20 +305,38 @@ const r2 = new S3Client({ bucket: "my-bucket", endpoint: "https://.r2.cloudflarestorage.com", }); +``` + +### Using Bun's S3Client with DigitalOcean Spaces + +To use Bun's S3 client with [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/), set `endpoint` to the DigitalOcean Spaces endpoint in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; -// DigitalOcean Spaces const spaces = new S3Client({ accessKeyId: "access-key", secretAccessKey: "secret-key", bucket: "my-bucket", + // region: "nyc3", endpoint: "https://.digitaloceanspaces.com", }); +``` + +### Using Bun's S3Client with MinIO + +To use Bun's S3 client with [MinIO](https://min.io/), set `endpoint` to the URL that MinIO is running on in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; -// MinIO const minio = new S3Client({ accessKeyId: "access-key", secretAccessKey: "secret-key", bucket: "my-bucket", + + // Make sure to use the correct endpoint URL + // It might not be localhost in production! endpoint: "http://localhost:9000", }); ``` @@ -346,7 +401,7 @@ await file.delete(); ### `S3Client.prototype.write` -You can call `.write` on the `S3Client` instance to write a file to S3. +To upload or write a file to S3, call `write` on the `S3Client` instance. ```ts const client = new Bun.S3Client({ @@ -364,7 +419,7 @@ await client.write("my-file.txt", new Response("Hello World!")); ### `S3Client.prototype.delete` -To delete a file from S3, you can use the `delete` method. +To delete a file from S3, call `delete` on the `S3Client` instance. ```ts const client = new Bun.S3Client({ @@ -380,7 +435,7 @@ await client.delete("my-file.txt"); ### `S3Client.prototype.exists` -To check if a file exists in S3, you can use the `exists` method. +To check if a file exists in S3, call `exists` on the `S3Client` instance. ```ts const client = new Bun.S3Client({ From f1e8cb8f47b0857171493d262511cfd1d6107276 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 6 Jan 2025 18:28:50 -0800 Subject: [PATCH 47/56] fix $ERR_INVALID_ARG_VALUE (#16194) --- src/bun.js/bindings/ErrorCode.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 1e5d8e720c..bbf1bd6728 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -820,6 +820,13 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject JSValue arg0 = callFrame->argument(1); JSValue arg1 = callFrame->argument(2); JSValue arg2 = callFrame->argument(3); + + // TODO: remove this if; this switch case was added but not all the callsites using bare $ERR_INVALID_ARG_VALUE(msg) were updated + if (callFrame->argumentCount() == 2 && arg0.isString()) { + auto message = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(createError(globalObject, error, message)); + } return JSValue::encode(ERR_INVALID_ARG_TYPE(scope, globalObject, arg0, arg1, arg2)); } From d4ff142f0972544d09d3bb810a758eb4266f1323 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 6 Jan 2025 18:47:13 -0800 Subject: [PATCH 48/56] Update s3.md --- docs/api/s3.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/api/s3.md b/docs/api/s3.md index fe98908466..4c88579a80 100644 --- a/docs/api/s3.md +++ b/docs/api/s3.md @@ -651,6 +651,27 @@ const response = await fetch("s3://my-bucket/my-file.txt"); const file = Bun.file("s3://my-bucket/my-file.txt"); ``` -This is the equivalent of calling `Bun.s3("my-file.txt", { bucket: "my-bucket" })`. +You can additionally pass `s3` options to the `fetch` and `Bun.file` functions. -This `s3://` protocol exists to make it easier to use the same code for local files and S3 files. +```ts +const response = await fetch("s3://my-bucket/my-file.txt", { + s3: { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + }, + headers: { + "x-amz-meta-foo": "bar", + }, +}); +``` + +### UTF-8, UTF-16, and BOM (byte order mark) + +Like `Response` and `Blob`, `S3File` assumes UTF-8 encoding by default. + +When calling one of the `text()` or `json()` methods on an `S3File`: + +- When a UTF-16 byte order mark (BOM) is detected, it will be treated as UTF-16. JavaScriptCore natively supports UTF-16, so it skips the UTF-8 transcoding process (and strips the BOM). This is mostly good, but it does mean if you have invalid surrogate pairs characters in your UTF-16 string, they will be passed through to JavaScriptCore (same as source code). +- When a UTF-8 BOM is detected, it gets stripped before the string is passed to JavaScriptCore and invalid UTF-8 codepoints are replaced with the Unicode replacement character (`\uFFFD`). +- UTF-32 is not supported. From f299ef8c731d7cc95d942c098a808fb6464986a6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 6 Jan 2025 21:55:18 -0500 Subject: [PATCH 49/56] Download correct musl binaries in `bun build --compile` (#16192) --- src/compile_target.zig | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/compile_target.zig b/src/compile_target.zig index 00acaf4fad..82f0731dd3 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -28,6 +28,14 @@ const Libc = enum { /// musl libc musl, + /// npm package name, `@oven-sh/bun-{os}-{arch}` + pub fn npmName(this: Libc) []const u8 { + return switch (this) { + .default => "", + .musl => "-musl", + }; + } + pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { if (self == .musl) { try writer.writeAll("-musl"); @@ -64,21 +72,25 @@ pub fn toNPMRegistryURL(this: *const CompileTarget, buf: []u8) ![]const u8 { pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_url: []const u8) ![]const u8 { return switch (this.os) { inline else => |os| switch (this.arch) { - inline else => |arch| switch (this.baseline) { - // https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz - inline else => |is_baseline| try std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++ - os.npmName() ++ "-" ++ arch.npmName() ++ - (if (is_baseline) "-baseline" else "") ++ - "/-/bun-" ++ - os.npmName() ++ "-" ++ arch.npmName() ++ - (if (is_baseline) "-baseline" else "") ++ - "-" ++ - "{d}.{d}.{d}.tgz", .{ - registry_url, - this.version.major, - this.version.minor, - this.version.patch, - }), + inline else => |arch| switch (this.libc) { + inline else => |libc| switch (this.baseline) { + // https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz + inline else => |is_baseline| try std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + libc.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "/-/bun-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + libc.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "-" ++ + "{d}.{d}.{d}.tgz", .{ + registry_url, + this.version.major, + this.version.minor, + this.version.patch, + }), + }, }, }, }; From 1923509b056ab902e169b6eb11f91a9ed8f96ef9 Mon Sep 17 00:00:00 2001 From: pfg Date: Mon, 6 Jan 2025 18:56:24 -0800 Subject: [PATCH 50/56] handle pnpx & pnpm dlx in package.json scripts (#16187) Co-authored-by: Jarred Sumner --- src/cli/list-of-yarn-commands.zig | 178 ++++++++++++------------------ src/cli/run_command.zig | 16 ++- test/cli/install/bun-run.test.ts | 63 ++++++++--- 3 files changed, 134 insertions(+), 123 deletions(-) diff --git a/src/cli/list-of-yarn-commands.zig b/src/cli/list-of-yarn-commands.zig index 12d0d23a2e..647cf2d29d 100644 --- a/src/cli/list-of-yarn-commands.zig +++ b/src/cli/list-of-yarn-commands.zig @@ -1,109 +1,77 @@ const std = @import("std"); const bun = @import("root").bun; -// yarn v2.3 commands -const yarn_v2 = [_][]const u8{ - "add", - "bin", - "cache", - "config", - "dedupe", - "dlx", - "exec", - "explain", - "info", - "init", - "install", - "link", - "node", - "npm", - "pack", - "patch", - "plugin", - "rebuild", - "remove", - "run", - "set", - "unplug", - "up", - "why", - "workspace", - "workspaces", -}; +pub const all_yarn_commands = bun.ComptimeStringMap(void, .{ + // yarn v2.3 commands + .{"add"}, + .{"bin"}, + .{"cache"}, + .{"config"}, + .{"dedupe"}, + .{"dlx"}, + .{"exec"}, + .{"explain"}, + .{"info"}, + .{"init"}, + .{"install"}, + .{"link"}, + .{"node"}, + .{"npm"}, + .{"pack"}, + .{"patch"}, + .{"plugin"}, + .{"rebuild"}, + .{"remove"}, + .{"run"}, + .{"set"}, + .{"unplug"}, + .{"up"}, + .{"why"}, + .{"workspace"}, + .{"workspaces"}, -// yarn v1 commands -const yarn_v1 = [_][]const u8{ - "access", - "add", - "audit", - "autoclean", - "bin", - "cache", - "check", - "config", - "create", - "exec", - "generate-lock-entry", - "generateLockEntry", - "global", - "help", - "import", - "info", - "init", - "install", - "licenses", - "link", - "list", - "login", - "logout", - "node", - "outdated", - "owner", - "pack", - "policies", - "publish", - "remove", - "run", - "tag", - "team", - "unlink", - "unplug", - "upgrade", - "upgrade-interactive", - "upgradeInteractive", - "version", - "versions", - "why", - "workspace", - "workspaces", -}; - -pub const all_yarn_commands = brk: { - @setEvalBranchQuota(9999); - var array: [yarn_v2.len + yarn_v1.len]u64 = undefined; - var array_i: usize = 0; - for (yarn_v2) |yarn| { - const hash = bun.hash(yarn); - @setEvalBranchQuota(9999); - if (std.mem.indexOfScalar(u64, array[0..array_i], hash) == null) { - @setEvalBranchQuota(9999); - array[array_i] = hash; - array_i += 1; - } - } - - for (yarn_v1) |yarn| { - @setEvalBranchQuota(9999); - - const hash = bun.hash(yarn); - if (std.mem.indexOfScalar(u64, array[0..array_i], hash) == null) { - @setEvalBranchQuota(9999); - - array[array_i] = hash; - array_i += 1; - } - } - - const final = array[0..array_i].*; - break :brk &final; -}; + // yarn v1 commands + .{"access"}, + .{"add"}, + .{"audit"}, + .{"autoclean"}, + .{"bin"}, + .{"cache"}, + .{"check"}, + .{"config"}, + .{"create"}, + .{"exec"}, + .{"generate-lock-entry"}, + .{"generateLockEntry"}, + .{"global"}, + .{"help"}, + .{"import"}, + .{"info"}, + .{"init"}, + .{"install"}, + .{"licenses"}, + .{"link"}, + .{"list"}, + .{"login"}, + .{"logout"}, + .{"node"}, + .{"outdated"}, + .{"owner"}, + .{"pack"}, + .{"policies"}, + .{"publish"}, + .{"remove"}, + .{"run"}, + .{"tag"}, + .{"team"}, + .{"unlink"}, + .{"unplug"}, + .{"upgrade"}, + .{"upgrade-interactive"}, + .{"upgradeInteractive"}, + .{"version"}, + .{"versions"}, + .{"why"}, + .{"workspace"}, + .{"workspaces"}, +}); diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index ab39f134e8..4ae3c93ed1 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -46,7 +46,7 @@ const NpmArgs = struct { pub const package_version: string = "npm_package_version"; }; const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; -const yarn_commands: []const u64 = @import("./list-of-yarn-commands.zig").all_yarn_commands; +const yarn_commands = @import("./list-of-yarn-commands.zig").all_yarn_commands; const ShellCompletions = @import("./shell_completions.zig"); const PosixSpawn = bun.posix.spawn; @@ -179,7 +179,7 @@ pub const RunCommand = struct { } // implicit yarn commands - if (std.mem.indexOfScalar(u64, yarn_commands, bun.hash(yarn_cmd)) == null) { + if (!yarn_commands.has(yarn_cmd)) { try copy_script.appendSlice(BUN_RUN); try copy_script.append(' '); try copy_script.appendSlice(yarn_cmd); @@ -231,6 +231,18 @@ pub const RunCommand = struct { delimiter = 0; continue; } + if (strings.hasPrefixComptime(script[start..], "pnpm dlx ")) { + try copy_script.appendSlice(BUN_BIN_NAME ++ " x "); + entry_i += "pnpm dlx ".len; + delimiter = 0; + continue; + } + if (strings.hasPrefixComptime(script[start..], "pnpx ")) { + try copy_script.appendSlice(BUN_BIN_NAME ++ " x "); + entry_i += "pnpx ".len; + delimiter = 0; + continue; + } } delimiter = 0; diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 3c6416479c..3b363ba90f 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -599,22 +599,53 @@ it("should pass arguments correctly in scripts", async () => { } }); -it("should run with bun instead of npm even with leading spaces", async () => { - const dir = tempDirWithFiles("test", { - "package.json": JSON.stringify({ - workspaces: ["a", "b"], - scripts: { "root_script": " npm run other_script ", "other_script": " echo hi " }, - }), - }); - { - const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "run", "root_script"], - cwd: dir, - env: bunEnv, - }); +const cases = [ + ["yarn run", "run"], + ["yarn add", "passthrough"], + ["yarn audit", "passthrough"], + ["yarn -abcd run", "passthrough"], + ["yarn info", "passthrough"], + ["yarn generate-lock-entry", "passthrough"], + ["yarn", "run"], + ["npm run", "run"], + ["npx", "x"], + ["pnpm run", "run"], + ["pnpm dlx", "x"], + ["pnpx", "x"], +]; +describe("should handle run case", () => { + for (const ccase of cases) { + it(ccase[0], async () => { + const dir = tempDirWithFiles("test", { + "package.json": JSON.stringify({ + scripts: { + "root_script": ` ${ccase[0]} target_script% `, + "target_script%": " echo target_script ", + }, + }), + }); + { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "root_script"], + cwd: dir, + env: bunEnv, + }); - expect(stderr.toString()).toMatch(/\$ bun(-debug)? run other_script \n\$ echo hi \n/); - expect(stdout.toString()).toEndWith("hi\n"); - expect(exitCode).toBe(0); + if (ccase[1] === "run") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? run target_script% \n\$ echo target_script \n/, + ); + expect(stdout.toString()).toEndWith("target_script\n"); + expect(exitCode).toBe(0); + } else if (ccase[1] === "x") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? x target_script% \nerror: unrecognised dependency format: target_script%/, + ); + expect(exitCode).toBe(1); + } else { + expect(stderr.toString()).toStartWith(`$ ${ccase[0]} target_script% \n`); + } + } + }); } }); From cfd05bdfcf172c28d661d73565121f66affbe09a Mon Sep 17 00:00:00 2001 From: Kai Tamkun <13513421+heimskr@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:25:57 -0800 Subject: [PATCH 51/56] Don't chmod UNIX sockets to 700 (#16200) --- packages/bun-usockets/src/bsd.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index 0c2543b161..a452163988 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -843,13 +843,6 @@ static LIBUS_SOCKET_DESCRIPTOR internal_bsd_create_listen_socket_unix(const char return LIBUS_SOCKET_ERROR; } -#ifndef _WIN32 - // 700 permission by default - fchmod(listenFd, S_IRWXU); -#else - _chmod(path, S_IREAD | S_IWRITE | S_IEXEC); -#endif - #ifdef _WIN32 _unlink(path); #else From 65530c91d05e79b98e7e49a0156a54a9babc6e89 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 6 Jan 2025 23:51:09 -0800 Subject: [PATCH 52/56] use native validators for validateObject and validateOneOf more (#16203) --- src/js/internal/util/inspect.js | 6 +-- src/js/node/child_process.ts | 49 +-------------------- src/js/node/stream.ts | 75 +-------------------------------- 3 files changed, 6 insertions(+), 124 deletions(-) diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index f98354aae0..a67c9dd49c 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -145,9 +145,9 @@ const ONLY_ENUMERABLE = 2; * Fast path for {@link extractedSplitNewLines} for ASCII/Latin1 strings. * @returns `value` split on newlines (newline included at end), or `undefined` * if non-ascii UTF8/UTF16. - * + * * Passing this a non-string will cause a panic. - * + * * @type {(value: string) => string[] | undefined} */ const extractedSplitNewLinesFastPathStringsOnly = $newZigFunction( @@ -457,7 +457,7 @@ const extractedSplitNewLines = value => { return extractedSplitNewLinesFastPathStringsOnly(value) || extractedSplitNewLinesSlow(value); } return extractedSplitNewLinesSlow(value); -} +}; const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; const numberRegExp = /^(0|[1-9][0-9]*)$/; diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index d8a12370b4..fee02dcbd1 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -9,6 +9,8 @@ const { validateString, validateAbortSignal, validateArray, + validateObject, + validateOneOf, } = require("internal/validators"); var NetModule; @@ -1630,53 +1632,6 @@ function validateTimeout(timeout) { } } -/** - * @callback validateOneOf - * @template T - * @param {T} value - * @param {string} name - * @param {T[]} oneOf - */ - -/** @type {validateOneOf} */ -const validateOneOf = (value, name, oneOf) => { - // const validateOneOf = hideStackFrames((value, name, oneOf) => { - if (!ArrayPrototypeIncludes.$call(oneOf, value)) { - const allowed = ArrayPrototypeJoin.$call( - ArrayPrototypeMap.$call(oneOf, v => (typeof v === "string" ? `'${v}'` : String(v))), - ", ", - ); - const reason = "must be one of: " + allowed; - throw $ERR_INVALID_ARG_VALUE(name, value, reason); - } -}; - -/** - * @callback validateObject - * @param {*} value - * @param {string} name - * @param {{ - * allowArray?: boolean, - * allowFunction?: boolean, - * nullable?: boolean - * }} [options] - */ - -/** @type {validateObject} */ -const validateObject = (value, name, options = null) => { - // const validateObject = hideStackFrames((value, name, options = null) => { - const allowArray = options?.allowArray ?? false; - const allowFunction = options?.allowFunction ?? false; - const nullable = options?.nullable ?? false; - if ( - (!nullable && value === null) || - (!allowArray && $isJSArray(value)) || - (typeof value !== "object" && (!allowFunction || typeof value !== "function")) - ) { - throw $ERR_INVALID_ARG_TYPE(name, "object", value); - } -}; - function isInt32(value) { return value === (value | 0); } diff --git a/src/js/node/stream.ts b/src/js/node/stream.ts index 458ec766a8..2a7f2b88e7 100644 --- a/src/js/node/stream.ts +++ b/src/js/node/stream.ts @@ -35,6 +35,7 @@ const { validateInt32, validateAbortSignal, validateFunction, + validateObject, } = require("internal/validators"); const ProcessNextTick = process.nextTick; @@ -577,77 +578,6 @@ var require_errors = __commonJS({ }, }); -// node_modules/readable-stream/lib/internal/validators.js -var require_validators = __commonJS({ - "node_modules/readable-stream/lib/internal/validators.js"(exports, module) { - "use strict"; - var { - ArrayPrototypeIncludes, - ArrayPrototypeJoin, - ArrayPrototypeMap, - NumberParseInt, - RegExpPrototypeTest, - String: String2, - } = require_primordials(); - var { - hideStackFrames, - codes: { ERR_INVALID_ARG_TYPE }, - } = require_errors(); - var signals = {}; - function isInt32(value) { - return value === (value | 0); - } - function isUint32(value) { - return value === value >>> 0; - } - var octalReg = /^[0-7]+$/; - var modeDesc = "must be a 32-bit unsigned integer or an octal string"; - function parseFileMode(value, name, def) { - if (typeof value === "undefined") { - value = def; - } - if (typeof value === "string") { - if (!RegExpPrototypeTest(octalReg, value)) { - throw $ERR_INVALID_ARG_VALUE(name, value, modeDesc); - } - value = NumberParseInt(value, 8); - } - validateInt32(value, name, 0, 2 ** 32 - 1); - return value; - } - var validateOneOf = hideStackFrames((value, name, oneOf) => { - if (!ArrayPrototypeIncludes(oneOf, value)) { - const allowed = ArrayPrototypeJoin( - ArrayPrototypeMap(oneOf, v => (typeof v === "string" ? `'${v}'` : String2(v))), - ", ", - ); - const reason = "must be one of: " + allowed; - throw $ERR_INVALID_ARG_VALUE(name, value, reason); - } - }); - var validateObject = hideStackFrames((value, name, options) => { - const useDefaultOptions = options == null; - const allowArray = useDefaultOptions ? false : options.allowArray; - const allowFunction = useDefaultOptions ? false : options.allowFunction; - const nullable = useDefaultOptions ? false : options.nullable; - if ( - (!nullable && value === null) || - (!allowArray && $isJSArray(value)) || - (typeof value !== "object" && (!allowFunction || typeof value !== "function")) - ) { - throw new ERR_INVALID_ARG_TYPE(name, "Object", value); - } - }); - module.exports = { - isInt32, - isUint32, - parseFileMode, - validateObject, - validateOneOf, - }; - }, -}); - // node_modules/readable-stream/lib/internal/streams/utils.js var require_utils = __commonJS({ "node_modules/readable-stream/lib/internal/streams/utils.js"(exports, module) { @@ -941,7 +871,6 @@ var require_end_of_stream = __commonJS({ var { AbortError, codes } = require_errors(); var { ERR_INVALID_ARG_TYPE, ERR_STREAM_PREMATURE_CLOSE } = codes; var { once } = require_util(); - var { validateObject } = require_validators(); var { Promise: Promise2 } = require_primordials(); var { isClosed, @@ -1151,7 +1080,6 @@ var require_operators = __commonJS({ codes: { ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS, ERR_OUT_OF_RANGE }, AbortError, } = require_errors(); - var { validateObject } = require_validators(); var kWeakHandler = require_primordials().Symbol("kWeak"); var { finished } = require_end_of_stream(); var { @@ -2558,7 +2486,6 @@ var require_readable = __commonJS({ ERR_STREAM_UNSHIFT_AFTER_END_EVENT, }, } = require_errors(); - var { validateObject } = require_validators(); var from = require_from(); var nop = () => {}; var { errorOrDestroy } = destroyImpl; From ace459598a7f4edef96e129faeb600778fee3d11 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 6 Jan 2025 23:51:46 -0800 Subject: [PATCH 53/56] update $ERR_INVALID_ARG_VALUE callsites (#16202) --- src/bun.js/bindings/ErrorCode.cpp | 7 ------- src/js/bun/sql.ts | 20 ++++++++++++++++---- src/js/internal/validators.ts | 10 +++++++--- src/js/node/http2.ts | 13 ++++++------- src/js/node/stream.ts | 2 +- src/js/node/v8.ts | 2 +- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index bbf1bd6728..1e5d8e720c 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -820,13 +820,6 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject JSValue arg0 = callFrame->argument(1); JSValue arg1 = callFrame->argument(2); JSValue arg2 = callFrame->argument(3); - - // TODO: remove this if; this switch case was added but not all the callsites using bare $ERR_INVALID_ARG_VALUE(msg) were updated - if (callFrame->argumentCount() == 2 && arg0.isString()) { - auto message = arg0.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - return JSC::JSValue::encode(createError(globalObject, error, message)); - } return JSValue::encode(ERR_INVALID_ARG_TYPE(scope, globalObject, arg0, arg1, arg2)); } diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts index 8ce069cb72..abe2a973cc 100644 --- a/src/js/bun/sql.ts +++ b/src/js/bun/sql.ts @@ -65,7 +65,7 @@ function normalizeSSLMode(value: string): SSLMode { } } - throw $ERR_INVALID_ARG_VALUE(`Invalid SSL mode: ${value}`); + throw $ERR_INVALID_ARG_VALUE("sslmode", value); } class Query extends PublicPromise { @@ -454,21 +454,33 @@ function loadOptions(o) { if (idleTimeout != null) { idleTimeout = Number(idleTimeout); if (idleTimeout > 2 ** 31 || idleTimeout < 0 || idleTimeout !== idleTimeout) { - throw $ERR_INVALID_ARG_VALUE("idle_timeout must be a non-negative integer less than 2^31"); + throw $ERR_INVALID_ARG_VALUE( + "options.idle_timeout", + idleTimeout, + "must be a non-negative integer less than 2^31", + ); } } if (connectionTimeout != null) { connectionTimeout = Number(connectionTimeout); if (connectionTimeout > 2 ** 31 || connectionTimeout < 0 || connectionTimeout !== connectionTimeout) { - throw $ERR_INVALID_ARG_VALUE("connection_timeout must be a non-negative integer less than 2^31"); + throw $ERR_INVALID_ARG_VALUE( + "options.connection_timeout", + connectionTimeout, + "must be a non-negative integer less than 2^31", + ); } } if (maxLifetime != null) { maxLifetime = Number(maxLifetime); if (maxLifetime > 2 ** 31 || maxLifetime < 0 || maxLifetime !== maxLifetime) { - throw $ERR_INVALID_ARG_VALUE("max_lifetime must be a non-negative integer less than 2^31"); + throw $ERR_INVALID_ARG_VALUE( + "options.max_lifetime", + maxLifetime, + "must be a non-negative integer less than 2^31", + ); } } diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index 414b943e7f..2df37ba6ea 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -1,10 +1,10 @@ const { hideFromStack } = require("internal/shared"); -const { ArrayIsArray } = require("internal/primordials"); const RegExpPrototypeExec = RegExp.prototype.exec; const ArrayPrototypeIncludes = Array.prototype.includes; const ArrayPrototypeJoin = Array.prototype.join; const ArrayPrototypeMap = Array.prototype.map; +const ArrayIsArray = Array.isArray; const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; /** @@ -28,7 +28,9 @@ const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/; function validateLinkHeaderFormat(value, name) { if (typeof value === "undefined" || !RegExpPrototypeExec.$call(linkValueRegExp, value)) { throw $ERR_INVALID_ARG_VALUE( - `The arguments ${name} is invalid must be an array or string of format "; rel=preload; as=style"`, + name, + value, + `must be an array or string of format "; rel=preload; as=style"`, ); } } @@ -59,7 +61,9 @@ function validateLinkHeaderValue(hints) { } throw $ERR_INVALID_ARG_VALUE( - `The arguments hints is invalid must be an array or string of format "; rel=preload; as=style"`, + "hints", + hints, + `must be an array or string of format "; rel=preload; as=style"`, ); } hideFromStack(validateLinkHeaderValue); diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index bb7544bdbc..a48c0027bc 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -349,8 +349,7 @@ class Http2ServerRequest extends Readable { set method(method) { validateString(method, "method"); - if (StringPrototypeTrim(method) === "") - throw $ERR_INVALID_ARG_VALUE(`The arguments method is invalid. Received ${method}`); + if (StringPrototypeTrim(method) === "") throw $ERR_INVALID_ARG_VALUE("method", method); this[kHeaders][HTTP2_HEADER_METHOD] = method; } @@ -642,7 +641,7 @@ class Http2ServerResponse extends Stream { } } else { if (headers.length % 2 !== 0) { - throw $ERR_INVALID_ARG_VALUE(`The arguments headers is invalid.`); + throw $ERR_INVALID_ARG_VALUE("headers", headers); } for (i = 0; i < headers.length; i += 2) { @@ -1637,7 +1636,7 @@ class Http2Stream extends Duplex { const sensitiveNames = {}; if (sensitives) { if (!$isJSArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid"); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -2048,7 +2047,7 @@ class ServerHttp2Stream extends Http2Stream { const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -2099,7 +2098,7 @@ class ServerHttp2Stream extends Http2Stream { const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; @@ -3091,7 +3090,7 @@ class ClientHttp2Session extends Http2Session { const sensitiveNames = {}; if (sensitives) { if (!$isArray(sensitives)) { - throw $ERR_INVALID_ARG_VALUE("The arguments headers[http2.neverIndex] is invalid."); + throw $ERR_INVALID_ARG_VALUE("headers[http2.neverIndex]", sensitives); } for (let i = 0; i < sensitives.length; i++) { sensitiveNames[sensitives[i]] = true; diff --git a/src/js/node/stream.ts b/src/js/node/stream.ts index 2a7f2b88e7..49433352a4 100644 --- a/src/js/node/stream.ts +++ b/src/js/node/stream.ts @@ -2361,7 +2361,7 @@ var require_readable = __commonJS({ } = options; if (encoding !== undefined && !Buffer.isEncoding(encoding)) - throw $ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); + throw $ERR_INVALID_ARG_VALUE("options.encoding", encoding); validateBoolean(objectMode, "options.objectMode"); // validateBoolean(native, "options.native"); diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index 5303d1b4e0..082fbfe1dc 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -133,7 +133,7 @@ function writeHeapSnapshot(path, options) { } if (!path) { - throw $ERR_INVALID_ARG_VALUE("path must be a non-empty string"); + throw $ERR_INVALID_ARG_VALUE("path", path, "must be a non-empty string"); } } else { path = getDefaultHeapSnapshotPath(); From 81fce29fd9a21a4a4b541154067d19390c316ee2 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 6 Jan 2025 23:52:19 -0800 Subject: [PATCH 54/56] S3: refactor + S3Client static method (#16198) --- packages/bun-types/bun.d.ts | 177 +- src/bun.js/ConsoleObject.zig | 8 +- src/bun.js/api/BunObject.zig | 7 +- src/bun.js/api/S3Client.classes.ts | 81 + src/bun.js/api/S3Stat.classes.ts | 30 + src/bun.js/bindings/BunClientData.cpp | 2 - src/bun.js/bindings/BunClientData.h | 1 - src/bun.js/bindings/BunObject+exports.h | 2 +- src/bun.js/bindings/BunObject.cpp | 3 +- src/bun.js/bindings/JSS3Bucket.cpp | 254 --- src/bun.js/bindings/JSS3Bucket.h | 50 - src/bun.js/bindings/JSS3File.cpp | 17 + src/bun.js/bindings/ZigGlobalObject.cpp | 6 - src/bun.js/bindings/ZigGlobalObject.h | 1 - src/bun.js/bindings/bindings.zig | 1 + .../bindings/generated_classes_list.zig | 3 + src/bun.js/webcore.zig | 2 + src/bun.js/webcore/S3Bucket.zig | 267 --- src/bun.js/webcore/S3Client.zig | 298 +++ src/bun.js/webcore/S3File.zig | 85 +- src/bun.js/webcore/S3Stat.zig | 58 + src/bun.js/webcore/blob.zig | 36 +- src/bun.js/webcore/response.classes.ts | 1 + src/bun.js/webcore/response.zig | 4 +- src/s3/credentials.zig | 39 +- src/s3/multipart.zig | 7 +- src/s3/multipart_options.zig | 10 +- src/s3/simple_request.zig | 6 + test/js/bun/s3/s3-insecure.test.ts | 6 +- test/js/bun/s3/s3.leak.test.ts | 4 +- test/js/bun/s3/s3.test.ts | 1869 +++++++++-------- 31 files changed, 1712 insertions(+), 1623 deletions(-) create mode 100644 src/bun.js/api/S3Client.classes.ts create mode 100644 src/bun.js/api/S3Stat.classes.ts delete mode 100644 src/bun.js/bindings/JSS3Bucket.cpp delete mode 100644 src/bun.js/bindings/JSS3Bucket.h delete mode 100644 src/bun.js/webcore/S3Bucket.zig create mode 100644 src/bun.js/webcore/S3Client.zig create mode 100644 src/bun.js/webcore/S3Stat.zig diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index edf6a27e5a..e4f66b59f9 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -17,6 +17,7 @@ declare module "bun" { import type { FFIFunctionCallableSymbol } from "bun:ffi"; import type { Encoding as CryptoEncoding } from "crypto"; import type { CipherNameAndProtocol, EphemeralKeyInfo, PeerCertificate } from "tls"; + import type { Stats } from "node:fs"; interface Env { NODE_ENV?: string; /** @@ -1241,110 +1242,14 @@ declare module "bun" { * Finish the upload. This also flushes the internal buffer. */ end(error?: Error): number | Promise; + + /** + * Get the stat of the file. + */ + stat(): Promise; } - type S3 = { - /** - * Create a new instance of an S3 bucket so that credentials can be managed - * from a single instance instead of being passed to every method. - * - * @param options The default options to use for the S3 client. Can be - * overriden by passing options to the methods. - * - * ## Keep S3 credentials in a single instance - * - * @example - * const bucket = new Bun.S3({ - * accessKeyId: "your-access-key", - * secretAccessKey: "your-secret-key", - * bucket: "my-bucket", - * endpoint: "https://s3.us-east-1.amazonaws.com", - * sessionToken: "your-session-token", - * }); - * - * // S3Bucket is callable, so you can do this: - * const file = bucket("my-file.txt"); - * - * // or this: - * await file.write("Hello Bun!"); - * await file.text(); - * - * // To delete the file: - * await bucket.delete("my-file.txt"); - * - * // To write a file without returning the instance: - * await bucket.write("my-file.txt", "Hello Bun!"); - * - */ - new (options?: S3Options): S3Bucket; - - /** - * Delete a file from an S3-compatible object storage service. - * - * @param path The path to the file. - * @param options The options to use for the S3 client. - * - * For an instance method version, {@link S3File.unlink}. You can also use {@link S3Bucket.unlink}. - * - * @example - * import { S3 } from "bun"; - * await S3.unlink("s3://my-bucket/my-file.txt", { - * accessKeyId: "your-access-key", - * secretAccessKey: "your-secret-key", - * }); - * - * @example - * await S3.unlink("key", { - * bucket: "my-bucket", - * accessKeyId: "your-access-key", - * secretAccessKey: "your-secret-key", - * }); - */ - delete(path: string, options?: S3Options): Promise; - /** - * unlink is an alias for {@link S3.delete} - */ - unlink: S3["delete"]; - - /** - * Writes data to an S3-compatible storage service. - * Supports various input types and handles large files with multipart uploads. - * - * @param path The path or key where the file will be written - * @param data The data to write - * @param options S3 configuration and upload options - * @returns promise that resolves with the number of bytes written - * - * @example - * // Writing a string - * await S3.write("hello.txt", "Hello World!", { - * bucket: "my-bucket", - * type: "text/plain" - * }); - * - * @example - * // Writing JSON - * await S3.write( - * "data.json", - * JSON.stringify({ hello: "world" }), - * { type: "application/json" } - * ); - * - * @example - * // Writing a large file with multipart upload - * await S3.write("large-file.dat", largeBuffer, { - * partSize: 10 * 1024 * 1024, // 10MB parts - * queueSize: 4, // Upload 4 parts in parallel - * retry: 3 // Retry failed parts up to 3 times - * }); - */ - write( - path: string, - data: string | ArrayBufferView | ArrayBufferLike | Response | Request | ReadableStream | Blob | File, - options?: S3Options, - ): Promise; - }; - var S3: S3; + var S3Client: S3Client; /** * Creates a new S3File instance for working with a single file. @@ -1487,7 +1392,7 @@ declare module "bun" { endpoint?: string; /** - * The size of each part in multipart uploads (in MiB). + * The size of each part in multipart uploads (in bytes). * - Minimum: 5 MiB * - Maximum: 5120 MiB * - Default: 5 MiB @@ -1495,7 +1400,7 @@ declare module "bun" { * @example * // Configuring multipart uploads * const file = s3("large-file.dat", { - * partSize: 10, // 10 MiB parts + * partSize: 10 * 1024 * 1024, // 10 MiB parts * queueSize: 4 // Upload 4 parts in parallel * }); * @@ -1589,6 +1494,13 @@ declare module "bun" { method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD"; } + interface S3Stats { + size: number; + lastModified: Date; + etag: string; + type: string; + } + /** * Represents a file in an S3-compatible storage service. * Extends the Blob interface for compatibility with web APIs. @@ -1858,6 +1770,13 @@ declare module "bun" { * await file.unlink(); */ unlink: S3File["delete"]; + + /** + * Get the stat of a file in an S3-compatible storage service. + * + * @returns Promise resolving to S3Stat + */ + stat(): Promise; } /** @@ -1867,7 +1786,7 @@ declare module "bun" { * * @example * // Basic bucket setup - * const bucket = new S3({ + * const bucket = new S3Client({ * bucket: "my-bucket", * accessKeyId: "key", * secretAccessKey: "secret" @@ -1881,19 +1800,53 @@ declare module "bun" { * const url = bucket.presign("file.pdf"); * await bucket.unlink("old.txt"); */ - type S3Bucket = { + type S3Client = { + /** + * Create a new instance of an S3 bucket so that credentials can be managed + * from a single instance instead of being passed to every method. + * + * @param options The default options to use for the S3 client. Can be + * overriden by passing options to the methods. + * + * ## Keep S3 credentials in a single instance + * + * @example + * const bucket = new Bun.S3Client({ + * accessKeyId: "your-access-key", + * secretAccessKey: "your-secret-key", + * bucket: "my-bucket", + * endpoint: "https://s3.us-east-1.amazonaws.com", + * sessionToken: "your-session-token", + * }); + * + * // S3Client is callable, so you can do this: + * const file = bucket.file("my-file.txt"); + * + * // or this: + * await file.write("Hello Bun!"); + * await file.text(); + * + * // To delete the file: + * await bucket.delete("my-file.txt"); + * + * // To write a file without returning the instance: + * await bucket.write("my-file.txt", "Hello Bun!"); + * + */ + new (options?: S3Options): S3Client; + /** * Creates an S3File instance for the given path. * * @example - * const file = bucket("image.jpg"); + * const file = bucket.file("image.jpg"); * await file.write(imageData); * const configFile = bucket("config.json", { * type: "application/json", * acl: "private" * }); */ - (path: string, options?: S3Options): S3File; + file(path: string, options?: S3Options): S3File; /** * Writes data directly to a path in the bucket. @@ -1978,6 +1931,7 @@ declare module "bun" { * } */ unlink(path: string, options?: S3Options): Promise; + delete: S3Client["unlink"]; /** * Get the size of a file in bytes. @@ -2016,6 +1970,13 @@ declare module "bun" { * } */ exists(path: string, options?: S3Options): Promise; + /** + * Get the stat of a file in an S3-compatible storage service. + * + * @param path The path to the file. + * @param options The options to use for the S3 client. + */ + stat(path: string, options?: S3Options): Promise; }; /** diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 06b282dde1..727c707559 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -21,7 +21,6 @@ const default_allocator = bun.default_allocator; const JestPrettyFormat = @import("./test/pretty_format.zig").JestPrettyFormat; const JSPromise = JSC.JSPromise; const EventType = JSC.EventType; -const S3Bucket = @import("./webcore/S3Bucket.zig"); pub const shim = Shimmer("Bun", "ConsoleObject", @This()); pub const Type = *anyopaque; pub const name = "Bun::ConsoleObject"; @@ -2217,10 +2216,6 @@ pub const Formatter = struct { ); }, .Class => { - if (S3Bucket.fromJS(value)) |s3bucket| { - S3Bucket.writeFormat(s3bucket, ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; - return; - } var printable = ZigString.init(&name_buf); value.getClassName(this.globalThis, &printable); this.addForNewLine(printable.len); @@ -2491,6 +2486,9 @@ pub const Formatter = struct { } else if (value.as(JSC.WebCore.Blob)) |blob| { blob.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; return; + } else if (value.as(JSC.WebCore.S3Client)) |s3client| { + s3client.writeFormat(ConsoleObject.Formatter, this, writer_, enable_ansi_colors) catch {}; + return; } else if (value.as(JSC.FetchHeaders) != null) { if (value.get_unsafe(this.globalThis, "toJSON")) |toJSONFunction| { this.addForNewLine("Headers ".len); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 8736bd7809..43afbb6793 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1,6 +1,5 @@ const conv = std.builtin.CallingConvention.Unspecified; const S3File = @import("../webcore/S3File.zig"); -const S3Bucket = @import("../webcore/S3Bucket.zig"); /// How to add a new function or property to the Bun global /// /// - Add a callback or property to the below struct @@ -72,6 +71,7 @@ pub const BunObject = struct { pub const stdin = toJSGetter(Bun.getStdin); pub const stdout = toJSGetter(Bun.getStdout); pub const unsafe = toJSGetter(Bun.getUnsafe); + pub const S3Client = toJSGetter(Bun.getS3ClientConstructor); // --- Getters --- fn getterName(comptime baseName: anytype) [:0]const u8 { @@ -132,6 +132,7 @@ pub const BunObject = struct { @export(BunObject.unsafe, .{ .name = getterName("unsafe") }); @export(BunObject.semver, .{ .name = getterName("semver") }); @export(BunObject.embeddedFiles, .{ .name = getterName("embeddedFiles") }); + @export(BunObject.S3Client, .{ .name = getterName("S3Client") }); // --- Getters -- // -- Callbacks -- @@ -3397,7 +3398,9 @@ pub fn getTOMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSVa pub fn getGlobConstructor(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { return JSC.API.Glob.getConstructor(globalThis); } - +pub fn getS3ClientConstructor(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { + return JSC.WebCore.S3Client.getConstructor(globalThis); +} pub fn getEmbeddedFiles(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { const vm = globalThis.bunVM(); const graph = vm.standalone_module_graph orelse return JSC.JSValue.createEmptyArray(globalThis, 0); diff --git a/src/bun.js/api/S3Client.classes.ts b/src/bun.js/api/S3Client.classes.ts new file mode 100644 index 0000000000..06839ca568 --- /dev/null +++ b/src/bun.js/api/S3Client.classes.ts @@ -0,0 +1,81 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "S3Client", + construct: true, + finalize: true, + configurable: false, + klass: { + file: { + fn: "staticFile", + length: 2, + }, + unlink: { + fn: "staticUnlink", + length: 2, + }, + delete: { + /// just an alias for unlink + fn: "staticUnlink", + length: 2, + }, + presign: { + fn: "staticPresign", + length: 2, + }, + exists: { + fn: "staticExists", + length: 2, + }, + size: { + fn: "staticSize", + length: 2, + }, + write: { + fn: "staticWrite", + length: 2, + }, + stat: { + fn: "staticStat", + length: 2, + }, + }, + JSType: "0b11101110", + proto: { + file: { + fn: "file", + length: 2, + }, + unlink: { + fn: "unlink", + length: 2, + }, + delete: { + /// just an alias for unlink + fn: "unlink", + length: 2, + }, + presign: { + fn: "presign", + length: 2, + }, + exists: { + fn: "exists", + length: 2, + }, + size: { + fn: "size", + length: 2, + }, + write: { + fn: "write", + length: 2, + }, + stat: { + fn: "stat", + length: 2, + }, + }, + }), +]; diff --git a/src/bun.js/api/S3Stat.classes.ts b/src/bun.js/api/S3Stat.classes.ts new file mode 100644 index 0000000000..e2339a014e --- /dev/null +++ b/src/bun.js/api/S3Stat.classes.ts @@ -0,0 +1,30 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "S3Stat", + construct: true, + finalize: true, + configurable: false, + klass: {}, + JSType: "0b11101110", + proto: { + size: { + getter: "getSize", + cache: true, + }, + lastModified: { + getter: "getLastModified", + cache: true, + }, + etag: { + getter: "getEtag", + cache: true, + }, + type: { + getter: "getContentType", + cache: true, + }, + }, + }), +]; diff --git a/src/bun.js/bindings/BunClientData.cpp b/src/bun.js/bindings/BunClientData.cpp index ed746e0e15..ee214c06b8 100644 --- a/src/bun.js/bindings/BunClientData.cpp +++ b/src/bun.js/bindings/BunClientData.cpp @@ -23,7 +23,6 @@ #include "JSDOMWrapper.h" #include #include "NodeVM.h" -#include "JSS3Bucket.h" #include "../../bake/BakeGlobalObject.h" namespace WebCore { @@ -34,7 +33,6 @@ RefPtr createBuiltinsSourceProvider(); JSHeapData::JSHeapData(Heap& heap) : m_heapCellTypeForJSWorkerGlobalScope(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForNodeVMGlobalObject(JSC::IsoHeapCellType::Args()) - , m_heapCellTypeForJSS3Bucket(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForBakeGlobalObject(JSC::IsoHeapCellType::Args()) , m_domBuiltinConstructorSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMBuiltinConstructorBase) , m_domConstructorSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMConstructorBase) diff --git a/src/bun.js/bindings/BunClientData.h b/src/bun.js/bindings/BunClientData.h index 953918f0eb..ef210b02ad 100644 --- a/src/bun.js/bindings/BunClientData.h +++ b/src/bun.js/bindings/BunClientData.h @@ -59,7 +59,6 @@ public: JSC::IsoHeapCellType m_heapCellTypeForJSWorkerGlobalScope; JSC::IsoHeapCellType m_heapCellTypeForNodeVMGlobalObject; - JSC::IsoHeapCellType m_heapCellTypeForJSS3Bucket; JSC::IsoHeapCellType m_heapCellTypeForBakeGlobalObject; private: diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index b638d6eb26..7ea8582949 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -31,6 +31,7 @@ macro(unsafe) \ macro(semver) \ macro(embeddedFiles) \ + macro(S3Client) \ // --- Callbacks --- #define FOR_EACH_CALLBACK(macro) \ @@ -58,7 +59,6 @@ macro(resolve) \ macro(resolveSync) \ macro(s3) \ - macro(S3) \ macro(serve) \ macro(sha) \ macro(shrink) \ diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index bd1760cfb0..6a812c8355 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -58,7 +58,6 @@ BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__getCacheStats); BUN_DECLARE_HOST_FUNCTION(Bun__fetch); BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect); BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7); -BUN_DECLARE_HOST_FUNCTION(Bun__S3Constructor); namespace Bun { using namespace JSC; @@ -629,6 +628,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj TOML BunObject_getter_wrap_TOML DontDelete|PropertyCallback Transpiler BunObject_getter_wrap_Transpiler DontDelete|PropertyCallback embeddedFiles BunObject_getter_wrap_embeddedFiles DontDelete|PropertyCallback + S3Client BunObject_getter_wrap_S3Client DontDelete|PropertyCallback allocUnsafe BunObject_callback_allocUnsafe DontDelete|Function 1 argv BunObject_getter_wrap_argv DontDelete|PropertyCallback build BunObject_callback_build DontDelete|Function 1 @@ -682,7 +682,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj revision constructBunRevision ReadOnly|DontDelete|PropertyCallback semver BunObject_getter_wrap_semver ReadOnly|DontDelete|PropertyCallback s3 BunObject_callback_s3 DontDelete|Function 1 - S3 Bun__S3Constructor DontDelete|Constructable|Function 1 sql constructBunSQLObject DontDelete|PropertyCallback serve BunObject_callback_serve DontDelete|Function 1 sha BunObject_callback_sha DontDelete|Function 1 diff --git a/src/bun.js/bindings/JSS3Bucket.cpp b/src/bun.js/bindings/JSS3Bucket.cpp deleted file mode 100644 index b85a41029a..0000000000 --- a/src/bun.js/bindings/JSS3Bucket.cpp +++ /dev/null @@ -1,254 +0,0 @@ - -#include "root.h" - -#include "JavaScriptCore/JSType.h" -#include "JavaScriptCore/JSObject.h" -#include "JavaScriptCore/JSGlobalObject.h" -#include -#include "ZigGeneratedClasses.h" - -#include "JSS3Bucket.h" -#include -#include -#include "JavaScriptCore/JSCJSValue.h" -#include "ErrorCode.h" - -namespace Bun { -using namespace JSC; - -// External C functions declarations -extern "C" { -SYSV_ABI void* JSS3Bucket__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__call(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__unlink(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__write(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__exists(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3Bucket__size(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI void* JSS3Bucket__deinit(void* ptr); -} - -// Forward declarations -JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_unlink); -JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_write); -JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_presign); -JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_exists); -JSC_DECLARE_HOST_FUNCTION(functionS3Bucket_size); - -static const HashTableValue JSS3BucketPrototypeTableValues[] = { - { "unlink"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, - { "delete"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_unlink, 0 } }, - { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_write, 1 } }, - { "presign"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_presign, 1 } }, - { "exists"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_exists, 1 } }, - { "size"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3Bucket_size, 1 } }, -}; - -class JSS3BucketPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static constexpr unsigned StructureFlags = Base::StructureFlags; - - static JSS3BucketPrototype* create( - JSC::VM& vm, - JSC::JSGlobalObject* globalObject, - JSC::Structure* structure) - { - JSS3BucketPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSS3BucketPrototype(vm, structure); - prototype->finishCreation(vm, globalObject); - return prototype; - } - - static JSC::Structure* createStructure( - JSC::VM& vm, - JSC::JSGlobalObject* globalObject, - JSC::JSValue prototype) - { - auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info()); - structure->setMayBePrototype(true); - return structure; - } - - DECLARE_INFO; - - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSS3BucketPrototype, Base); - return &vm.plainObjectSpace(); - } - -protected: - JSS3BucketPrototype(JSC::VM& vm, JSC::Structure* structure) - : Base(vm, structure) - { - } - - void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) - { - Base::finishCreation(vm); - ASSERT(inherits(info())); - reifyStaticProperties(vm, info(), JSS3BucketPrototypeTableValues, *this); - } -}; - -// Implementation of JSS3Bucket methods -void JSS3Bucket::destroy(JSCell* cell) -{ - static_cast(cell)->JSS3Bucket::~JSS3Bucket(); -} - -JSS3Bucket::~JSS3Bucket() -{ - if (ptr) { - JSS3Bucket__deinit(ptr); - } -} - -JSC::GCClient::IsoSubspace* JSS3Bucket::subspaceForImpl(JSC::VM& vm) -{ - // This needs it's own heapcell because of the destructor. - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForJSS3Bucket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSS3Bucket = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForJSS3Bucket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForJSS3Bucket = std::forward(space); }, - [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForJSS3Bucket; }); -} - -JSC_HOST_CALL_ATTRIBUTES EncodedJSValue JSS3Bucket::call(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) -{ - auto& vm = lexicalGlobalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - JSValue thisValue = callFrame->jsCallee(); - auto* thisObject = jsDynamicCast(thisValue); - if (UNLIKELY(!thisObject)) { - Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - ASSERT(thisObject->ptr); - - return JSS3Bucket__call(thisObject->ptr, lexicalGlobalObject, callFrame); -} - -JSC_HOST_CALL_ATTRIBUTES EncodedJSValue JSS3Bucket::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) -{ - auto& vm = lexicalGlobalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, "S3Bucket is not constructable. To instantiate a bucket, do Bun.S3()"_s); - return {}; -} - -JSS3Bucket* JSS3Bucket::create(JSC::VM& vm, Zig::GlobalObject* globalObject, void* ptr) -{ - auto* structure = globalObject->m_JSS3BucketStructure.getInitializedOnMainThread(globalObject); - NativeExecutable* executable = vm.getHostFunction(&JSS3Bucket::call, ImplementationVisibility::Public, &JSS3Bucket::construct, String("S3Bucket"_s)); - JSS3Bucket* functionObject = new (NotNull, JSC::allocateCell(vm)) JSS3Bucket(vm, executable, globalObject, structure, ptr); - functionObject->finishCreation(vm, executable, 1, "S3Bucket"_s); - return functionObject; -} - -JSC::Structure* JSS3Bucket::createStructure(JSC::JSGlobalObject* globalObject) -{ - auto& vm = globalObject->vm(); - auto* prototype = JSS3BucketPrototype::create(vm, globalObject, JSS3BucketPrototype::createStructure(vm, globalObject, globalObject->functionPrototype())); - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::JSFunctionType, StructureFlags), info(), NonArray); -} - -JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_unlink, (JSGlobalObject * globalObject, CallFrame* callframe)) -{ - auto* thisObject = jsDynamicCast(callframe->thisValue()); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!thisObject) { - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - return JSS3Bucket__unlink(thisObject->ptr, globalObject, callframe); -} - -JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_write, (JSGlobalObject * globalObject, CallFrame* callframe)) -{ - auto* thisObject = jsDynamicCast(callframe->thisValue()); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!thisObject) { - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - return JSS3Bucket__write(thisObject->ptr, globalObject, callframe); -} - -JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_presign, (JSGlobalObject * globalObject, CallFrame* callframe)) -{ - auto* thisObject = jsDynamicCast(callframe->thisValue()); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!thisObject) { - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - return JSS3Bucket__presign(thisObject->ptr, globalObject, callframe); -} - -JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_exists, (JSGlobalObject * globalObject, CallFrame* callframe)) -{ - auto* thisObject = jsDynamicCast(callframe->thisValue()); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!thisObject) { - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - return JSS3Bucket__exists(thisObject->ptr, globalObject, callframe); -} - -JSC_DEFINE_HOST_FUNCTION(functionS3Bucket_size, (JSGlobalObject * globalObject, CallFrame* callframe)) -{ - auto* thisObject = jsDynamicCast(callframe->thisValue()); - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!thisObject) { - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3Bucket instance"_s); - return {}; - } - - return JSS3Bucket__size(thisObject->ptr, globalObject, callframe); -} - -extern "C" { -SYSV_ABI void* BUN__getJSS3Bucket(JSC::EncodedJSValue value) -{ - JSValue thisValue = JSC::JSValue::decode(value); - auto* thisObject = jsDynamicCast(thisValue); - return thisObject ? thisObject->ptr : nullptr; -}; - -BUN_DEFINE_HOST_FUNCTION(Bun__S3Constructor, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - void* ptr = JSS3Bucket__construct(globalObject, callframe); - RETURN_IF_EXCEPTION(scope, {}); - ASSERT(ptr); - - return JSValue::encode(JSS3Bucket::create(vm, defaultGlobalObject(globalObject), ptr)); -} -} - -Structure* createJSS3BucketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) -{ - return JSS3Bucket::createStructure(globalObject); -} - -const JSC::ClassInfo JSS3BucketPrototype::s_info = { "S3Bucket"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3BucketPrototype) }; -const JSC::ClassInfo JSS3Bucket::s_info = { "S3Bucket"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3Bucket) }; - -} // namespace Bun diff --git a/src/bun.js/bindings/JSS3Bucket.h b/src/bun.js/bindings/JSS3Bucket.h deleted file mode 100644 index 84d0868e23..0000000000 --- a/src/bun.js/bindings/JSS3Bucket.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -namespace Zig { -class GlobalObject; -} - -namespace Bun { -using namespace JSC; - -class JSS3Bucket : public JSC::JSFunction { - using Base = JSC::JSFunction; - static constexpr unsigned StructureFlags = Base::StructureFlags; - -public: - static constexpr bool needsDestruction = true; - - JSS3Bucket(JSC::VM& vm, NativeExecutable* executable, JSGlobalObject* globalObject, Structure* structure, void* ptr) - : Base(vm, executable, globalObject, structure) - { - this->ptr = ptr; - } - DECLARE_INFO; - - static void destroy(JSCell* cell); - ~JSS3Bucket(); - - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - - static JSC_HOST_CALL_ATTRIBUTES EncodedJSValue call(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame); - static JSC_HOST_CALL_ATTRIBUTES EncodedJSValue construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame); - - static JSS3Bucket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, void* ptr); - static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); - - void* ptr; -}; - -// Constructor helper -JSValue constructS3Bucket(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe); -Structure* createJSS3BucketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); - -} // namespace Bun diff --git a/src/bun.js/bindings/JSS3File.cpp b/src/bun.js/bindings/JSS3File.cpp index 614d6a983d..7e2d2309b5 100644 --- a/src/bun.js/bindings/JSS3File.cpp +++ b/src/bun.js/bindings/JSS3File.cpp @@ -24,12 +24,14 @@ using namespace WebCore; extern "C" { SYSV_ABI void* JSS3File__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); SYSV_ABI EncodedJSValue JSS3File__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3File__stat(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); SYSV_ABI EncodedJSValue JSS3File__bucket(void* ptr, JSC::JSGlobalObject*); SYSV_ABI bool JSS3File__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); } // Forward declarations JSC_DECLARE_HOST_FUNCTION(functionS3File_presign); +JSC_DECLARE_HOST_FUNCTION(functionS3File_stat); static JSC_DECLARE_CUSTOM_GETTER(getterS3File_bucket); static JSC_DEFINE_CUSTOM_GETTER(getterS3File_bucket, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { @@ -46,6 +48,7 @@ static JSC_DEFINE_CUSTOM_GETTER(getterS3File_bucket, (JSC::JSGlobalObject * glob } static const HashTableValue JSS3FilePrototypeTableValues[] = { { "presign"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3File_presign, 1 } }, + { "stat"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionS3File_stat, 1 } }, { "bucket"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor | PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, getterS3File_bucket, 0 } }, }; class JSS3FilePrototype final : public WebCore::JSBlobPrototype { @@ -93,6 +96,8 @@ protected: Base::finishCreation(vm, globalObject); ASSERT(inherits(info())); reifyStaticProperties(vm, JSS3File::info(), JSS3FilePrototypeTableValues, *this); + + this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsOwnedString(vm, "S3File"_s), 0); } }; @@ -171,6 +176,18 @@ JSC_DEFINE_HOST_FUNCTION(functionS3File_presign, (JSGlobalObject * globalObject, return JSS3File__presign(thisObject->wrapped(), globalObject, callframe); } +JSC_DEFINE_HOST_FUNCTION(functionS3File_stat, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto* thisObject = jsDynamicCast(callframe->thisValue()); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!thisObject) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3File instance"_s); + return {}; + } + return JSS3File__stat(thisObject->wrapped(), globalObject, callframe); +} + const JSC::ClassInfo JSS3FilePrototype::s_info = { "S3File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3FilePrototype) }; const JSC::ClassInfo JSS3File::s_info = { "S3File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3File) }; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 16402e965b..5ba9f8fcc0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -159,7 +159,6 @@ #include "JSPerformanceResourceTiming.h" #include "JSPerformanceTiming.h" -#include "JSS3Bucket.h" #include "JSS3File.h" #include "S3Error.h" #if ENABLE(REMOTE_INSPECTOR) @@ -2867,10 +2866,6 @@ void GlobalObject::finishCreation(VM& vm) init.set(result.toObject(init.owner)); }); - m_JSS3BucketStructure.initLater( - [](const Initializer& init) { - init.set(Bun::createJSS3BucketStructure(init.vm, init.owner)); - }); m_JSS3FileStructure.initLater( [](const Initializer& init) { init.set(Bun::createJSS3FileStructure(init.vm, init.owner)); @@ -3808,7 +3803,6 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSCryptoKey.visit(visitor); thisObject->m_lazyStackCustomGetterSetter.visit(visitor); thisObject->m_JSDOMFileConstructor.visit(visitor); - thisObject->m_JSS3BucketStructure.visit(visitor); thisObject->m_JSS3FileStructure.visit(visitor); thisObject->m_S3ErrorStructure.visit(visitor); thisObject->m_JSFFIFunctionStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index fb6d919ba5..c556cd8688 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -478,7 +478,6 @@ public: LazyProperty m_processEnvObject; - LazyProperty m_JSS3BucketStructure; LazyProperty m_JSS3FileStructure; LazyProperty m_S3ErrorStructure; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 75911433c4..6e83c569d3 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5642,6 +5642,7 @@ pub const JSValue = enum(i64) { return JSC.Node.validators.throwErrInvalidArgType(global, property_name, .{}, "string", prop); }, i32 => return prop.coerce(i32, global), + i64 => return prop.coerce(i64, global), else => @compileError("TODO:" ++ @typeName(T)), } } diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 72095df5a5..995ddb4466 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -77,4 +77,7 @@ pub const Classes = struct { pub const NativeZlib = JSC.API.NativeZlib; pub const NativeBrotli = JSC.API.NativeBrotli; pub const FrameworkFileSystemRouter = bun.bake.FrameworkRouter.JSFrameworkRouter; + + pub const S3Client = JSC.WebCore.S3Client; + pub const S3Stat = JSC.WebCore.S3Stat; }; diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 3484590697..8699b4b8a7 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -2,6 +2,8 @@ pub usingnamespace @import("./webcore/response.zig"); pub usingnamespace @import("./webcore/encoding.zig"); pub usingnamespace @import("./webcore/streams.zig"); pub usingnamespace @import("./webcore/blob.zig"); +pub usingnamespace @import("./webcore/S3Stat.zig"); +pub usingnamespace @import("./webcore/S3Client.zig"); pub usingnamespace @import("./webcore/request.zig"); pub usingnamespace @import("./webcore/body.zig"); pub const ObjectURLRegistry = @import("./webcore/ObjectURLRegistry.zig"); diff --git a/src/bun.js/webcore/S3Bucket.zig b/src/bun.js/webcore/S3Bucket.zig deleted file mode 100644 index 4d3b95ff16..0000000000 --- a/src/bun.js/webcore/S3Bucket.zig +++ /dev/null @@ -1,267 +0,0 @@ -const bun = @import("root").bun; -const JSC = bun.JSC; -const JSValue = JSC.JSValue; -const Blob = JSC.WebCore.Blob; -const PathOrBlob = JSC.Node.PathOrBlob; -const ZigString = JSC.ZigString; -const Method = bun.http.Method; -const S3File = @import("./S3File.zig"); -const S3Credentials = bun.S3.S3Credentials; - -const S3BucketOptions = struct { - credentials: *S3Credentials, - options: bun.S3.MultiPartUploadOptions = .{}, - acl: ?bun.S3.ACL = null, - pub usingnamespace bun.New(@This()); - - pub fn deinit(this: *@This()) void { - this.credentials.deref(); - this.destroy(); - } -}; - -pub fn writeFormatCredentials(credentials: *S3Credentials, options: bun.S3.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { - try writer.writeAll("\n"); - - { - const Writer = @TypeOf(writer); - - formatter.indent += 1; - defer formatter.indent -|= 1; - - const endpoint = if (credentials.endpoint.len > 0) credentials.endpoint else "https://s3..amazonaws.com"; - - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("endpoint: \"", enable_ansi_colors)); - try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{endpoint}); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - try writer.writeAll("\n"); - - const region = if (credentials.region.len > 0) credentials.region else S3Credentials.guessRegion(credentials.endpoint); - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("region: \"", enable_ansi_colors)); - try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{region}); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - try writer.writeAll("\n"); - - // PS: We don't want to print the credentials if they are empty just signal that they are there without revealing them - if (credentials.accessKeyId.len > 0) { - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("accessKeyId: \"[REDACTED]\"", enable_ansi_colors)); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - - try writer.writeAll("\n"); - } - - if (credentials.secretAccessKey.len > 0) { - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("secretAccessKey: \"[REDACTED]\"", enable_ansi_colors)); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - - try writer.writeAll("\n"); - } - - if (credentials.sessionToken.len > 0) { - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("sessionToken: \"[REDACTED]\"", enable_ansi_colors)); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - - try writer.writeAll("\n"); - } - - if (acl) |acl_value| { - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("acl: ", enable_ansi_colors)); - try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{acl_value.toString()}); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - - try writer.writeAll("\n"); - } - - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("partSize: ", enable_ansi_colors)); - try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.partSize), .NumberObject, enable_ansi_colors); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - - try writer.writeAll("\n"); - - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("queueSize: ", enable_ansi_colors)); - try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.queueSize), .NumberObject, enable_ansi_colors); - formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); - try writer.writeAll("\n"); - - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime bun.Output.prettyFmt("retry: ", enable_ansi_colors)); - try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.retry), .NumberObject, enable_ansi_colors); - try writer.writeAll("\n"); - } -} -pub fn writeFormat(this: *S3BucketOptions, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { - try writer.writeAll(comptime bun.Output.prettyFmt("S3Bucket", enable_ansi_colors)); - if (this.credentials.bucket.len > 0) { - try writer.print( - comptime bun.Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), - .{ - this.credentials.bucket, - }, - ); - } else { - try writer.writeAll(comptime bun.Output.prettyFmt(" {{", enable_ansi_colors)); - } - - try writeFormatCredentials(this.credentials, this.options, this.acl, Formatter, formatter, writer, enable_ansi_colors); - try formatter.writeIndent(@TypeOf(writer), writer); - try writer.writeAll("}"); - formatter.resetLine(); -} -extern fn BUN__getJSS3Bucket(value: JSValue) callconv(JSC.conv) ?*S3BucketOptions; - -pub fn fromJS(value: JSValue) ?*S3BucketOptions { - return BUN__getJSS3Bucket(value); -} - -pub fn call(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - if (args.len() == 0) { - return globalThis.ERR_MISSING_ARGS("Expected a path ", .{}).throw(); - } - return globalThis.throwInvalidArguments("Expected a path", .{}); - }; - errdefer path.deinit(); - const options = args.nextEat(); - var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); - blob.allocator = bun.default_allocator; - return blob.toJS(globalThis); -} - -pub fn presign(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - if (args.len() == 0) { - return globalThis.ERR_MISSING_ARGS("Expected a path to presign", .{}).throw(); - } - return globalThis.throwInvalidArguments("Expected a path to presign", .{}); - }; - errdefer path.deinit(); - - const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); - defer blob.detach(); - return S3File.getPresignUrlFrom(&blob, globalThis, options); -} - -pub fn exists(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - if (args.len() == 0) { - return globalThis.ERR_MISSING_ARGS("Expected a path to check if it exists", .{}).throw(); - } - return globalThis.throwInvalidArguments("Expected a path to check if it exists", .{}); - }; - errdefer path.deinit(); - const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); - defer blob.detach(); - return S3File.S3BlobStatTask.exists(globalThis, &blob); -} - -pub fn size(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - if (args.len() == 0) { - return globalThis.ERR_MISSING_ARGS("Expected a path to check the size of", .{}).throw(); - } - return globalThis.throwInvalidArguments("Expected a path to check the size of", .{}); - }; - errdefer path.deinit(); - const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); - defer blob.detach(); - return S3File.S3BlobStatTask.size(globalThis, &blob); -} - -pub fn write(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(3).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - return globalThis.ERR_MISSING_ARGS("Expected a path to write to", .{}).throw(); - }; - errdefer path.deinit(); - const data = args.nextEat() orelse { - return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to write", .{}).throw(); - }; - - const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); - defer blob.detach(); - var blob_internal: PathOrBlob = .{ .blob = blob }; - return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ - .mkdirp_if_not_exists = false, - .extra_options = options, - }); -} - -pub fn unlink(ptr: *S3BucketOptions, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { - return globalThis.ERR_MISSING_ARGS("Expected a path to unlink", .{}).throw(); - }; - errdefer path.deinit(); - const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); - defer blob.detach(); - return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); -} -pub fn construct(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) ?*S3BucketOptions { - const arguments = callframe.arguments_old(1).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - const options = args.nextEat() orelse { - globalThis.ERR_MISSING_ARGS("Expected S3 options to be passed", .{}).throw() catch return null; - }; - if (options.isEmptyOrUndefinedOrNull() or !options.isObject()) { - globalThis.throwInvalidArguments("Expected S3 options to be passed", .{}) catch return null; - } - var aws_options = S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, options, null, globalThis) catch return null; - defer aws_options.deinit(); - return S3BucketOptions.new(.{ - .credentials = aws_options.credentials.dupe(), - .options = aws_options.options, - .acl = aws_options.acl, - }); -} -pub fn finalize(ptr: *S3BucketOptions) callconv(JSC.conv) void { - ptr.deinit(); -} -pub const exports = struct { - pub const JSS3Bucket__exists = JSC.toJSHostFunctionWithContext(S3BucketOptions, exists); - pub const JSS3Bucket__size = JSC.toJSHostFunctionWithContext(S3BucketOptions, size); - pub const JSS3Bucket__write = JSC.toJSHostFunctionWithContext(S3BucketOptions, write); - pub const JSS3Bucket__unlink = JSC.toJSHostFunctionWithContext(S3BucketOptions, unlink); - pub const JSS3Bucket__presign = JSC.toJSHostFunctionWithContext(S3BucketOptions, presign); - pub const JSS3Bucket__call = JSC.toJSHostFunctionWithContext(S3BucketOptions, call); -}; - -comptime { - @export(exports.JSS3Bucket__exists, .{ .name = "JSS3Bucket__exists" }); - @export(exports.JSS3Bucket__size, .{ .name = "JSS3Bucket__size" }); - @export(exports.JSS3Bucket__write, .{ .name = "JSS3Bucket__write" }); - @export(exports.JSS3Bucket__unlink, .{ .name = "JSS3Bucket__unlink" }); - @export(exports.JSS3Bucket__presign, .{ .name = "JSS3Bucket__presign" }); - @export(exports.JSS3Bucket__call, .{ .name = "JSS3Bucket__call" }); - @export(finalize, .{ .name = "JSS3Bucket__deinit" }); - @export(construct, .{ .name = "JSS3Bucket__construct" }); -} diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig new file mode 100644 index 0000000000..37b1799cb4 --- /dev/null +++ b/src/bun.js/webcore/S3Client.zig @@ -0,0 +1,298 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const Blob = JSC.WebCore.Blob; +const PathOrBlob = JSC.Node.PathOrBlob; +const ZigString = JSC.ZigString; +const Method = bun.http.Method; +const S3File = @import("./S3File.zig"); +const S3Credentials = bun.S3.S3Credentials; + +pub fn writeFormatCredentials(credentials: *S3Credentials, options: bun.S3.MultiPartUploadOptions, acl: ?bun.S3.ACL, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll("\n"); + + { + const Writer = @TypeOf(writer); + + formatter.indent += 1; + defer formatter.indent -|= 1; + + const endpoint = if (credentials.endpoint.len > 0) credentials.endpoint else "https://s3..amazonaws.com"; + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("endpoint: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{endpoint}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + const region = if (credentials.region.len > 0) credentials.region else S3Credentials.guessRegion(credentials.endpoint); + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("region: \"", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{region}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + // PS: We don't want to print the credentials if they are empty just signal that they are there without revealing them + if (credentials.accessKeyId.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("accessKeyId: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.secretAccessKey.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("secretAccessKey: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (credentials.sessionToken.len > 0) { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("sessionToken: \"[REDACTED]\"", enable_ansi_colors)); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + if (acl) |acl_value| { + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("acl: ", enable_ansi_colors)); + try writer.print(comptime bun.Output.prettyFmt("{s}\"", enable_ansi_colors), .{acl_value.toString()}); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + } + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("partSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.partSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("queueSize: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.queueSize), .NumberObject, enable_ansi_colors); + formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory(); + try writer.writeAll("\n"); + + try formatter.writeIndent(Writer, writer); + try writer.writeAll(comptime bun.Output.prettyFmt("retry: ", enable_ansi_colors)); + try formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(options.retry), .NumberObject, enable_ansi_colors); + try writer.writeAll("\n"); + } +} + +pub const S3Client = struct { + const log = bun.Output.scoped(.S3Client, false); + pub usingnamespace JSC.Codegen.JSS3Client; + + pub usingnamespace bun.New(@This()); + credentials: *S3Credentials, + options: bun.S3.MultiPartUploadOptions = .{}, + acl: ?bun.S3.ACL = null, + + pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*@This() { + const arguments = callframe.arguments_old(1).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + var aws_options = try S3Credentials.getCredentialsWithOptions(globalThis.bunVM().transpiler.env.getS3Credentials(), .{}, args.nextEat(), null, globalThis); + defer aws_options.deinit(); + return S3Client.new(.{ + .credentials = aws_options.credentials.dupe(), + .options = aws_options.options, + .acl = aws_options.acl, + }); + } + + pub fn writeFormat(this: *@This(), comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { + try writer.writeAll(comptime bun.Output.prettyFmt("S3Client", enable_ansi_colors)); + if (this.credentials.bucket.len > 0) { + try writer.print( + comptime bun.Output.prettyFmt(" (\"{s}\") {{", enable_ansi_colors), + .{ + this.credentials.bucket, + }, + ); + } else { + try writer.writeAll(comptime bun.Output.prettyFmt(" {{", enable_ansi_colors)); + } + + try writeFormatCredentials(this.credentials, this.options, this.acl, Formatter, formatter, writer, enable_ansi_colors); + try formatter.writeIndent(@TypeOf(writer), writer); + try writer.writeAll("}"); + formatter.resetLine(); + } + pub fn file(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path ", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl)); + blob.allocator = bun.default_allocator; + return blob.toJS(globalThis); + } + + pub fn presign(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to presign", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to presign", .{}); + }; + errdefer path.deinit(); + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.getPresignUrlFrom(&blob, globalThis, options); + } + + pub fn exists(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check if it exists", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check if it exists", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.exists(globalThis, &blob); + } + + pub fn size(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check the size of", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check the size of", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.size(globalThis, &blob); + } + + pub fn stat(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + if (args.len() == 0) { + return globalThis.ERR_MISSING_ARGS("Expected a path to check the stat of", .{}).throw(); + } + return globalThis.throwInvalidArguments("Expected a path to check the stat of", .{}); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return S3File.S3BlobStatTask.stat(globalThis, &blob); + } + + pub fn write(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to write to", .{}).throw(); + }; + errdefer path.deinit(); + const data = args.nextEat() orelse { + return globalThis.ERR_MISSING_ARGS("Expected a Blob-y thing to write", .{}).throw(); + }; + + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + var blob_internal: PathOrBlob = .{ .blob = blob }; + return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ + .mkdirp_if_not_exists = false, + .extra_options = options, + }); + } + + pub fn unlink(ptr: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + const path: JSC.Node.PathLike = try JSC.Node.PathLike.fromJS(globalThis, &args) orelse { + return globalThis.ERR_MISSING_ARGS("Expected a path to unlink", .{}).throw(); + }; + errdefer path.deinit(); + const options = args.nextEat(); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl); + defer blob.detach(); + return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); + } + + pub fn deinit(this: *@This()) void { + this.credentials.deref(); + this.destroy(); + } + + pub fn finalize( + this: *@This(), + ) void { + this.deinit(); + } + + // Static methods + + pub fn staticWrite(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.write(globalThis, callframe); + } + + pub fn staticPresign(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.presign(globalThis, callframe); + } + + pub fn staticExists(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.exists(globalThis, callframe); + } + + pub fn staticSize(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.size(globalThis, callframe); + } + + pub fn staticUnlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.unlink(globalThis, callframe); + } + + pub fn staticFile(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + const path = (try JSC.Node.PathLike.fromJS(globalThis, &args)) orelse { + return globalThis.throwInvalidArguments("Expected file path string", .{}); + }; + + return try S3File.constructInternalJS(globalThis, path, args.nextEat()); + } + pub fn staticStat(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + return S3File.stat(globalThis, callframe); + } +}; diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index f73f99b890..e6f34f97fe 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const bun = @import("root").bun; const JSC = bun.JSC; const JSValue = JSC.JSValue; @@ -7,9 +8,9 @@ const ZigString = JSC.ZigString; const Method = bun.http.Method; const strings = bun.strings; const Output = bun.Output; -const S3Bucket = @import("./S3Bucket.zig"); +const S3Client = @import("./S3Client.zig"); const S3 = bun.S3; - +const S3Stat = @import("./S3Stat.zig").S3Stat; pub fn writeFormat(s3: *Blob.S3Store, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { try writer.writeAll(comptime Output.prettyFmt("S3Ref", enable_ansi_colors)); const credentials = s3.getCredentials(); @@ -31,7 +32,7 @@ pub fn writeFormat(s3: *Blob.S3Store, comptime Formatter: type, formatter: *Form ); } - try S3Bucket.writeFormatCredentials(credentials, s3.options, s3.acl, Formatter, formatter, writer, enable_ansi_colors); + try S3Client.writeFormatCredentials(credentials, s3.options, s3.acl, Formatter, formatter, writer, enable_ansi_colors); try formatter.writeIndent(@TypeOf(writer), writer); try writer.writeAll("}"); formatter.resetLine(); @@ -99,7 +100,7 @@ pub fn unlink(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS } } -pub fn upload(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { +pub fn write(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { const arguments = callframe.arguments_old(3).slice(); var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); defer args.deinit(); @@ -350,8 +351,27 @@ pub const S3BlobStatTask = struct { const globalThis = this.promise.globalObject().?; switch (result) { - .success => |stat| { - this.promise.resolve(globalThis, JSValue.jsNumber(stat.size)); + .success => |stat_result| { + this.promise.resolve(globalThis, JSValue.jsNumber(stat_result.size)); + }, + inline .not_found, .failure => |err| { + this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); + }, + } + } + + pub fn onS3StatResolved(result: S3.S3StatResult, this: *S3BlobStatTask) void { + defer this.deinit(); + const globalThis = this.promise.globalObject().?; + switch (result) { + .success => |stat_result| { + this.promise.resolve(globalThis, S3Stat.init( + stat_result.size, + stat_result.etag, + stat_result.contentType, + stat_result.lastModified, + globalThis, + ).toJS(globalThis)); }, inline .not_found, .failure => |err| { this.promise.reject(globalThis, err.toJS(globalThis, this.store.data.s3.path())); @@ -373,7 +393,20 @@ pub const S3BlobStatTask = struct { S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3ExistsResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); return promise; } + pub fn stat(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { + const this = S3BlobStatTask.new(.{ + .promise = JSC.JSPromise.Strong.init(globalThis), + .store = blob.store.?, + }); + this.store.ref(); + const promise = this.promise.value(); + const credentials = blob.store.?.data.s3.getCredentials(); + const path = blob.store.?.data.s3.path(); + const env = globalThis.bunVM().transpiler.env; + S3.stat(credentials, path, @ptrCast(&S3BlobStatTask.onS3StatResolved), this, if (env.getHttpProxy(true, null)) |proxy| proxy.href else null); + return promise; + } pub fn size(globalThis: *JSC.JSGlobalObject, blob: *Blob) JSValue { const this = S3BlobStatTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalThis), @@ -478,6 +511,44 @@ pub fn getPresignUrl(this: *Blob, globalThis: *JSC.JSGlobalObject, callframe: *J return getPresignUrlFrom(this, globalThis, if (args.len > 0) args.ptr[0] else null); } +pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(JSC.conv) JSValue { + return S3BlobStatTask.stat(globalThis, this); +} + +pub fn stat(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const arguments = callframe.arguments_old(3).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + // accept a path or a blob + var path_or_blob = try PathOrBlob.fromJSNoCopy(globalThis, &args); + errdefer { + if (path_or_blob == .path) { + path_or_blob.path.deinit(); + } + } + + if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data != .s3)) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + + switch (path_or_blob) { + .path => |path| { + const options = args.nextEat(); + if (path == .fd) { + return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); + } + var blob = try constructS3FileInternalStore(globalThis, path.path, options); + defer blob.deinit(); + + return S3BlobStatTask.stat(globalThis, &blob); + }, + .blob => |*blob| { + return S3BlobStatTask.stat(globalThis, blob); + }, + } +} + pub fn constructInternalJS( globalObject: *JSC.JSGlobalObject, path: JSC.Node.PathLike, @@ -532,10 +603,12 @@ comptime { @export(construct, .{ .name = "JSS3File__construct" }); @export(hasInstance, .{ .name = "JSS3File__hasInstance" }); @export(getBucket, .{ .name = "JSS3File__bucket" }); + @export(getStat, .{ .name = "JSS3File__stat" }); } pub const exports = struct { pub const JSS3File__presign = JSC.toJSHostFunctionWithContext(Blob, getPresignUrl); + pub const JSS3File__stat = JSC.toJSHostFunctionWithContext(Blob, getStat); }; extern fn BUN__createJSS3File(*JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSValue; extern fn BUN__createJSS3FileUnsafely(*JSC.JSGlobalObject, *Blob) callconv(JSC.conv) JSValue; diff --git a/src/bun.js/webcore/S3Stat.zig b/src/bun.js/webcore/S3Stat.zig new file mode 100644 index 0000000000..5635307477 --- /dev/null +++ b/src/bun.js/webcore/S3Stat.zig @@ -0,0 +1,58 @@ +const bun = @import("../../bun.zig"); +const JSC = @import("../../JSC.zig"); + +pub const S3Stat = struct { + const log = bun.Output.scoped(.S3Stat, false); + pub usingnamespace JSC.Codegen.JSS3Stat; + pub usingnamespace bun.New(@This()); + + size: u64, + etag: bun.String, + contentType: bun.String, + lastModified: f64, + + pub fn constructor(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() { + return globalThis.throwInvalidArguments("S3Stat is not constructable", .{}); + } + + pub fn init( + size: u64, + etag: []const u8, + contentType: []const u8, + lastModified: []const u8, + globalThis: *JSC.JSGlobalObject, + ) *@This() { + var date_str = bun.String.init(lastModified); + defer date_str.deref(); + const last_modified = date_str.parseDate(globalThis); + + return S3Stat.new(.{ + .size = size, + .etag = bun.String.createUTF8(etag), + .contentType = bun.String.createUTF8(contentType), + .lastModified = last_modified, + }); + } + + pub fn getSize(this: *@This(), _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsNumber(this.size); + } + + pub fn getEtag(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.etag.toJS(globalObject); + } + + pub fn getContentType(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.contentType.toJS(globalObject); + } + + pub fn getLastModified(this: *@This(), globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.fromDateNumber(globalObject, this.lastModified); + } + + pub fn finalize(this: *@This()) void { + this.etag.deref(); + this.contentType.deref(); + this.destroy(); + } +}; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 37b1b04942..53d4c87113 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1010,7 +1010,7 @@ pub const Blob = struct { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), + @truncate(s3.options.partSize), ), ctx)) |stream| { return destination_blob.pipeReadableStreamToBlob(ctx, stream, options.extra_options); } else { @@ -1048,7 +1048,7 @@ pub const Blob = struct { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), + @truncate(s3.options.partSize), ), ctx)) |stream| { return S3.uploadStream( (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), @@ -1113,7 +1113,7 @@ pub const Blob = struct { if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob( ctx, source_blob, - @truncate(s3.options.partSize * S3.MultiPartUploadOptions.OneMiB), + @truncate(s3.options.partSize), ), ctx)) |stream| { return S3.uploadStream( (if (options.extra_options != null) aws_options.credentials.dupe() else s3.getCredentials()), @@ -3478,12 +3478,9 @@ pub const Blob = struct { pub fn unlink(this: *const FileStore, globalThis: *JSC.JSGlobalObject) JSValue { return switch (this.pathlike) { - .path => switch (globalThis.bunVM().nodeFS().unlink(.{ - .path = this.pathlike.path, - }, .sync)) { - .err => |err| JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)), - else => JSC.JSPromise.resolvedPromiseValue(globalThis, .true), - }, + .path => |path_like| JSC.Node.Async.unlink.create(globalThis, undefined, .{ + .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, + }, globalThis.bunVM()), .fd => JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Is not possible to unlink a file descriptor", .{})), }; } @@ -4738,11 +4735,26 @@ pub const Blob = struct { _ = Bun__Blob__getSizeForBindings; } } - - pub fn getSize(this: *Blob, globalThis: *JSC.JSGlobalObject) JSValue { + pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, callback: *JSC.CallFrame) JSC.JSValue { + const store = this.store orelse return JSC.JSValue.jsUndefined(); + // TODO: make this async for files + return switch (store.data) { + .file => |*file| { + return switch (file.pathlike) { + .path => |path_like| JSC.Node.Async.stat.create(globalThis, undefined, .{ + .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, + }, globalThis.bunVM()), + .fd => |fd| JSC.Node.Async.fstat.create(globalThis, undefined, .{ .fd = fd }, globalThis.bunVM()), + }; + }, + .s3 => S3File.getStat(this, globalThis, callback), + else => JSC.JSValue.jsUndefined(), + }; + } + pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) JSValue { if (this.size == Blob.max_size) { if (this.isS3()) { - return S3File.S3BlobStatTask.size(globalThis, this); + return JSC.JSValue.jsNumber(std.math.nan(f64)); } this.resolveSize(); if (this.size == Blob.max_size and this.store != null) { diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 97a471db0e..bbb6112950 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -173,6 +173,7 @@ export default [ size: { getter: "getSize", }, + stat: { fn: "getStat", length: 0 }, writer: { fn: "getWriter", diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 047012427f..341e8c01aa 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -529,7 +529,9 @@ pub const Response = struct { .url = bun.String.empty, }; - const result = blob.store.?.data.s3.getCredentials().signRequest(.{ + const credentials = blob.store.?.data.s3.getCredentials(); + + const result = credentials.signRequest(.{ .path = blob.store.?.data.s3.path(), .method = .GET, }, .{ .expires = 15 * 60 }) catch |sign_err| { diff --git a/src/s3/credentials.zig b/src/s3/credentials.zig index e053b681f3..d467373e32 100644 --- a/src/s3/credentials.zig +++ b/src/s3/credentials.zig @@ -160,11 +160,11 @@ pub const S3Credentials = struct { } } - if (try opts.getOptional(globalObject, "pageSize", i32)) |pageSize| { - if (pageSize < MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB and pageSize > MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB) { + if (try opts.getOptional(globalObject, "pageSize", i64)) |pageSize| { + if (pageSize < MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE and pageSize > MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE) { return globalObject.throwRangeError(pageSize, .{ - .min = @intCast(MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB), - .max = @intCast(MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB), + .min = @intCast(MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE), + .max = @intCast(MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE), .field_name = "pageSize", }); } else { @@ -437,6 +437,7 @@ pub const S3Credentials = struct { const method = signOptions.method; const request_path = signOptions.path; const content_hash = signOptions.content_hash; + const search_params = signOptions.search_params; var content_disposition = signOptions.content_disposition; @@ -553,11 +554,9 @@ pub const S3Credentials = struct { const protocol = if (this.insecure_http) "http" else "https"; // detect service name and host from region or endpoint - var encoded_host_buffer: [512]u8 = undefined; - var encoded_host: []const u8 = ""; const host = brk_host: { if (this.endpoint.len > 0) { - encoded_host = encodeURIComponent(this.endpoint, &encoded_host_buffer, true) catch return error.InvalidEndpoint; + if (this.endpoint.len >= 512) return error.InvalidEndpoint; break :brk_host try bun.default_allocator.dupe(u8, this.endpoint); } else { break :brk_host try std.fmt.allocPrint(bun.default_allocator, "s3.{s}.amazonaws.com", .{region}); @@ -599,15 +598,15 @@ pub const S3Credentials = struct { const canonical = brk_canonical: { if (acl) |acl_value| { if (encoded_session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Acl={s}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, acl_value, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash }); } } else { if (encoded_session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-Security-Token={s}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, token, host, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, if (encoded_host.len > 0) encoded_host else host, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={s}%2F{s}%2F{s}%2F{s}%2Faws4_request&X-Amz-Date={s}&X-Amz-Expires={}&X-Amz-SignedHeaders=host\nhost:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, this.accessKeyId, amz_day, region, service_name, amz_date, expires, host, signed_headers, aws_content_hash }); } } }; @@ -653,29 +652,29 @@ pub const S3Credentials = struct { if (acl) |acl_value| { if (content_disposition != null) { if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); } } else { if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, acl_value, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-acl:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, acl_value, aws_content_hash, amz_date, signed_headers, aws_content_hash }); } } } else { if (content_disposition != null) { if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\ncontent-disposition:{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", encoded_content_disposition, host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); } } else { if (session_token) |token| { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\nx-amz-security-token:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, aws_content_hash, amz_date, token, signed_headers, aws_content_hash }); } else { - break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", if (encoded_host.len > 0) encoded_host else host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); + break :brk_canonical try std.fmt.bufPrint(&tmp_buffer, "{s}\n{s}\n{s}\nhost:{s}\nx-amz-content-sha256:{s}\nx-amz-date:{s}\n\n{s}\n{s}", .{ method_name, normalizedPath, if (search_params) |p| p[1..] else "", host, aws_content_hash, amz_date, signed_headers, aws_content_hash }); } } } @@ -718,8 +717,8 @@ pub const S3Credentials = struct { ._headers = [_]picohttp.Header{ .{ .name = "x-amz-content-sha256", .value = aws_content_hash }, .{ .name = "x-amz-date", .value = amz_date }, - .{ .name = "Authorization", .value = authorization[0..] }, .{ .name = "Host", .value = host }, + .{ .name = "Authorization", .value = authorization[0..] }, .{ .name = "", .value = "" }, .{ .name = "", .value = "" }, .{ .name = "", .value = "" }, diff --git a/src/s3/multipart.zig b/src/s3/multipart.zig index 9b3faadf25..af5a37ebcb 100644 --- a/src/s3/multipart.zig +++ b/src/s3/multipart.zig @@ -11,9 +11,8 @@ const S3Error = @import("./error.zig").S3Error; pub const MultiPartUpload = struct { const OneMiB: usize = MultiPartUploadOptions.OneMiB; - const MAX_SINGLE_UPLOAD_SIZE_IN_MiB: usize = MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE_IN_MiB; // we limit to 5 GiB - const MAX_SINGLE_UPLOAD_SIZE: usize = MAX_SINGLE_UPLOAD_SIZE_IN_MiB * OneMiB; // we limit to 5 GiB - const MIN_SINGLE_UPLOAD_SIZE_IN_MiB: usize = MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE_IN_MiB; + const MAX_SINGLE_UPLOAD_SIZE: usize = MultiPartUploadOptions.MAX_SINGLE_UPLOAD_SIZE; // we limit to 5 GiB + const MIN_SINGLE_UPLOAD_SIZE: usize = MultiPartUploadOptions.MIN_SINGLE_UPLOAD_SIZE; const DefaultPartSize = MultiPartUploadOptions.DefaultPartSize; const MAX_QUEUE_SIZE = MultiPartUploadOptions.MAX_QUEUE_SIZE; const AWS = S3Credentials; @@ -496,7 +495,7 @@ pub const MultiPartUpload = struct { } pub fn partSizeInBytes(this: *@This()) usize { - return this.options.partSize * OneMiB; + return this.options.partSize; } pub fn continueStream(this: *@This()) void { diff --git a/src/s3/multipart_options.zig b/src/s3/multipart_options.zig index e99d18d09e..e84aa59b6b 100644 --- a/src/s3/multipart_options.zig +++ b/src/s3/multipart_options.zig @@ -1,9 +1,9 @@ pub const MultiPartUploadOptions = struct { pub const OneMiB: usize = 1048576; - pub const MAX_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5120; // we limit to 5 GiB - pub const MAX_SINGLE_UPLOAD_SIZE: usize = MAX_SINGLE_UPLOAD_SIZE_IN_MiB * OneMiB; // we limit to 5 GiB - pub const MIN_SINGLE_UPLOAD_SIZE_IN_MiB: usize = 5; - pub const DefaultPartSize = OneMiB * MIN_SINGLE_UPLOAD_SIZE_IN_MiB; + pub const MAX_SINGLE_UPLOAD_SIZE: usize = 5120 * OneMiB; // we limit to 5 GiB + pub const MIN_SINGLE_UPLOAD_SIZE: usize = 5 * OneMiB; + + pub const DefaultPartSize = MIN_SINGLE_UPLOAD_SIZE; pub const MAX_QUEUE_SIZE = 64; // dont make sense more than this because we use fetch anything greater will be 64 /// more than 255 dont make sense http thread cannot handle more than that @@ -16,7 +16,7 @@ pub const MultiPartUploadOptions = struct { /// }); /// See. https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property /// The value is in MiB min is 5 and max 5120 (but we limit to 4 GiB aka 4096) - partSize: u16 = 5, + partSize: u64 = DefaultPartSize, /// default is 3 max 255 retry: u8 = 3, }; diff --git a/src/s3/simple_request.zig b/src/s3/simple_request.zig index e78d7ecddb..d9a6891b7b 100644 --- a/src/s3/simple_request.zig +++ b/src/s3/simple_request.zig @@ -13,6 +13,10 @@ pub const S3StatResult = union(enum) { size: usize = 0, /// etag is not owned and need to be copied if used after this callback etag: []const u8 = "", + /// format: Mon, 06 Jan 2025 22:40:57 GMT, lastModified is not owned and need to be copied if used after this callback + lastModified: []const u8 = "", + /// format: text/plain, contentType is not owned and need to be copied if used after this callback + contentType: []const u8 = "", }, not_found: S3Error, @@ -222,6 +226,8 @@ pub const S3HttpSimpleTask = struct { callback(.{ .success = .{ .etag = response.headers.get("etag") orelse "", + .lastModified = response.headers.get("last-modified") orelse "", + .contentType = response.headers.get("content-type") orelse "", .size = if (response.headers.get("content-length")) |content_len| (std.fmt.parseInt(usize, content_len, 10) catch 0) else 0, }, }, this.callback_context); diff --git a/test/js/bun/s3/s3-insecure.test.ts b/test/js/bun/s3/s3-insecure.test.ts index ee62e37b68..d757fff77b 100644 --- a/test/js/bun/s3/s3-insecure.test.ts +++ b/test/js/bun/s3/s3-insecure.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "bun:test"; -import { S3, s3, file } from "bun"; +import { S3Client } from "bun"; describe("s3", async () => { it("should not fail to connect when endpoint is http and not https", async () => { @@ -15,14 +15,14 @@ describe("s3", async () => { }, }); - const s3 = new S3({ + const s3 = new S3Client({ accessKeyId: "test", secretAccessKey: "test", endpoint: server.url.href, bucket: "test", }); - const file = s3("hello.txt"); + const file = s3.file("hello.txt"); let err; try { await file.text(); diff --git a/test/js/bun/s3/s3.leak.test.ts b/test/js/bun/s3/s3.leak.test.ts index 9b25c622cb..4f81470722 100644 --- a/test/js/bun/s3/s3.leak.test.ts +++ b/test/js/bun/s3/s3.leak.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "bun:test"; import { bunExe, bunEnv, getSecret, tempDirWithFiles } from "harness"; -import type { S3FileOptions } from "bun"; +import type { S3Options } from "bun"; import path from "path"; -const s3Options: S3FileOptions = { +const s3Options: S3Options = { accessKeyId: getSecret("S3_R2_ACCESS_KEY"), secretAccessKey: getSecret("S3_R2_SECRET_KEY"), endpoint: getSecret("S3_R2_ENDPOINT"), diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index 6aa06c93a6..a991031980 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -1,933 +1,1060 @@ import { describe, expect, it, beforeAll, afterAll } from "bun:test"; -import { bunExe, bunEnv, getSecret, tempDirWithFiles } from "harness"; +import { bunExe, bunEnv, getSecret, tempDirWithFiles, isLinux } from "harness"; import { randomUUID } from "crypto"; -import { S3, s3, file } from "bun"; -import type { S3File, S3FileOptions } from "bun"; +import { S3Client, s3, file, which } from "bun"; +const S3 = (...args) => new S3Client(...args); +import child_process from "child_process"; +import type { S3Options } from "bun"; import path from "path"; -const s3Options: S3FileOptions = { - accessKeyId: getSecret("S3_R2_ACCESS_KEY"), - secretAccessKey: getSecret("S3_R2_SECRET_KEY"), - endpoint: getSecret("S3_R2_ENDPOINT"), -}; -const S3Bucket = getSecret("S3_R2_BUCKET"); - -function makePayLoadFrom(text: string, size: number): string { - while (Buffer.byteLength(text) < size) { - text += text; +const dockerCLI = which("docker") as string; +function isDockerEnabled(): boolean { + if (!dockerCLI) { + return false; + } + + try { + const info = child_process.execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] }); + return info.toString().indexOf("Server Version:") !== -1; + } catch (error) { + return false; } - return text.slice(0, size); } -// 10 MiB big enough to Multipart upload in more than one part -const bigPayload = makePayLoadFrom("Bun is the best runtime ever", 10 * 1024 * 1024); -const bigishPayload = makePayLoadFrom("Bun is the best runtime ever", 1 * 1024 * 1024); +const allCredentials = [ + { + accessKeyId: getSecret("S3_R2_ACCESS_KEY"), + secretAccessKey: getSecret("S3_R2_SECRET_KEY"), + endpoint: getSecret("S3_R2_ENDPOINT"), + bucket: getSecret("S3_R2_BUCKET"), + service: "R2" as string, + }, +]; -describe.skipIf(!s3Options.accessKeyId)("s3", () => { - for (let bucketInName of [true, false]) { - describe("fetch", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - var tmp_filename: string; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - s3: options, - }); - expect(result.status).toBe(200); - }); +// TODO: figure out why minio is not creating a bucket on Linux, works on macOS and windows +if (isDockerEnabled() && !isLinux) { + const minio_dir = tempDirWithFiles("minio", {}); + const result = child_process.spawnSync( + "docker", + [ + "run", + "-d", + "--name", + "minio", + "-p", + "9000:9000", + "-p", + "9001:9001", + "-e", + "MINIO_ROOT_USER=minioadmin", + "-e", + "MINIO_ROOT_PASSWORD=minioadmin", + "-v", + `${minio_dir}:/data`, + "minio/minio", + "server", + "--console-address", + ":9001", + "/data", + ], + { + stdio: ["ignore", "pipe", "pipe"], + }, + ); - afterAll(async () => { - const result = await fetch(tmp_filename, { - method: "DELETE", - s3: options, - }); - expect(result.status).toBe(204); - }); + if (result.error) { + if (!result.error.message.includes('The container name "/minio" is already in use by container')) + throw result.error; + } + // wait for minio to be ready + await Bun.sleep(1_000); - it("should download file via fetch GET", async () => { - const result = await fetch(tmp_filename, { s3: options }); - expect(result.status).toBe(200); - expect(await result.text()).toBe("Hello Bun!"); - }); + /// create a bucket + child_process.spawnSync(dockerCLI, [`exec`, `minio`, `mc`, `mb`, `http://localhost:9000/buntest`], { + stdio: "ignore", + }); - it("should download range", async () => { - const result = await fetch(tmp_filename, { - headers: { "range": "bytes=6-10" }, - s3: options, - }); - expect(result.status).toBe(206); - expect(await result.text()).toBe("Bun!"); - }); + allCredentials.push({ + endpoint: "http://localhost:9000", // MinIO endpoint + accessKeyId: "minioadmin", + secretAccessKey: "minioadmin", + bucket: "buntest", + service: "MinIO" as string, + }); +} +for (let credentials of allCredentials) { + describe(`${credentials.service}`, () => { + const s3Options: S3Options = { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + endpoint: credentials.endpoint, + }; - it("should check if a key exists or content-length", async () => { - const result = await fetch(tmp_filename, { - method: "HEAD", - s3: options, - }); - expect(result.status).toBe(200); // 404 if do not exists - expect(result.headers.get("content-length")).toBe("10"); // content-length - }); + const S3Bucket = credentials.bucket; - it("should check if a key does not exist", async () => { - const result = await fetch(tmp_filename + "-does-not-exist", { s3: options }); - expect(result.status).toBe(404); - }); + function makePayLoadFrom(text: string, size: number): string { + while (Buffer.byteLength(text) < size) { + text += text; + } + return text.slice(0, size); + } - it("should be able to set content-type", async () => { - { - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - headers: { - "Content-Type": "application/json", - }, - s3: options, - }); - expect(result.status).toBe(200); - const response = await fetch(tmp_filename, { s3: options }); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } - { - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - headers: { - "Content-Type": "text/plain", - }, - s3: options, - }); - expect(result.status).toBe(200); - const response = await fetch(tmp_filename, { s3: options }); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } - }); + // 10 MiB big enough to Multipart upload in more than one part + const bigPayload = makePayLoadFrom("Bun is the best runtime ever", 10 * 1024 * 1024); + const bigishPayload = makePayLoadFrom("Bun is the best runtime ever", 1 * 1024 * 1024); - it("should be able to upload large files", async () => { - // 10 MiB big enough to Multipart upload in more than one part - const buffer = Buffer.alloc(1 * 1024 * 1024, "a"); - { - await fetch(tmp_filename, { - method: "PUT", - body: async function* () { - for (let i = 0; i < 10; i++) { - await Bun.sleep(10); - yield buffer; - } - }, - s3: options, - }).then(res => res.text()); - - const result = await fetch(tmp_filename, { method: "HEAD", s3: options }); - expect(result.status).toBe(200); - expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); - } - }, 20_000); - }); - }); - - describe("Bun.S3", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; - const options = bucketInName ? null : { bucket: S3Bucket }; - - var bucket = S3(s3Options); - beforeAll(async () => { - const file = bucket(tmp_filename, options); - await file.write("Hello Bun!"); - }); - - afterAll(async () => { - const file = bucket(tmp_filename, options); - await file.unlink(); - }); - - it("should download file via Bun.s3().text()", async () => { - const file = bucket(tmp_filename, options); - const text = await file.text(); - expect(text).toBe("Hello Bun!"); - }); - - it("should download range", async () => { - const file = bucket(tmp_filename, options); - const text = await file.slice(6, 10).text(); - expect(text).toBe("Bun!"); - }); - - it("should check if a key exists or content-length", async () => { - const file = bucket(tmp_filename, options); - const exists = await file.exists(); - expect(exists).toBe(true); - const contentLength = await file.size; - expect(contentLength).toBe(10); - }); - - it("should check if a key does not exist", async () => { - const file = bucket(tmp_filename + "-does-not-exist", options); - const exists = await file.exists(); - expect(exists).toBe(false); - }); - - it("should be able to set content-type", async () => { - { - const s3file = bucket(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/css" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); - } - { - const s3file = bucket(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } - - { - const s3file = bucket(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } - - { - await bucket.write(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); - const response = await fetch(bucket(tmp_filename, options).presign()); - expect(response.headers.get("content-type")).toStartWith("application/xml"); - } - }); - - it("should be able to upload large files using bucket.write + readable Request", async () => { - { - await bucket.write( - tmp_filename, - new Request("https://example.com", { + describe.skipIf(!s3Options.accessKeyId)("s3", () => { + for (let bucketInName of [true, false]) { + describe("fetch", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + var tmp_filename: string; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const result = await fetch(tmp_filename, { method: "PUT", - body: async function* () { - for (let i = 0; i < 10; i++) { - if (i % 5 === 0) { + body: "Hello Bun!", + s3: options, + }); + expect(result.status).toBe(200); + }); + + afterAll(async () => { + const result = await fetch(tmp_filename, { + method: "DELETE", + s3: options, + }); + expect(result.status).toBe(204); + }); + + it("should download file via fetch GET", async () => { + const result = await fetch(tmp_filename, { s3: options }); + expect(result.status).toBe(200); + expect(await result.text()).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const result = await fetch(tmp_filename, { + headers: { "range": "bytes=6-10" }, + s3: options, + }); + expect(result.status).toBe(206); + expect(await result.text()).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const result = await fetch(tmp_filename, { + method: "HEAD", + s3: options, + }); + expect(result.status).toBe(200); // 404 if do not exists + expect(result.headers.get("content-length")).toBe("10"); // content-length + }); + + it("should check if a key does not exist", async () => { + const result = await fetch(tmp_filename + "-does-not-exist", { s3: options }); + expect(result.status).toBe(404); + }); + + it("should be able to set content-type", async () => { + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "application/json", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "text/plain", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + }); + + it("should be able to upload large files", async () => { + // 10 MiB big enough to Multipart upload in more than one part + const buffer = Buffer.alloc(1 * 1024 * 1024, "a"); + { + await fetch(tmp_filename, { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { await Bun.sleep(10); + yield buffer; } - yield bigishPayload; - } - }, - }), - options, - ); - expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); - } - }, 10_000); + }, + s3: options, + }).then(res => res.text()); - it("should be able to upload large files in one go using bucket.write", async () => { - { - await bucket.write(tmp_filename, bigPayload, options); - expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await bucket(tmp_filename, options).text()).toBe(bigPayload); - } - }, 10_000); + const result = await fetch(tmp_filename, { method: "HEAD", s3: options }); + expect(result.status).toBe(200); + expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); + } + }, 20_000); + }); + }); - it("should be able to upload large files in one go using S3File.write", async () => { - { - const s3File = bucket(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); - } - }, 10_000); - }); - }); + describe("Bun.S3Client", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? null : { bucket: S3Bucket }; - describe("Bun.file", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - const s3file = file(tmp_filename, options); + var bucket = S3(s3Options); + beforeAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.write("Hello Bun!"); + }); + + afterAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.unlink(); + }); + + it("should download file via Bun.s3().text()", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const file = bucket.file(tmp_filename, options); + const exists = await file.exists(); + expect(exists).toBe(true); + const stat = await file.stat(); + expect(stat.size).toBe(10); + }); + + it("should check if a key does not exist", async () => { + const file = bucket.file(tmp_filename + "-does-not-exist", options); + const exists = await file.exists(); + expect(exists).toBe(false); + }); + + it("should be able to set content-type", async () => { + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/css" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = bucket.file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + + { + await bucket.write(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); + const response = await fetch(bucket.file(tmp_filename, options).presign()); + expect(response.headers.get("content-type")).toStartWith("application/xml"); + } + }); + + it("should be able to upload large files using bucket.write + readable Request", async () => { + { + await bucket.write( + tmp_filename, + new Request("https://example.com", { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { + if (i % 5 === 0) { + await Bun.sleep(10); + } + yield bigishPayload; + } + }, + }), + options, + ); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); + } + }, 10_000); + + it("should be able to upload large files in one go using bucket.write", async () => { + { + await bucket.write(tmp_filename, bigPayload, options); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await bucket.file(tmp_filename, options).text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = bucket.file(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + }); + }); + + describe("Bun.file", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); + + afterAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.unlink(); + }); + + it("should download file via Bun.file().text()", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const s3file = file(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + }); + + it("should check if a key does not exist", async () => { + const s3file = file(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); + + it("should be able to set content-type", async () => { + { + const s3file = file(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); + + it("should be able to upload large files in one go using Bun.write", async () => { + { + await Bun.write(file(tmp_filename, options), bigPayload); + expect(await S3Client.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await file(tmp_filename, options).text()).toEqual(bigPayload); + } + }, 15_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = file(tmp_filename, options); + await s3File.write(bigPayload); + expect(s3File.size).toBeNaN(); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + }); + }); + + describe("Bun.s3", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); + + afterAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.unlink(); + }); + + it("should download file via Bun.s3().text()", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const s3file = s3(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + expect(s3file.size).toBeNaN(); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + }); + + it("should check if a key does not exist", async () => { + const s3file = s3(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); + + it("presign url", async () => { + const s3file = s3(tmp_filename, options); + const response = await fetch(s3file.presign()); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun!"); + }); + + it("should be able to set content-type", async () => { + { + const s3file = s3(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = s3(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); + + it("should be able to upload large files in one go using Bun.write", async () => { + { + const s3file = s3(tmp_filename, options); + await Bun.write(s3file, bigPayload); + const stat = await s3file.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + expect(await s3file.text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = s3(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); + + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + + describe("readable stream", () => { + afterAll(async () => { + await Promise.all([ + s3(tmp_filename + "-readable-stream", options).unlink(), + s3(tmp_filename + "-readable-stream-big", options).unlink(), + ]); + }); + it("should work with small files", async () => { + const s3file = s3(tmp_filename + "-readable-stream", options); + await s3file.write("Hello Bun!"); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(10); + expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello Bun!")); + }); + it("should work with large files ", async () => { + const s3file = s3(tmp_filename + "-readable-stream-big", options); + await s3file.write(bigishPayload); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(Buffer.byteLength(bigishPayload)); + expect(Buffer.concat(chunks).toString()).toBe(bigishPayload); + }, 30_000); + }); + }); + }); + } + describe("special characters", () => { + it("should allow special characters in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`🌈🦄${randomUUID()}.txt`, options); await s3file.write("Hello Bun!"); - }); - - afterAll(async () => { - const s3file = file(tmp_filename, options); + await s3file.exists(); await s3file.unlink(); + expect().pass(); }); - - it("should download file via Bun.file().text()", async () => { - const s3file = file(tmp_filename, options); - const text = await s3file.text(); - expect(text).toBe("Hello Bun!"); + it("should allow forward slashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}/test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); }); - - it("should download range", async () => { - const s3file = file(tmp_filename, options); - const text = await s3file.slice(6, 10).text(); - expect(text).toBe("Bun!"); + it("should allow backslashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}\\test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); }); - - it("should check if a key exists or content-length", async () => { - const s3file = file(tmp_filename, options); - const exists = await s3file.exists(); - expect(exists).toBe(true); - const contentLength = await s3file.size; - expect(contentLength).toBe(10); - }); - - it("should check if a key does not exist", async () => { - const s3file = file(tmp_filename + "-does-not-exist", options); - const exists = await s3file.exists(); - expect(exists).toBe(false); - }); - - it("should be able to set content-type", async () => { + it("should allow starting with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; { - const s3file = file(tmp_filename, { ...options, type: "text/css" }); + const s3file = s3(`/${randomUUID()}test.txt`, options); await s3file.write("Hello Bun!"); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); + await s3file.unlink(); } { - const s3file = file(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } - - { - const s3file = file(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); + const s3file = s3(`\\${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } + expect().pass(); }); - it("should be able to upload large files in one go using Bun.write", async () => { + it("should allow ending with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; { - await Bun.write(file(tmp_filename, options), bigPayload); - expect(await s3(tmp_filename, options).size).toBe(Buffer.byteLength(bigPayload)); - expect(await file(tmp_filename, options).text()).toEqual(bigPayload); + const s3file = s3(`${randomUUID()}/`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } - }, 15_000); - - it("should be able to upload large files in one go using S3File.write", async () => { { - const s3File = file(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); + const s3file = s3(`${randomUUID()}\\`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } - }, 10_000); + expect().pass(); + }); }); - }); - describe("Bun.s3", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - const s3file = s3(tmp_filename, options); - await s3file.write("Hello Bun!"); + describe("static methods", () => { + it("its defined", () => { + expect(S3Client).toBeDefined(); + expect(S3Client.write).toBeDefined(); + expect(S3Client.file).toBeDefined(); + expect(S3Client.stat).toBeDefined(); + expect(S3Client.unlink).toBeDefined(); + expect(S3Client.exists).toBeDefined(); + expect(S3Client.presign).toBeDefined(); + expect(S3Client.size).toBeDefined(); + expect(S3Client.delete).toBeDefined(); }); - - afterAll(async () => { - const s3file = s3(tmp_filename, options); - await s3file.unlink(); - }); - - it("should download file via Bun.s3().text()", async () => { - const s3file = s3(tmp_filename, options); - const text = await s3file.text(); - expect(text).toBe("Hello Bun!"); - }); - - it("should download range", async () => { - const s3file = s3(tmp_filename, options); - const text = await s3file.slice(6, 10).text(); - expect(text).toBe("Bun!"); - }); - - it("should check if a key exists or content-length", async () => { - const s3file = s3(tmp_filename, options); - const exists = await s3file.exists(); - expect(exists).toBe(true); - const contentLength = await s3file.size; - expect(contentLength).toBe(10); - }); - - it("should check if a key does not exist", async () => { - const s3file = s3(tmp_filename + "-does-not-exist", options); - const exists = await s3file.exists(); - expect(exists).toBe(false); - }); - - it("presign url", async () => { - const s3file = s3(tmp_filename, options); - const response = await fetch(s3file.presign()); + it("should work", async () => { + const filename = randomUUID() + ".txt"; + await S3Client.write(filename, "Hello Bun!", { ...s3Options, bucket: S3Bucket }); + expect(await S3Client.file(filename, { ...s3Options, bucket: S3Bucket }).text()).toBe("Hello Bun!"); + const stat = await S3Client.stat(filename, { ...s3Options, bucket: S3Bucket }); + expect(stat.size).toBe(10); + expect(stat.etag).toBeString(); + expect(stat.lastModified).toBeValidDate(); + expect(stat.type).toBe("text/plain;charset=utf-8"); + const url = S3Client.presign(filename, { ...s3Options, bucket: S3Bucket }); + expect(url).toBeDefined(); + const response = await fetch(url); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello Bun!"); - }); - - it("should be able to set content-type", async () => { - { - const s3file = s3(tmp_filename, { ...options, type: "text/css" }); - await s3file.write("Hello Bun!"); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); - } - { - const s3file = s3(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } - - { - const s3file = s3(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } - }); - - it("should be able to upload large files in one go using Bun.write", async () => { - { - const s3file = s3(tmp_filename, options); - await Bun.write(s3file, bigPayload); - expect(await s3file.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3file.text()).toBe(bigPayload); - } - }, 10_000); - - it("should be able to upload large files in one go using S3File.write", async () => { - { - const s3File = s3(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); - } - }, 10_000); - - describe("readable stream", () => { - afterAll(async () => { - await Promise.all([ - s3(tmp_filename + "-readable-stream", options).unlink(), - s3(tmp_filename + "-readable-stream-big", options).unlink(), - ]); - }); - it("should work with small files", async () => { - const s3file = s3(tmp_filename + "-readable-stream", options); - await s3file.write("Hello Bun!"); - const stream = s3file.stream(); - const reader = stream.getReader(); - let bytes = 0; - let chunks: Array = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - bytes += value?.length ?? 0; - - if (value) chunks.push(value as Buffer); - } - expect(bytes).toBe(10); - expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello Bun!")); - }); - it("should work with large files ", async () => { - const s3file = s3(tmp_filename + "-readable-stream-big", options); - await s3file.write(bigishPayload); - const stream = s3file.stream(); - const reader = stream.getReader(); - let bytes = 0; - let chunks: Array = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - bytes += value?.length ?? 0; - if (value) chunks.push(value as Buffer); - } - expect(bytes).toBe(Buffer.byteLength(bigishPayload)); - expect(Buffer.concat(chunks).toString()).toBe(bigishPayload); - }, 30_000); + await S3Client.unlink(filename, { ...s3Options, bucket: S3Bucket }); + expect().pass(); }); }); - }); - } - describe("special characters", () => { - it("should allow special characters in the path", async () => { - const options = { ...s3Options, bucket: S3Bucket }; - const s3file = s3(`🌈🦄${randomUUID()}.txt`, options); - await s3file.write("Hello Bun!"); - await s3file.exists(); - await s3file.unlink(); - expect().pass(); - }); - it("should allow forward slashes in the path", async () => { - const options = { ...s3Options, bucket: S3Bucket }; - const s3file = s3(`${randomUUID()}/test.txt`, options); - await s3file.write("Hello Bun!"); - await s3file.exists(); - await s3file.unlink(); - expect().pass(); - }); - it("should allow backslashes in the path", async () => { - const options = { ...s3Options, bucket: S3Bucket }; - const s3file = s3(`${randomUUID()}\\test.txt`, options); - await s3file.write("Hello Bun!"); - await s3file.exists(); - await s3file.unlink(); - expect().pass(); - }); - it("should allow starting with slashs and backslashes", async () => { - const options = { ...s3Options, bucket: S3Bucket }; - { - const s3file = s3(`/${randomUUID()}test.txt`, options); - await s3file.write("Hello Bun!"); - await s3file.unlink(); - } - { - const s3file = s3(`\\${randomUUID()}test.txt`, options); - await s3file.write("Hello Bun!"); - await s3file.unlink(); - } - expect().pass(); - }); + describe("errors", () => { + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file("./do-not-exist.txt")); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ENOENT"); + expect(e?.path).toBe("./do-not-exist.txt"); + expect(e?.syscall).toBe("open"); + } + }); - it("should allow ending with slashs and backslashes", async () => { - const options = { ...s3Options, bucket: S3Bucket }; - { - const s3file = s3(`${randomUUID()}/`, options); - await s3file.write("Hello Bun!"); - await s3file.unlink(); - } - { - const s3file = s3(`${randomUUID()}\\`, options); - await s3file.write("Hello Bun!"); - await s3file.unlink(); - } - expect().pass(); - }); - }); - describe("errors", () => { - it("Bun.write(s3file, file) should throw if the file does not exist", async () => { - try { - await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file("./do-not-exist.txt")); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ENOENT"); - expect(e?.path).toBe("./do-not-exist.txt"); - expect(e?.syscall).toBe("open"); - } - }); - - it("Bun.write(s3file, file) should work with empty file", async () => { - const dir = tempDirWithFiles("fsr", { - "hello.txt": "", - }); - await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file(path.join(dir, "hello.txt"))); - }); - it("Bun.write(s3file, file) should throw if the file does not exist", async () => { - try { - await Bun.write( - s3("test.txt", { ...s3Options, bucket: S3Bucket }), - s3("do-not-exist.txt", { ...s3Options, bucket: S3Bucket }), - ); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("NoSuchKey"); - expect(e?.path).toBe("do-not-exist.txt"); - expect(e?.name).toBe("S3Error"); - } - }); - it("Bun.write(s3file, file) should throw if the file does not exist", async () => { - try { - await Bun.write( - s3("test.txt", { ...s3Options, bucket: S3Bucket }), - s3("do-not-exist.txt", { ...s3Options, bucket: "does-not-exists" }), - ); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("AccessDenied"); - expect(e?.path).toBe("do-not-exist.txt"); - expect(e?.name).toBe("S3Error"); - } - }); - it("should error if bucket is missing", async () => { - try { - await Bun.write(s3("test.txt", s3Options), "Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_S3_INVALID_PATH"); - expect(e?.name).toBe("S3Error"); - } - }); - - it("should error if bucket is missing on payload", async () => { - try { - await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), s3("test2.txt", s3Options)); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_S3_INVALID_PATH"); - expect(e?.path).toBe("test2.txt"); - expect(e?.name).toBe("S3Error"); - } - }); - - it("should error when invalid method", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path)].map(async fn => { - const s3file = fn("method-test", { - ...s3Options, - bucket: S3Bucket, + it("Bun.write(s3file, file) should work with empty file", async () => { + const dir = tempDirWithFiles("fsr", { + "hello.txt": "", }); - + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file(path.join(dir, "hello.txt"))); + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { try { - await s3file.presign({ method: "OPTIONS" }); + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: S3Bucket }), + ); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBe("ERR_S3_INVALID_METHOD"); - } - }), - ); - }); - - it("should error when path is too long", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path)].map(async fn => { - try { - const s3file = fn("test" + "a".repeat(4096), { - ...s3Options, - bucket: S3Bucket, - }); - - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(["ENAMETOOLONG", "ERR_S3_INVALID_PATH"]).toContain(e?.code); - } - }), - ); - }); - }); - describe("credentials", () => { - it("should error with invalid access key id", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - accessKeyId: "invalid", - }); - - try { - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("InvalidArgument"); - } - }), - ); - }); - it("should error with invalid secret key id", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - secretAccessKey: "invalid", - }); - try { - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("AccessDenied"); - } - }), - ); - }); - - it("should error with invalid endpoint", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - try { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "🙂.🥯", - }); - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); - } - }), - ); - }); - it("should error with invalid endpoint", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - try { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, // credentials and endpoint dont match - endpoint: "s3.us-west-1.amazonaws.com", - }); - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("PermanentRedirect"); - } - }), - ); - }); - it("should error with invalid endpoint", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - try { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "..asd.@%&&&%%", - }); - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); - } - }), - ); - }); - - it("should error with invalid bucket", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://credentials-test", { - ...s3Options, - bucket: "invalid", - }); - - try { - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("AccessDenied"); + expect(e?.code).toBe("NoSuchKey"); + expect(e?.path).toBe("do-not-exist.txt"); expect(e?.name).toBe("S3Error"); } - }), - ); - }); - - it("should error when missing credentials", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path), file].map(async fn => { - const s3file = fn("s3://credentials-test", { - bucket: "invalid", - }); - + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { try { - await s3file.write("Hello Bun!"); + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: "does-not-exists" }), + ); expect.unreachable(); } catch (e: any) { - expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); } - }), - ); - }); - it("should error when presign missing credentials", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path)].map(async fn => { - const s3file = fn("method-test", { + }); + it("should error if bucket is missing", async () => { + try { + await Bun.write(s3("test.txt", s3Options), "Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error if bucket is missing on payload", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), s3("test2.txt", s3Options)); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.path).toBe("test2.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + + it("should error when invalid method", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + ...s3Options, + bucket: S3Bucket, + }); + + try { + await s3file.presign({ method: "OPTIONS" }); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_METHOD"); + } + }), + ); + }); + + it("should error when path is too long", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + try { + const s3file = fn("test" + "a".repeat(4096), { + ...s3Options, + bucket: S3Bucket, + }); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["ENAMETOOLONG", "ERR_S3_INVALID_PATH"]).toContain(e?.code); + } + }), + ); + }); + }); + describe("credentials", () => { + it("should error with invalid access key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + accessKeyId: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["InvalidAccessKeyId", "InvalidArgument"]).toContain(e?.code); + } + }), + ); + }); + it("should error with invalid secret key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + secretAccessKey: "invalid", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["SignatureDoesNotMatch", "AccessDenied"]).toContain(e?.code); + } + }), + ); + }); + + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "🙂.🥯", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, // credentials and endpoint dont match + endpoint: "s3.us-west-1.amazonaws.com", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("PermanentRedirect"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "..asd.@%&&&%%", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); + }); + + it("should error with invalid bucket", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + ...s3Options, + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.name).toBe("S3Error"); + } + }), + ); + }); + + it("should error when missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + bucket: "invalid", + }); + + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + it("should error when presign missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + bucket: S3Bucket, + }); + + try { + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); + }); + + it("should error when presign with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.endpoint = Buffer.alloc(1024, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_ENDPOINT"); + } + }), + ); + }); + it("should error when presign with invalid token", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.sessionToken = Buffer.alloc(4096, "a").toString(); + + try { + const s3file = fn(randomUUID(), options); + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_SESSION_TOKEN"); + } + }), + ); + }); + }); + + describe("S3 static methods", () => { + describe("presign", () => { + it("should work", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint and region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = undefined; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-east-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint + region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = "us-west-1"; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-west-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with expires", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with acl", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + acl: "public-read", + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Acl=public-read")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + endpoint: "https://s3.bun.sh", + }); + expect(url).toBeDefined(); + expect(url.includes("https://s3.bun.sh")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://folder/credentials-test", s3Options).presign({ + expiresIn: 10, + bucket: "my-bucket", + }); + expect(url).toBeDefined(); + expect(url.includes("my-bucket")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + }); + + it("exists, write, size, unlink should work", async () => { + const fullPath = randomUUID(); + const bucket = S3({ + ...s3Options, bucket: S3Bucket, }); + expect(await bucket.exists(fullPath)).toBe(false); - try { - await s3file.presign(); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); - } - }), - ); - }); + await bucket.write(fullPath, "bun"); + expect(await bucket.exists(fullPath)).toBe(true); + expect(await bucket.size(fullPath)).toBe(3); + await bucket.unlink(fullPath); + expect(await bucket.exists(fullPath)).toBe(false); + }); - it("should error when presign with invalid endpoint", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path)].map(async fn => { - let options = { ...s3Options, bucket: S3Bucket }; - options.endpoint = Buffer.alloc(1024, "a").toString(); + it("should be able to upload a slice", async () => { + const filename = randomUUID(); + const fullPath = `s3://${S3Bucket}/${filename}`; + const s3file = s3(fullPath, s3Options); + await s3file.write("Hello Bun!"); + const slice = s3file.slice(6, 10); + expect(await slice.text()).toBe("Bun!"); + expect(await s3file.text()).toBe("Hello Bun!"); - try { - const s3file = fn(randomUUID(), options); - - await s3file.write("Hello Bun!"); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_S3_INVALID_ENDPOINT"); - } - }), - ); - }); - it("should error when presign with invalid token", async () => { - await Promise.all( - [s3, (path, ...args) => S3(...args)(path)].map(async fn => { - let options = { ...s3Options, bucket: S3Bucket }; - options.sessionToken = Buffer.alloc(4096, "a").toString(); - - try { - const s3file = fn(randomUUID(), options); - await s3file.presign(); - expect.unreachable(); - } catch (e: any) { - expect(e?.code).toBe("ERR_S3_INVALID_SESSION_TOKEN"); - } - }), - ); + await s3file.write(slice); + const text = await s3file.text(); + expect(text).toBe("Bun!"); + await s3file.unlink(); + }); + }); }); }); - - describe("S3 static methods", () => { - describe("presign", () => { - it("should work", async () => { - const s3file = s3("s3://bucket/credentials-test", s3Options); - const url = s3file.presign(); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=86400")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - it("default endpoint and region should work", async () => { - let options = { ...s3Options }; - options.endpoint = undefined; - options.region = undefined; - const s3file = s3("s3://bucket/credentials-test", options); - const url = s3file.presign(); - expect(url).toBeDefined(); - expect(url.includes("https://s3.us-east-1.amazonaws.com")).toBe(true); - expect(url.includes("X-Amz-Expires=86400")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - it("default endpoint + region should work", async () => { - let options = { ...s3Options }; - options.endpoint = undefined; - options.region = "us-west-1"; - const s3file = s3("s3://bucket/credentials-test", options); - const url = s3file.presign(); - expect(url).toBeDefined(); - expect(url.includes("https://s3.us-west-1.amazonaws.com")).toBe(true); - expect(url.includes("X-Amz-Expires=86400")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - it("should work with expires", async () => { - const s3file = s3("s3://bucket/credentials-test", s3Options); - const url = s3file.presign({ - expiresIn: 10, - }); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - it("should work with acl", async () => { - const s3file = s3("s3://bucket/credentials-test", s3Options); - const url = s3file.presign({ - expiresIn: 10, - acl: "public-read", - }); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Acl=public-read")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - - it("s3().presign() should work", async () => { - const url = s3("s3://bucket/credentials-test", s3Options).presign({ - expiresIn: 10, - }); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - - it("s3().presign() endpoint should work", async () => { - const url = s3("s3://bucket/credentials-test", s3Options).presign({ - expiresIn: 10, - endpoint: "https://s3.bun.sh", - }); - expect(url).toBeDefined(); - expect(url.includes("https://s3.bun.sh")).toBe(true); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - - it("s3().presign() endpoint should work", async () => { - const url = s3("s3://folder/credentials-test", s3Options).presign({ - expiresIn: 10, - bucket: "my-bucket", - }); - expect(url).toBeDefined(); - expect(url.includes("my-bucket")).toBe(true); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - }); - - it("exists, write, size, unlink should work", async () => { - const fullPath = randomUUID(); - const bucket = S3({ - ...s3Options, - bucket: S3Bucket, - }); - expect(await bucket.exists(fullPath)).toBe(false); - - await bucket.write(fullPath, "bun"); - expect(await bucket.exists(fullPath)).toBe(true); - expect(await bucket.size(fullPath)).toBe(3); - await bucket.unlink(fullPath); - expect(await bucket.exists(fullPath)).toBe(false); - }); - - it("should be able to upload a slice", async () => { - const filename = randomUUID(); - const fullPath = `s3://${S3Bucket}/${filename}`; - const s3file = s3(fullPath, s3Options); - await s3file.write("Hello Bun!"); - const slice = s3file.slice(6, 10); - expect(await slice.text()).toBe("Bun!"); - expect(await s3file.text()).toBe("Hello Bun!"); - - await s3file.write(slice); - const text = await s3file.text(); - expect(text).toBe("Bun!"); - await s3file.unlink(); - }); - }); -}); +} From c431ef1b7a3512a55174c61c91eb24088230d7c2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 7 Jan 2025 00:34:58 -0800 Subject: [PATCH 55/56] Replace 4 duplicate implementations of getting the sourceURL (#16205) --- src/bun.js/bindings/CallSite.h | 1 + src/bun.js/bindings/ErrorStackTrace.cpp | 136 ++++++++++++++---- src/bun.js/bindings/ErrorStackTrace.h | 9 ++ .../bindings/InspectorTestReporterAgent.cpp | 18 +-- src/bun.js/bindings/ZigGlobalObject.cpp | 78 +++++----- src/bun.js/bindings/bindings.cpp | 28 +--- test/js/node/v8/capture-stack-trace.test.js | 29 +++- 7 files changed, 185 insertions(+), 114 deletions(-) diff --git a/src/bun.js/bindings/CallSite.h b/src/bun.js/bindings/CallSite.h index 35c2e42174..8dac8702b1 100644 --- a/src/bun.js/bindings/CallSite.h +++ b/src/bun.js/bindings/CallSite.h @@ -80,6 +80,7 @@ public: void setLineNumber(OrdinalNumber lineNumber) { m_lineNumber = lineNumber; } void setColumnNumber(OrdinalNumber columnNumber) { m_columnNumber = columnNumber; } + void setSourceURL(JSC::VM& vm, JSC::JSString* sourceURL) { m_sourceURL.set(vm, this, sourceURL); } void formatAsString(JSC::VM& vm, JSC::JSGlobalObject* globalObject, WTF::StringBuilder& sb); diff --git a/src/bun.js/bindings/ErrorStackTrace.cpp b/src/bun.js/bindings/ErrorStackTrace.cpp index c52da44fc7..19b5ff9ef5 100644 --- a/src/bun.js/bindings/ErrorStackTrace.cpp +++ b/src/bun.js/bindings/ErrorStackTrace.cpp @@ -389,13 +389,7 @@ static bool isVisibleBuiltinFunction(JSC::CodeBlock* codeBlock) } const JSC::SourceCode& source = codeBlock->source(); - if (auto* provider = source.provider()) { - const auto& url = provider->sourceURL(); - if (!url.isEmpty()) { - return true; - } - } - return false; + return !Zig::sourceURL(source).isEmpty(); } JSCStackFrame::JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor) @@ -512,45 +506,33 @@ JSCStackFrame::SourcePositions* JSCStackFrame::getSourcePositions() ALWAYS_INLINE String JSCStackFrame::retrieveSourceURL() { - static auto sourceURLWasmString = MAKE_STATIC_STRING_IMPL("[wasm code]"); - static auto sourceURLNativeString = MAKE_STATIC_STRING_IMPL("[native code]"); + static const auto sourceURLWasmString = MAKE_STATIC_STRING_IMPL("[wasm code]"); if (m_isWasmFrame) { return String(sourceURLWasmString); } + auto url = Zig::sourceURL(m_codeBlock); + if (!url.isEmpty()) { + return url; + } + if (m_callee && m_callee->isObject()) { if (auto* jsFunction = jsDynamicCast(m_callee)) { - if (auto* executable = jsFunction->executable()) { - if (!executable->isHostFunction()) { - auto* jsExectuable = jsFunction->jsExecutable(); - if (jsExectuable) { - const auto* sourceProvider = jsExectuable->source().provider(); - if (sourceProvider) { - return sourceProvider->sourceURL(); - } - } - } + WTF::String url = Zig::sourceURL(m_vm, jsFunction); + if (!url.isEmpty()) { + return url; } } } - if (!m_codeBlock) { - return String(sourceURLNativeString); - } - - auto* provider = m_codeBlock->source().provider(); - if (provider) { - return provider->sourceURL(); - } - return String(); } ALWAYS_INLINE String JSCStackFrame::retrieveFunctionName() { - static auto functionNameModuleCodeString = MAKE_STATIC_STRING_IMPL("module code"); - static auto functionNameGlobalCodeString = MAKE_STATIC_STRING_IMPL("global code"); + static const auto functionNameModuleCodeString = MAKE_STATIC_STRING_IMPL("module code"); + static const auto functionNameGlobalCodeString = MAKE_STATIC_STRING_IMPL("global code"); if (m_isWasmFrame) { return JSC::Wasm::makeString(m_wasmFunctionIndexOrName); @@ -618,4 +600,98 @@ bool JSCStackFrame::calculateSourcePositions() return true; } +String sourceURL(const JSC::SourceOrigin& origin) +{ + if (origin.isNull()) { + return String(); + } + + return origin.string(); +} + +String sourceURL(JSC::SourceProvider* sourceProvider) +{ + if (UNLIKELY(!sourceProvider)) { + return String(); + } + + String url = sourceProvider->sourceURLDirective(); + if (!url.isEmpty()) { + return url; + } + + url = sourceProvider->sourceURL(); + if (!url.isEmpty()) { + return url; + } + + const auto& origin = sourceProvider->sourceOrigin(); + return sourceURL(origin); +} + +String sourceURL(const JSC::SourceCode& sourceCode) +{ + return sourceURL(sourceCode.provider()); +} + +String sourceURL(JSC::CodeBlock* codeBlock) +{ + if (UNLIKELY(!codeBlock)) { + return String(); + } + + if (!codeBlock->ownerExecutable()) { + return String(); + } + + const auto& source = codeBlock->source(); + return sourceURL(source); +} + +String sourceURL(JSC::VM& vm, JSC::StackFrame& frame) +{ + if (frame.isWasmFrame()) { + return "[wasm code]"_s; + } + + if (UNLIKELY(!frame.codeBlock())) { + return "[native code]"_s; + } + + return sourceURL(frame.codeBlock()); +} + +String sourceURL(JSC::StackVisitor& visitor) +{ + switch (visitor->codeType()) { + case JSC::StackVisitor::Frame::Eval: + case JSC::StackVisitor::Frame::Module: + case JSC::StackVisitor::Frame::Function: + case JSC::StackVisitor::Frame::Global: { + return sourceURL(visitor->codeBlock()); + } + case JSC::StackVisitor::Frame::Native: + return "[native code]"_s; + case JSC::StackVisitor::Frame::Wasm: + return "[wasm code]"_s; + } + + RELEASE_ASSERT_NOT_REACHED(); +} + +String sourceURL(JSC::VM& vm, JSC::JSFunction* function) +{ + auto* executable = function->executable(); + if (!executable || executable->isHostFunction()) { + return String(); + } + + auto* jsExecutable = function->jsExecutable(); + if (!jsExecutable) { + return String(); + } + + return Zig::sourceURL(jsExecutable->source()); +} + } diff --git a/src/bun.js/bindings/ErrorStackTrace.h b/src/bun.js/bindings/ErrorStackTrace.h index 8939059c93..9213ef1d88 100644 --- a/src/bun.js/bindings/ErrorStackTrace.h +++ b/src/bun.js/bindings/ErrorStackTrace.h @@ -213,4 +213,13 @@ private: bool isImplementationVisibilityPrivate(JSC::StackVisitor& visitor); bool isImplementationVisibilityPrivate(const JSC::StackFrame& frame); + +String sourceURL(const JSC::SourceOrigin& origin); +String sourceURL(JSC::SourceProvider* sourceProvider); +String sourceURL(const JSC::SourceCode& sourceCode); +String sourceURL(JSC::CodeBlock* codeBlock); +String sourceURL(JSC::VM& vm, JSC::StackFrame& frame); +String sourceURL(JSC::StackVisitor& visitor); +String sourceURL(JSC::VM& vm, JSC::JSFunction* function); + } diff --git a/src/bun.js/bindings/InspectorTestReporterAgent.cpp b/src/bun.js/bindings/InspectorTestReporterAgent.cpp index dad53f2b54..00d8bbc7da 100644 --- a/src/bun.js/bindings/InspectorTestReporterAgent.cpp +++ b/src/bun.js/bindings/InspectorTestReporterAgent.cpp @@ -132,30 +132,20 @@ void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int if (visitor->hasLineAndColumnInfo()) { lineColumn = visitor->computeLineAndColumn(); - String sourceURLForFrame = visitor->sourceURL(); + String sourceURLForFrame = Zig::sourceURL(visitor); // Sometimes, the sourceURL is empty. // For example, pages in Next.js. if (sourceURLForFrame.isEmpty()) { + auto* codeBlock = visitor->codeBlock(); + ASSERT(codeBlock); // hasLineAndColumnInfo() checks codeBlock(), so this is safe to access here. - const auto& source = visitor->codeBlock()->source(); + const auto& source = codeBlock->source(); // source.isNull() is true when the SourceProvider is a null pointer. if (!source.isNull()) { auto* provider = source.provider(); - // I'm not 100% sure we should show sourceURLDirective here. - if (!provider->sourceURLDirective().isEmpty()) { - sourceURLForFrame = provider->sourceURLDirective(); - } else if (!provider->sourceURL().isEmpty()) { - sourceURLForFrame = provider->sourceURL(); - } else { - const auto& origin = provider->sourceOrigin(); - if (!origin.isNull()) { - sourceURLForFrame = origin.string(); - } - } - sourceID = provider->asID(); } } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 5ba9f8fcc0..c407e2fbe1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -559,30 +559,7 @@ WTF::String Bun::formatStackTrace( remappedFrame.position.line_zero_based = originalLine.zeroBasedInt(); remappedFrame.position.column_zero_based = originalColumn.zeroBasedInt(); - String sourceURLForFrame = frame.sourceURL(vm); - - // Sometimes, the sourceURL is empty. - // For example, pages in Next.js. - if (sourceURLForFrame.isEmpty()) { - // hasLineAndColumnInfo() checks codeBlock(), so this is safe to access here. - const auto& source = frame.codeBlock()->source(); - - // source.isNull() is true when the SourceProvider is a null pointer. - if (!source.isNull()) { - auto* provider = source.provider(); - // I'm not 100% sure we should show sourceURLDirective here. - if (!provider->sourceURLDirective().isEmpty()) { - sourceURLForFrame = provider->sourceURLDirective(); - } else if (!provider->sourceURL().isEmpty()) { - sourceURLForFrame = provider->sourceURL(); - } else { - const auto& origin = provider->sourceOrigin(); - if (!origin.isNull()) { - sourceURLForFrame = origin.string(); - } - } - } - } + String sourceURLForFrame = Zig::sourceURL(vm, frame); bool isDefinitelyNotRunninginNodeVMGlobalObject = (globalObject == lexicalGlobalObject && globalObject); @@ -687,29 +664,46 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj // We need to sourcemap it if it's a GlobalObject. if (globalObject == lexicalGlobalObject) { - size_t framesCount = stackTrace.size(); - ZigStackFrame remappedFrames[64]; - framesCount = framesCount > 64 ? 64 : framesCount; - for (int i = 0; i < framesCount; i++) { - remappedFrames[i] = {}; - remappedFrames[i].source_url = Bun::toStringRef(lexicalGlobalObject, stackTrace.at(i).sourceURL()); + for (int i = 0; i < stackTrace.size(); i++) { + ZigStackFrame frame = {}; + + String sourceURLForFrame = Zig::sourceURL(vm, stackFrames.at(i)); + if (JSCStackFrame::SourcePositions* sourcePositions = stackTrace.at(i).getSourcePositions()) { - remappedFrames[i].position.line_zero_based = sourcePositions->line.zeroBasedInt(); - remappedFrames[i].position.column_zero_based = sourcePositions->column.zeroBasedInt(); + frame.position.line_zero_based = sourcePositions->line.zeroBasedInt(); + frame.position.column_zero_based = sourcePositions->column.zeroBasedInt(); } else { - remappedFrames[i].position.line_zero_based = -1; - remappedFrames[i].position.column_zero_based = -1; + frame.position.line_zero_based = -1; + frame.position.column_zero_based = -1; + } + + if (!sourceURLForFrame.isEmpty()) { + frame.source_url = Bun::toStringRef(sourceURLForFrame); + + // This ensures the lifetime of the sourceURL is accounted for correctly + Bun__remapStackFramePositions(globalObject, &frame, 1); + + sourceURLForFrame = frame.source_url.toWTFString(); + } + + auto* callsite = jsCast(callSites.at(i)); + + if (!sourceURLForFrame.isEmpty()) + callsite->setSourceURL(vm, jsString(vm, sourceURLForFrame)); + + if (frame.remapped) { + callsite->setLineNumber(frame.position.line()); + callsite->setColumnNumber(frame.position.column()); } } + } else { + // if it's a different JSGlobalObject, let's still give you the sourceURL directive just to be nice. + for (int i = 0; i < stackTrace.size(); i++) { - Bun__remapStackFramePositions(globalObject, remappedFrames, framesCount); - - for (size_t i = 0; i < framesCount; i++) { - JSC::JSValue callSiteValue = callSites.at(i); - if (remappedFrames[i].remapped) { - CallSite* callSite = JSC::jsCast(callSiteValue); - callSite->setColumnNumber(remappedFrames[i].position.column()); - callSite->setLineNumber(remappedFrames[i].position.line()); + String sourceURLForFrame = Zig::sourceURL(vm, stackFrames.at(i)); + if (!sourceURLForFrame.isEmpty()) { + auto* callsite = jsCast(callSites.at(i)); + callsite->setSourceURL(vm, jsString(vm, sourceURLForFrame)); } } } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index d8b9c3bbc5..506b1614bc 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6232,33 +6232,7 @@ CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JS lineColumn = visitor->computeLineAndColumn(); - String sourceURLForFrame = visitor->sourceURL(); - - // Sometimes, the sourceURL is empty. - // For example, pages in Next.js. - if (sourceURLForFrame.isEmpty()) { - - // hasLineAndColumnInfo() checks codeBlock(), so this is safe to access here. - const auto& source = visitor->codeBlock()->source(); - - // source.isNull() is true when the SourceProvider is a null pointer. - if (!source.isNull()) { - auto* provider = source.provider(); - // I'm not 100% sure we should show sourceURLDirective here. - if (!provider->sourceURLDirective().isEmpty()) { - sourceURLForFrame = provider->sourceURLDirective(); - } else if (!provider->sourceURL().isEmpty()) { - sourceURLForFrame = provider->sourceURL(); - } else { - const auto& origin = provider->sourceOrigin(); - if (!origin.isNull()) { - sourceURLForFrame = origin.string(); - } - } - } - } - - sourceURL = sourceURLForFrame; + sourceURL = Zig::sourceURL(visitor); return WTF::IterationStatus::Done; } diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index 69dcf9307f..814aee3ab3 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -1,6 +1,6 @@ import { nativeFrameForTesting } from "bun:internal-for-testing"; -import { afterEach, expect, test } from "bun:test"; import { noInline } from "bun:jsc"; +import { afterEach, expect, mock, test } from "bun:test"; const origPrepareStackTrace = Error.prepareStackTrace; afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; @@ -697,3 +697,30 @@ test("Error.prepareStackTrace propagates exceptions", () => { ]), ).toThrow("hi"); }); + +test("CallFrame.p.getScriptNameOrSourceURL inside eval", () => { + let prevPrepareStackTrace = Error.prepareStackTrace; + const prepare = mock((e, s) => { + expect(s[0].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[1].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[2].getScriptNameOrSourceURL()).toBe("[native code]"); + expect(s[3].getScriptNameOrSourceURL()).toBe(import.meta.path); + expect(s[4].getScriptNameOrSourceURL()).toBe(import.meta.path); + }); + Error.prepareStackTrace = prepare; + let evalScript = `(function() { + throw new Error("bad error!"); + })() //# sourceURL=https://zombo.com/welcome-to-zombo.js`; + + try { + function insideAFunction() { + eval(evalScript); + } + insideAFunction(); + } catch (e) { + e.stack; + } + Error.prepareStackTrace = prevPrepareStackTrace; + + expect(prepare).toHaveBeenCalledTimes(1); +}); From c22315d399de0169b28976cc1179caf757eedd9e Mon Sep 17 00:00:00 2001 From: pfg Date: Tue, 7 Jan 2025 00:39:03 -0800 Subject: [PATCH 56/56] disable serve-body-leak test on windows (#16201) --- test/js/bun/http/serve-body-leak.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/bun/http/serve-body-leak.test.ts b/test/js/bun/http/serve-body-leak.test.ts index ed40ed810d..510a00a078 100644 --- a/test/js/bun/http/serve-body-leak.test.ts +++ b/test/js/bun/http/serve-body-leak.test.ts @@ -1,6 +1,6 @@ import type { Subprocess } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; -import { bunEnv, bunExe, isDebug, isFlaky, isLinux } from "harness"; +import { bunEnv, bunExe, isDebug, isFlaky, isLinux, isWindows } from "harness"; import { join } from "path"; const payload = Buffer.alloc(512 * 1024, "1").toString("utf-8"); // decent size payload to test memory leak @@ -149,7 +149,7 @@ for (const test_info of [ ["should not leak memory when streaming the body and echoing it back", callStreamingEcho, false, 64], ] as const) { const [testName, fn, skip, maxMemoryGrowth] = test_info; - it.todoIf(skip)( + it.todoIf(skip || isFlaky && isWindows)( testName, async () => { const { url, process } = await getURL();