From 86924f36e8bb4d092850e2e6ffa6d3ac1cc9773d Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 2 Oct 2025 18:43:10 -0700 Subject: [PATCH 001/391] Add 'bun why' to help menu (#23197) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith --- src/bun.js/bindings/napi.cpp | 8 ++++---- src/cli.zig | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 75c49b966a..b06b4a2b6f 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -640,11 +640,11 @@ extern "C" napi_status napi_is_typedarray(napi_env env, napi_value value, bool* // it doesn't copy the string // but it's only safe to use if we are not setting a property // because we can't guarantee the lifetime of it -#define PROPERTY_NAME_FROM_UTF8(identifierName) \ - size_t utf8Len = strlen(utf8Name); \ +#define PROPERTY_NAME_FROM_UTF8(identifierName) \ + size_t utf8Len = strlen(utf8Name); \ WTF::String&& nameString = WTF::charactersAreAllASCII(std::span { reinterpret_cast(utf8Name), utf8Len }) \ - ? WTF::String(WTF::StringImpl::createWithoutCopying({ utf8Name, utf8Len })) \ - : WTF::String::fromUTF8(utf8Name); \ + ? WTF::String(WTF::StringImpl::createWithoutCopying({ utf8Name, utf8Len })) \ + : WTF::String::fromUTF8(utf8Name); \ const JSC::PropertyName identifierName = JSC::Identifier::fromString(vm, nameString); extern "C" napi_status napi_has_named_property(napi_env env, napi_value object, diff --git a/src/cli.zig b/src/cli.zig index 0fada12099..6cf201e69f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -181,6 +181,7 @@ pub const HelpCommand = struct { \\ patch \ Prepare a package for patching \\ pm \ Additional package management utilities \\ info {s:<16} Display package metadata from the registry + \\ why {s:<16} Explain why a package is installed \\ \\ build ./a.ts ./b.jsx Bundle TypeScript & JavaScript into a single file \\ @@ -214,6 +215,7 @@ pub const HelpCommand = struct { packages_to_remove_filler[package_remove_i], packages_to_add_filler[(package_add_i + 1) % packages_to_add_filler.len], packages_to_add_filler[(package_add_i + 2) % packages_to_add_filler.len], + packages_to_add_filler[(package_add_i + 3) % packages_to_add_filler.len], packages_to_create_filler[package_create_i], }; From 84f94ca6ddf76283993aab1e748918cb7770169d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 18:50:05 -0700 Subject: [PATCH 002/391] fix(Bun.RedisClient) keep it alive when connecting (#23195) ### What does this PR do? Fixes https://github.com/oven-sh/bun/issues/23178 Fixes https://github.com/oven-sh/bun/issues/23187 Fixes https://github.com/oven-sh/bun/issues/23198 ### How did you verify your code works? Test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/valkey/js_valkey.zig | 4 +-- test/js/valkey/valkey.connecting.fixture.ts | 28 +++++++++++++++++++++ test/js/valkey/valkey.test.ts | 11 +++++++- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/js/valkey/valkey.connecting.fixture.ts diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index fc5a415db8..69c8d88b00 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1194,8 +1194,8 @@ pub const JSValkeyClient = struct { const has_activity = has_pending_commands or !subs_deletable or this.client.flags.is_reconnecting; // There's a couple cases to handle here: - if (has_activity) { - // If we currently have pending activity, we need to keep the event + if (has_activity or this.client.status == .connecting) { + // If we currently have pending activity or we are connecting, we need to keep the event // loop alive. this.poll_ref.ref(this.client.vm); } else { diff --git a/test/js/valkey/valkey.connecting.fixture.ts b/test/js/valkey/valkey.connecting.fixture.ts new file mode 100644 index 0000000000..9a167758d6 --- /dev/null +++ b/test/js/valkey/valkey.connecting.fixture.ts @@ -0,0 +1,28 @@ +import { RedisClient } from "bun"; + +function getOptions() { + if (process.env.BUN_VALKEY_TLS) { + const paths = JSON.parse(process.env.BUN_VALKEY_TLS); + return { + tls: { + key: Bun.file(paths.key), + cert: Bun.file(paths.cert), + ca: Bun.file(paths.ca), + }, + }; + } + return {}; +} + +{ + const client = new RedisClient(process.env.BUN_VALKEY_URL, getOptions()); + client + .connect() + .then(redis => { + console.log("connected"); + client.close(); + }) + .catch(err => { + console.error(err); + }); +} diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index d69ac7d6d6..2c50270127 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1,6 +1,7 @@ import { randomUUIDv7, RedisClient, spawn } from "bun"; import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; -import { bunExe } from "harness"; +import { bunExe, bunRun } from "harness"; +import { join } from "node:path"; import { ctx as _ctx, awaitableCounter, @@ -36,6 +37,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); describe("Basic Operations", () => { + test("should keep process alive when connecting", async () => { + const result = bunRun(join(import.meta.dir, "valkey.connecting.fixture.ts"), { + "BUN_VALKEY_URL": connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, + "BUN_VALKEY_TLS": connectionType === ConnectionType.TLS ? JSON.stringify(TLS_REDIS_OPTIONS.tlsPaths) : "", + }); + expect(result.stdout).toContain(`connected`); + }); + test("should set and get strings", async () => { const redis = ctx.redis; const testKey = "greeting"; From 55f8e8add37f0c826bf5327001c91768e1ac5281 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Thu, 2 Oct 2025 19:00:14 -0700 Subject: [PATCH 003/391] fix(Bun.SQL) time should be represented as a string and date as a time (#23193) ### What does this PR do? Time should be represented as HH:MM:SS or HHH:MM:SS string ### How did you verify your code works? Test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/sql/mysql/protocol/DecodeBinaryValue.zig | 24 ++++++++++-- src/sql/mysql/protocol/ResultSet.zig | 8 +++- test/js/sql/sql-mysql.test.ts | 41 ++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/sql/mysql/protocol/DecodeBinaryValue.zig b/src/sql/mysql/protocol/DecodeBinaryValue.zig index e557383ed5..5406d48060 100644 --- a/src/sql/mysql/protocol/DecodeBinaryValue.zig +++ b/src/sql/mysql/protocol/DecodeBinaryValue.zig @@ -92,18 +92,36 @@ pub fn decodeBinaryValue(globalObject: *jsc.JSGlobalObject, field_type: types.Fi }, .MYSQL_TYPE_TIME => { return switch (try reader.byte()) { - 0 => SQLDataCell{ .tag = .null, .value = .{ .null = 0 } }, + 0 => { + const slice = "00:00:00"; + return SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; + }, 8, 12 => |l| { var data = try reader.read(l); defer data.deinit(); const time = try Time.fromData(&data); - return SQLDataCell{ .tag = .date, .value = .{ .date = time.toJSTimestamp() } }; + + const total_hours = time.hours + time.days * 24; + // -838:59:59 to 838:59:59 is valid (it only store seconds) + // it should be represented as HH:MM:SS or HHH:MM:SS if total_hours > 99 + var buffer: [32]u8 = undefined; + const sign = if (time.negative) "-" else ""; + const slice = brk: { + if (total_hours > 99) { + break :brk std.fmt.bufPrint(&buffer, "{s}{d:0>3}:{d:0>2}:{d:0>2}", .{ sign, total_hours, time.minutes, time.seconds }) catch return error.InvalidBinaryValue; + } else { + break :brk std.fmt.bufPrint(&buffer, "{s}{d:0>2}:{d:0>2}:{d:0>2}", .{ sign, total_hours, time.minutes, time.seconds }) catch return error.InvalidBinaryValue; + } + }; + return SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; }, else => return error.InvalidBinaryValue, }; }, .MYSQL_TYPE_DATE, .MYSQL_TYPE_TIMESTAMP, .MYSQL_TYPE_DATETIME => switch (try reader.byte()) { - 0 => SQLDataCell{ .tag = .null, .value = .{ .null = 0 } }, + 0 => { + return SQLDataCell{ .tag = .date, .value = .{ .date = 0 } }; + }, 11, 7, 4 => |l| { var data = try reader.read(l); defer data.deinit(); diff --git a/src/sql/mysql/protocol/ResultSet.zig b/src/sql/mysql/protocol/ResultSet.zig index 1d49650869..d13a71ac8f 100644 --- a/src/sql/mysql/protocol/ResultSet.zig +++ b/src/sql/mysql/protocol/ResultSet.zig @@ -113,7 +113,13 @@ pub const Row = struct { cell.* = SQLDataCell{ .tag = .json, .value = .{ .json = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; }, - .MYSQL_TYPE_DATE, .MYSQL_TYPE_TIME, .MYSQL_TYPE_DATETIME, .MYSQL_TYPE_TIMESTAMP => { + .MYSQL_TYPE_TIME => { + // lets handle TIME special case as string + // -838:59:50 to 838:59:59 is valid + const slice = value.slice(); + cell.* = SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 }; + }, + .MYSQL_TYPE_DATE, .MYSQL_TYPE_DATETIME, .MYSQL_TYPE_TIMESTAMP => { var str = bun.String.init(value.slice()); defer str.deref(); const date = brk: { diff --git a/test/js/sql/sql-mysql.test.ts b/test/js/sql/sql-mysql.test.ts index ca60698759..fde64b5115 100644 --- a/test/js/sql/sql-mysql.test.ts +++ b/test/js/sql/sql-mysql.test.ts @@ -413,6 +413,47 @@ if (isDockerEnabled()) { expect(result.getTime()).toBe(-251); } }); + test("time", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a TIME)`; + const times = [ + { a: "00:00:00" }, + { a: "01:01:01" }, + { a: "10:10:10" }, + { a: "12:12:59" }, + { a: "-838:59:59" }, + { a: "838:59:59" }, + { a: null }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(times)}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result).toEqual(times); + const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple(); + expect(result2).toEqual(times); + }); + + test("date", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a DATE)`; + const dates = [{ a: "2024-01-01" }, { a: "2024-01-02" }, { a: "2024-01-03" }, { a: null }]; + await sql`INSERT INTO ${sql(random_name)} ${sql(dates)}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result).toEqual([ + { a: new Date("2024-01-01") }, + { a: new Date("2024-01-02") }, + { a: new Date("2024-01-03") }, + { a: null }, + ]); + const result2 = await sql`SELECT * FROM ${sql(random_name)}`.simple(); + expect(result2).toEqual([ + { a: new Date("2024-01-01") }, + { a: new Date("2024-01-02") }, + { a: new Date("2024-01-03") }, + { a: null }, + ]); + }); test("JSON", async () => { await using sql = new SQL({ ...getOptions(), max: 1 }); From d99d622472e68b55ae9ef9fe06685a21310b61ba Mon Sep 17 00:00:00 2001 From: pfg Date: Thu, 2 Oct 2025 19:12:45 -0700 Subject: [PATCH 004/391] Rereun-each fix (#23168) ### What does this PR do? Fix --rerun-each. Fixes #21409 ### How did you verify your code works? Test case --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/cli/test_command.zig | 17 ++-- test/cli/test/rerun-each.test.ts | 132 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 test/cli/test/rerun-each.test.ts diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 9626a14b65..27152e6cdc 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1828,6 +1828,13 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.on_unhandled_rejection.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { + // Clear the module cache before re-running (except for the first run) + if (repeat_index > 0) { + try vm.clearEntryPoint(); + var entry = jsc.ZigString.init(file_path); + try vm.global.deleteModuleRegistryEntry(&entry); + } + var bun_test_root = &jest.Jest.runner.?.bun_test_root; // Determine if this file should run tests concurrently based on glob pattern const should_run_concurrent = reporter.jest.shouldFileRunConcurrently(file_id); @@ -1838,7 +1845,10 @@ pub const TestCommand = struct { bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); var promise = try vm.loadEntryPointForTestRunner(file_path); - reporter.summary().files += 1; + // Only count the file once, not once per repeat + if (repeat_index == 0) { + reporter.summary().files += 1; + } switch (promise.status(vm.global.vm())) { .rejected => { @@ -1905,11 +1915,6 @@ pub const TestCommand = struct { } vm.global.handleRejectedPromises(); - if (repeat_index > 0) { - try vm.clearEntryPoint(); - var entry = jsc.ZigString.init(file_path); - try vm.global.deleteModuleRegistryEntry(&entry); - } if (Output.is_github_action) { Output.prettyErrorln("\n::endgroup::\n", .{}); diff --git a/test/cli/test/rerun-each.test.ts b/test/cli/test/rerun-each.test.ts new file mode 100644 index 0000000000..a8166b3285 --- /dev/null +++ b/test/cli/test/rerun-each.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("--rerun-each should run tests exactly N times", async () => { + using dir = tempDir("test-rerun-each", { + "counter.test.ts": ` + import { test, expect } from "bun:test"; + + // Use a global counter that persists across module reloads + if (!globalThis.testRunCounter) { + globalThis.testRunCounter = 0; + } + + test("should increment counter", () => { + globalThis.testRunCounter++; + console.log(\`Run #\${globalThis.testRunCounter}\`); + expect(true).toBe(true); + }); + `, + }); + + // Test with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should see "Run #1", "Run #2", "Run #3" in the output + expect(stdout).toContain("Run #1"); + expect(stdout).toContain("Run #2"); + expect(stdout).toContain("Run #3"); + + // Should NOT see "Run #4" + expect(stdout).not.toContain("Run #4"); + + // Should run exactly 3 tests - check stderr for test summary + const combined = stdout + stderr; + expect(combined).toMatch(/3 pass/); + + // Test with --rerun-each=1 (should run once) + await using proc2 = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=1"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]); + + expect(exitCode2).toBe(0); + const combined2 = stdout2 + stderr2; + expect(combined2).toMatch(/1 pass/); +}); + +test("--rerun-each should report correct file count", async () => { + using dir = tempDir("test-rerun-each-file-count", { + "test1.test.ts": ` + import { test, expect } from "bun:test"; + test("test in file 1", () => { + expect(true).toBe(true); + }); + `, + }); + + // Run with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test1.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should report "Ran 3 tests across 1 file" not "across 3 files" + const combined = stdout + stderr; + expect(combined).toContain("Ran 3 tests across 1 file"); + expect(combined).not.toContain("across 3 files"); +}); + +test("--rerun-each should handle test failures correctly", async () => { + using dir = tempDir("test-rerun-each-fail", { + "fail.test.ts": ` + import { test, expect } from "bun:test"; + + if (!globalThis.failCounter) { + globalThis.failCounter = 0; + } + + test("fails on second run", () => { + globalThis.failCounter++; + console.log(\`Attempt #\${globalThis.failCounter}\`); + // Fail on the second run + expect(globalThis.failCounter).not.toBe(2); + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "fail.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should have non-zero exit code due to failure + expect(exitCode).not.toBe(0); + + // Should see all three attempts + expect(stdout).toContain("Attempt #1"); + expect(stdout).toContain("Attempt #2"); + expect(stdout).toContain("Attempt #3"); + + // Should report 2 passes and 1 failure - check both stdout and stderr + const combined = stdout + stderr; + expect(combined).toMatch(/2 pass/); + expect(combined).toMatch(/1 fail/); +}); From 79e0aa9bcf5b64d7b53920459504a0b6db573a22 Mon Sep 17 00:00:00 2001 From: pfg Date: Thu, 2 Oct 2025 20:12:59 -0700 Subject: [PATCH 005/391] bun:test performance regression fix (#23199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What does this PR do? Fixes #23120 bun:test changes introduced an added 16-100ms sleep between test files. For a test suite with many fast-running test files, this caused significant impact. Elysia's test suite was running 2x slower (1.8s → 3.9s). image ### How did you verify your code works? Running elysia test suite & minimized reproduction case
Minimzed reproduction case ```ts // full2.test.ts import { it } from 'bun:test' it("timeout", () => { setTimeout(() => {}, 295000); }, 0); // bench.ts import {$} from "bun"; await $`rm -rf tests`; await $`mkdir -p tests`; for (let i = 0; i < 128; i += 1) { await Bun.write(`tests/${i}.test.ts`, ` for (let i = 0; i < 1000; i ++) { it("test${i}", () => {}, 0); } `); } Bun.spawnSync({ cmd: ["hyperfine", ...["bun-1.2.22", "bun-1.2.23+wakeup", "bun-1.2.23"].map(v => `${v} test ./full2.test.ts tests`)], stdio: ["inherit", "inherit", "inherit"], }); ```
--- src/bun.js/test/bun_test.zig | 10 +++++++++- src/cli/test_command.zig | 18 ++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 3cb212ec7d..a66516086a 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -187,6 +187,7 @@ pub const BunTest = struct { default_concurrent: bool, first_last: BunTestRoot.FirstLast, extra_execution_entries: std.ArrayList(*ExecutionEntry), + wants_wakeup: bool = false, phase: enum { collection, @@ -465,7 +466,14 @@ pub const BunTest = struct { const done_callback_test = bun.new(RunTestsTask, .{ .weak = weak.clone(), .globalThis = globalThis, .phase = phase }); errdefer bun.destroy(done_callback_test); const task = jsc.ManagedTask.New(RunTestsTask, RunTestsTask.call).init(done_callback_test); - jsc.VirtualMachine.get().enqueueTask(task); + const vm = globalThis.bunVM(); + var strong = weak.clone().upgrade() orelse { + if (bun.Environment.ci_assert) bun.assert(false); // shouldn't be calling runNextTick after moving on to the next file + return; // but just in case + }; + defer strong.deinit(); + strong.get().wants_wakeup = true; // we need to wake up the event loop so autoTick() doesn't wait for 16-100ms because we just enqueued a task + vm.enqueueTask(task); } pub const RunTestsTask = struct { weak: BunTestPtr.Weak, diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 27152e6cdc..0749181606 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1844,6 +1844,9 @@ pub const TestCommand = struct { reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); + + // need to wake up so autoTick() doesn't wait for 16-100ms after loading the entrypoint + vm.wakeup(); var promise = try vm.loadEntryPointForTestRunner(file_path); // Only count the file once, not once per repeat if (repeat_index == 0) { @@ -1869,16 +1872,7 @@ pub const TestCommand = struct { else => {}, } - { - vm.drainMicrotasks(); - var count = vm.unhandled_error_counter; - vm.global.handleRejectedPromises(); - while (vm.unhandled_error_counter > count) { - count = vm.unhandled_error_counter; - vm.drainMicrotasks(); - vm.global.handleRejectedPromises(); - } - } + vm.eventLoop().tick(); blk: { @@ -1901,6 +1895,10 @@ pub const TestCommand = struct { var prev_unhandled_count = vm.unhandled_error_counter; while (buntest.phase != .done) { + if (buntest.wants_wakeup) { + buntest.wants_wakeup = false; + vm.wakeup(); + } vm.eventLoop().autoTick(); if (buntest.phase == .done) break; vm.eventLoop().tick(); From 693e7995bb3093c42195d52c1773bf686b33c209 Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 2 Oct 2025 21:42:47 -0700 Subject: [PATCH 006/391] Use cached structure in JSBunRequest::clone (#23202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace `createJSBunRequestStructure()` call with direct access to the cached structure in `JSBunRequest::clone()` method for better performance. ## Changes - Updated `JSBunRequest::clone()` to use `m_JSBunRequestStructure.getInitializedOnMainThread()` instead of calling `createJSBunRequestStructure()` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/JSBunRequest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/JSBunRequest.cpp b/src/bun.js/bindings/JSBunRequest.cpp index 9bcb685006..1dcdf1e47a 100644 --- a/src/bun.js/bindings/JSBunRequest.cpp +++ b/src/bun.js/bindings/JSBunRequest.cpp @@ -102,7 +102,7 @@ JSBunRequest* JSBunRequest::clone(JSC::VM& vm, JSGlobalObject* globalObject) { auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* structure = createJSBunRequestStructure(vm, defaultGlobalObject(globalObject)); + auto* structure = defaultGlobalObject(globalObject)->m_JSBunRequestStructure.getInitializedOnMainThread(globalObject); auto* raw = Request__clone(this->wrapped(), globalObject); EXCEPTION_ASSERT(!!raw == !throwScope.exception()); RETURN_IF_EXCEPTION(throwScope, nullptr); From 666180d7fcfd4acdd4bebcc0fd3d9ae3df5465a3 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 3 Oct 2025 02:38:55 -0700 Subject: [PATCH 007/391] fix(install): isolated install with `file` dependency resolving to root package (#23204) ### What does this PR do? Fixes `file:.` in root package.json or `file:../..` in workspace package.json (if '../..' points to the root of the project) ### How did you verify your code works? Added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/PackageManager.zig | 8 ++ .../PackageManager/CommandLineArguments.zig | 5 +- src/install/isolated_install.zig | 48 +++++++--- src/install/isolated_install/FileCopier.zig | 36 +++++-- src/install/isolated_install/Hardlinker.zig | 39 +++++--- src/install/isolated_install/Installer.zig | 95 +++++++++++++------ src/install/isolated_install/Store.zig | 9 ++ src/install/lockfile/bun.lock.zig | 12 ++- src/walker_skippable.zig | 2 +- test/cli/install/isolated-install.test.ts | 42 ++++++++ 10 files changed, 228 insertions(+), 68 deletions(-) diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index ea1243d845..28f8eaf6ae 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -875,6 +875,14 @@ pub fn init( }; manager.event_loop.loop().internal_loop_data.setParentEventLoop(bun.jsc.EventLoopHandle.init(&manager.event_loop)); manager.lockfile = try ctx.allocator.create(Lockfile); + + { + // make sure folder packages can find the root package without creating a new one + var normalized: bun.AbsPath(.{ .sep = .posix }) = .from(root_package_json_path); + defer normalized.deinit(); + try manager.folders.put(manager.allocator, FolderResolution.hash(normalized.slice()), .{ .package_id = 0 }); + } + jsc.MiniEventLoop.global = &manager.event_loop.mini; if (!manager.options.enable.cache) { manager.options.enable.manifest_cache = false; diff --git a/src/install/PackageManager/CommandLineArguments.zig b/src/install/PackageManager/CommandLineArguments.zig index 6ce9dbde57..d28d22994c 100644 --- a/src/install/PackageManager/CommandLineArguments.zig +++ b/src/install/PackageManager/CommandLineArguments.zig @@ -808,7 +808,10 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com cli.lockfile_only = args.flag("--lockfile-only"); if (args.option("--linker")) |linker| { - cli.node_linker = .fromStr(linker); + cli.node_linker = Options.NodeLinker.fromStr(linker) orelse { + Output.errGeneric("Expected --linker to be one of 'isolated' or 'hoisted'", .{}); + Global.exit(1); + }; } if (args.option("--cache-dir")) |cache_dir| { diff --git a/src/install/isolated_install.zig b/src/install/isolated_install.zig index 9020d50503..8576956ef2 100644 --- a/src/install/isolated_install.zig +++ b/src/install/isolated_install.zig @@ -59,7 +59,7 @@ pub fn installIsolatedPackages( // First pass: create full dependency tree with resolved peers next_node: while (node_queue.readItem()) |entry| { - { + check_cycle: { // check for cycles const nodes_slice = nodes.slice(); const node_pkg_ids = nodes_slice.items(.pkg_id); @@ -74,11 +74,17 @@ pub fn installIsolatedPackages( // 'node_modules/.bun/parent@version/node_modules'. const dep_id = node_dep_ids[curr_id.get()]; - if (dep_id == invalid_dependency_id or entry.dep_id == invalid_dependency_id) { + if (dep_id == invalid_dependency_id and entry.dep_id == invalid_dependency_id) { node_nodes[entry.parent_id.get()].appendAssumeCapacity(curr_id); continue :next_node; } + if (dep_id == invalid_dependency_id or entry.dep_id == invalid_dependency_id) { + // one is the root package, one is a dependency on the root package (it has a valid dep_id) + // create a new node for it. + break :check_cycle; + } + // ensure the dependency name is the same before skipping the cycle. if they aren't // we lose dependency name information for the symlinks if (dependencies[dep_id].name_hash == dependencies[entry.dep_id].name_hash) { @@ -93,7 +99,11 @@ pub fn installIsolatedPackages( const node_id: Store.Node.Id = .from(@intCast(nodes.len)); const pkg_deps = pkg_dependency_slices[entry.pkg_id]; - var skip_dependencies_of_workspace_node = false; + // for skipping dependnecies of workspace packages and the root package. the dependencies + // of these packages should only be pulled in once, but we might need to create more than + // one entry if there's multiple dependencies on the workspace or root package. + var skip_dependencies = entry.pkg_id == 0 and entry.dep_id != invalid_dependency_id; + if (entry.dep_id != invalid_dependency_id) { const entry_dep = dependencies[entry.dep_id]; if (pkg_deps.len == 0 or entry_dep.version.tag == .workspace) dont_dedupe: { @@ -106,6 +116,9 @@ pub fn installIsolatedPackages( const node_dep_ids = nodes_slice.items(.dep_id); const dedupe_dep_id = node_dep_ids[dedupe_node_id.get()]; + if (dedupe_dep_id == invalid_dependency_id) { + break :dont_dedupe; + } const dedupe_dep = dependencies[dedupe_dep_id]; if (dedupe_dep.name_hash != entry_dep.name_hash) { @@ -115,7 +128,7 @@ pub fn installIsolatedPackages( if (dedupe_dep.version.tag == .workspace and entry_dep.version.tag == .workspace) { if (dedupe_dep.behavior.isWorkspace() != entry_dep.behavior.isWorkspace()) { // only attach the dependencies to one of the workspaces - skip_dependencies_of_workspace_node = true; + skip_dependencies = true; break :dont_dedupe; } } @@ -132,8 +145,8 @@ pub fn installIsolatedPackages( .pkg_id = entry.pkg_id, .dep_id = entry.dep_id, .parent_id = entry.parent_id, - .nodes = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), - .dependencies = if (skip_dependencies_of_workspace_node) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + .nodes = if (skip_dependencies) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), + .dependencies = if (skip_dependencies) .empty else try .initCapacity(lockfile.allocator, pkg_deps.len), }); const nodes_slice = nodes.slice(); @@ -146,7 +159,7 @@ pub fn installIsolatedPackages( node_nodes[parent_id].appendAssumeCapacity(node_id); } - if (skip_dependencies_of_workspace_node) { + if (skip_dependencies) { continue; } @@ -411,6 +424,11 @@ pub fn installIsolatedPackages( const curr_dep_id = node_dep_ids[entry.node_id.get()]; for (dedupe_entry.value_ptr.items) |info| { + if (info.dep_id == invalid_dependency_id or curr_dep_id == invalid_dependency_id) { + if (info.dep_id != curr_dep_id) { + continue; + } + } if (info.dep_id != invalid_dependency_id and curr_dep_id != invalid_dependency_id) { const curr_dep = dependencies[curr_dep_id]; const existing_dep = dependencies[info.dep_id]; @@ -685,6 +703,7 @@ pub fn installIsolatedPackages( const node_id = entry_node_ids[entry_id.get()]; const pkg_id = node_pkg_ids[node_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; const pkg_name = pkg_names[pkg_id]; const pkg_name_hash = pkg_name_hashes[pkg_id]; @@ -700,15 +719,15 @@ pub fn installIsolatedPackages( continue; }, .root => { - // .monotonic is okay in this block because the task isn't running on another - // thread. - if (entry_id == .root) { + if (dep_id == invalid_dependency_id) { + // .monotonic is okay in this block because the task isn't running on another + // thread. entry_steps[entry_id.get()].store(.symlink_dependencies, .monotonic); - installer.startTask(entry_id); - continue; + } else { + // dep_id is valid meaning this was a dependency that resolved to the root + // package. it gets an entry in the store. } - entry_steps[entry_id.get()].store(.done, .monotonic); - installer.onTaskComplete(entry_id, .skipped); + installer.startTask(entry_id); continue; }, .workspace => { @@ -830,7 +849,6 @@ pub fn installIsolatedPackages( .isolated_package_install_context = entry_id, }; - const dep_id = node_dep_ids[node_id.get()]; const dep = lockfile.buffers.dependencies.items[dep_id]; switch (pkg_res_tag) { diff --git a/src/install/isolated_install/FileCopier.zig b/src/install/isolated_install/FileCopier.zig index 2200f6ee43..07d6dd313a 100644 --- a/src/install/isolated_install/FileCopier.zig +++ b/src/install/isolated_install/FileCopier.zig @@ -3,8 +3,32 @@ pub const FileCopier = struct { src_path: bun.AbsPath(.{ .sep = .auto, .unit = .os }), dest_subpath: bun.RelPath(.{ .sep = .auto, .unit = .os }), + walker: Walker, - pub fn copy(this: *FileCopier, skip_dirnames: []const bun.OSPathSlice) OOM!sys.Maybe(void) { + pub fn init( + src_dir: FD, + src_path: bun.AbsPath(.{ .sep = .auto, .unit = .os }), + dest_subpath: bun.RelPath(.{ .sep = .auto, .unit = .os }), + skip_dirnames: []const bun.OSPathSlice, + ) OOM!FileCopier { + return .{ + .src_dir = src_dir, + .src_path = src_path, + .dest_subpath = dest_subpath, + .walker = try .walk( + src_dir, + bun.default_allocator, + &.{}, + skip_dirnames, + ), + }; + } + + pub fn deinit(this: *const FileCopier) void { + this.walker.deinit(); + } + + pub fn copy(this: *FileCopier) OOM!sys.Maybe(void) { var dest_dir = bun.MakePath.makeOpenPath(FD.cwd().stdDir(), this.dest_subpath.sliceZ(), .{}) catch |err| { // TODO: remove the need for this and implement openDir makePath makeOpenPath in bun var errno: bun.sys.E = switch (@as(anyerror, err)) { @@ -46,15 +70,7 @@ pub const FileCopier = struct { var copy_file_state: bun.CopyFileState = .{}; - var walker: Walker = try .walk( - this.src_dir, - bun.default_allocator, - &.{}, - skip_dirnames, - ); - defer walker.deinit(); - - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { diff --git a/src/install/isolated_install/Hardlinker.zig b/src/install/isolated_install/Hardlinker.zig index 28a78f6212..c0d4629ae6 100644 --- a/src/install/isolated_install/Hardlinker.zig +++ b/src/install/isolated_install/Hardlinker.zig @@ -3,8 +3,32 @@ const Hardlinker = @This(); src_dir: FD, src: bun.AbsPath(.{ .sep = .auto, .unit = .os }), dest: bun.RelPath(.{ .sep = .auto, .unit = .os }), +walker: Walker, -pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.Maybe(void) { +pub fn init( + folder_dir: FD, + src: bun.AbsPath(.{ .sep = .auto, .unit = .os }), + dest: bun.RelPath(.{ .sep = .auto, .unit = .os }), + skip_dirnames: []const bun.OSPathSlice, +) OOM!Hardlinker { + return .{ + .src_dir = folder_dir, + .src = src, + .dest = dest, + .walker = try .walk( + folder_dir, + bun.default_allocator, + &.{}, + skip_dirnames, + ), + }; +} + +pub fn deinit(this: *Hardlinker) void { + this.walker.deinit(); +} + +pub fn link(this: *Hardlinker) OOM!sys.Maybe(void) { if (bun.install.PackageManager.verbose_install) { bun.Output.prettyErrorln( \\Hardlinking {} to {} @@ -14,16 +38,9 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M bun.fmt.fmtOSPath(this.dest.slice(), .{ .path_sep = .auto }), }, ); + bun.Output.flush(); } - var walker: Walker = try .walk( - this.src_dir, - bun.default_allocator, - &.{}, - skip_dirnames, - ); - defer walker.deinit(); - if (comptime Environment.isWindows) { const cwd_buf = bun.w_path_buffer_pool.get(); defer bun.w_path_buffer_pool.put(cwd_buf); @@ -31,7 +48,7 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M return .initErr(bun.sys.Error.fromCode(bun.sys.E.ACCES, .link)); }; - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { @@ -125,7 +142,7 @@ pub fn link(this: *Hardlinker, skip_dirnames: []const bun.OSPathSlice) OOM!sys.M return .success; } - while (switch (walker.next()) { + while (switch (this.walker.next()) { .result => |res| res, .err => |err| return .initErr(err), }) |entry| { diff --git a/src/install/isolated_install/Installer.zig b/src/install/isolated_install/Installer.zig index 258adec2fd..42c1ccabef 100644 --- a/src/install/isolated_install/Installer.zig +++ b/src/install/isolated_install/Installer.zig @@ -421,9 +421,14 @@ pub const Installer = struct { }; }, - .folder => { + .folder, .root => { + const path = switch (pkg_res.tag) { + .folder => pkg_res.value.folder.slice(string_buf), + .root => ".", + else => unreachable, + }; // the folder does not exist in the cache. xdev is per folder dependency - const folder_dir = switch (bun.openDirForIteration(FD.cwd(), pkg_res.value.folder.slice(string_buf))) { + const folder_dir = switch (bun.openDirForIteration(FD.cwd(), path)) { .result => |fd| fd, .err => |err| return .failure(.{ .link_package = err }), }; @@ -440,13 +445,15 @@ pub const Installer = struct { installer.appendStorePath(&dest, this.entry_id); - var hardlinker: Hardlinker = .{ - .src_dir = folder_dir, - .src = src, - .dest = dest, - }; + var hardlinker: Hardlinker = try .init( + folder_dir, + src, + dest, + &.{comptime bun.OSPathLiteral("node_modules")}, + ); + defer hardlinker.deinit(); - switch (try hardlinker.link(&.{comptime bun.OSPathLiteral("node_modules")})) { + switch (try hardlinker.link()) { .result => {}, .err => |err| { if (err.getErrno() == .XDEV) { @@ -501,13 +508,15 @@ pub const Installer = struct { defer dest.deinit(); installer.appendStorePath(&dest, this.entry_id); - var file_copier: FileCopier = .{ - .src_dir = folder_dir, - .src_path = src_path, - .dest_subpath = dest, - }; + var file_copier: FileCopier = try .init( + folder_dir, + src_path, + dest, + &.{comptime bun.OSPathLiteral("node_modules")}, + ); + defer file_copier.deinit(); - switch (try file_copier.copy(&.{})) { + switch (try file_copier.copy()) { .result => {}, .err => |err| { if (PackageManager.verbose_install) { @@ -559,6 +568,18 @@ pub const Installer = struct { continue :backend .hardlink; } + if (installer.manager.options.log_level.isVerbose()) { + bun.Output.prettyErrorln( + \\Cloning {} to {} + , + .{ + bun.fmt.fmtOSPath(pkg_cache_dir_subpath.sliceZ(), .{ .path_sep = .auto }), + bun.fmt.fmtOSPath(dest_subpath.sliceZ(), .{ .path_sep = .auto }), + }, + ); + bun.Output.flush(); + } + switch (sys.clonefileat(cache_dir, pkg_cache_dir_subpath.sliceZ(), FD.cwd(), dest_subpath.sliceZ())) { .result => {}, .err => |clonefile_err1| { @@ -613,13 +634,15 @@ pub const Installer = struct { defer src.deinit(); src.appendJoin(pkg_cache_dir_subpath.slice()); - var hardlinker: Hardlinker = .{ - .src_dir = cached_package_dir.?, - .src = src, - .dest = dest_subpath, - }; + var hardlinker: Hardlinker = try .init( + cached_package_dir.?, + src, + dest_subpath, + &.{}, + ); + defer hardlinker.deinit(); - switch (try hardlinker.link(&.{})) { + switch (try hardlinker.link()) { .result => {}, .err => |err| { if (err.getErrno() == .XDEV) { @@ -678,13 +701,15 @@ pub const Installer = struct { defer src_path.deinit(); src_path.append(pkg_cache_dir_subpath.slice()); - var file_copier: FileCopier = .{ - .src_dir = cached_package_dir.?, - .src_path = src_path, - .dest_subpath = dest_subpath, - }; + var file_copier: FileCopier = try .init( + cached_package_dir.?, + src_path, + dest_subpath, + &.{}, + ); + defer file_copier.deinit(); - switch (try file_copier.copy(&.{})) { + switch (try file_copier.copy()) { .result => {}, .err => |err| { if (PackageManager.verbose_install) { @@ -1231,6 +1256,7 @@ pub const Installer = struct { const nodes = this.store.nodes.slice(); const node_pkg_ids = nodes.items(.pkg_id); + const node_dep_ids = nodes.items(.dep_id); // const node_peers = nodes.items(.peers); const pkgs = this.lockfile.packages.slice(); @@ -1240,10 +1266,25 @@ pub const Installer = struct { const node_id = entry_node_ids[entry_id.get()]; // const peers = node_peers[node_id.get()]; const pkg_id = node_pkg_ids[node_id.get()]; + const dep_id = node_dep_ids[node_id.get()]; const pkg_res = pkg_resolutions[pkg_id]; switch (pkg_res.tag) { - .root => {}, + .root => { + if (dep_id != invalid_dependency_id) { + const pkg_name = pkg_names[pkg_id]; + buf.append("node_modules/" ++ Store.modules_dir_name); + buf.appendFmt("{}", .{ + Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile), + }); + buf.append("node_modules"); + if (pkg_name.isEmpty()) { + buf.append(std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir)); + } else { + buf.append(pkg_name.slice(string_buf)); + } + } + }, .workspace => { buf.append(pkg_res.value.workspace.slice(string_buf)); }, diff --git a/src/install/isolated_install/Store.zig b/src/install/isolated_install/Store.zig index cded0df949..14cf02cca6 100644 --- a/src/install/isolated_install/Store.zig +++ b/src/install/isolated_install/Store.zig @@ -145,6 +145,15 @@ pub const Store = struct { const pkg_res = pkg_resolutions[pkg_id]; switch (pkg_res.tag) { + .root => { + if (pkg_name.isEmpty()) { + try writer.writeAll(std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir)); + } else { + try writer.print("{}@root", .{ + pkg_name.fmtStorePath(string_buf), + }); + } + }, .folder => { try writer.print("{}@file+{}", .{ pkg_name.fmtStorePath(string_buf), diff --git a/src/install/lockfile/bun.lock.zig b/src/install/lockfile/bun.lock.zig index 8c393ea965..a4d00dc91c 100644 --- a/src/install/lockfile/bun.lock.zig +++ b/src/install/lockfile/bun.lock.zig @@ -1652,9 +1652,15 @@ pub fn parseIntoBinaryLockfile( return error.InvalidPackageResolution; }; - const name_str, const res_str = Dependency.splitNameAndVersion(res_info_str) catch { - try log.addError(source, res_info.loc, "Invalid package resolution"); - return error.InvalidPackageResolution; + const name_str, const res_str = name_and_res: { + if (strings.hasPrefixComptime(res_info_str, "@root:")) { + break :name_and_res .{ "", res_info_str[1..] }; + } + + break :name_and_res Dependency.splitNameAndVersion(res_info_str) catch { + try log.addError(source, res_info.loc, "Invalid package resolution"); + return error.InvalidPackageResolution; + }; }; const name_hash = String.Builder.stringHash(name_str); diff --git a/src/walker_skippable.zig b/src/walker_skippable.zig index 079cf90c98..bab654ff4b 100644 --- a/src/walker_skippable.zig +++ b/src/walker_skippable.zig @@ -109,7 +109,7 @@ pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) { return .initResult(null); } -pub fn deinit(self: *Walker) void { +pub fn deinit(self: *const Walker) void { if (self.stack.items.len > 0) { for (self.stack.items[1..]) |*item| { if (self.stack.items.len != 0) { diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 99ca776545..3766f0c60c 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -268,6 +268,48 @@ test("can install folder dependencies", async () => { ).toBe("module.exports = 'hello from pkg-1';"); }); +test("can install folder dependencies on root package", async () => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root-file-dep", + workspaces: ["packages/*"], + dependencies: { + self: "file:.", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + root: "file:../..", + }, + }), + ), + ]); + + console.log({ packageDir }); + + await runBunInstall(bunEnv, packageDir); + + expect( + await Promise.all([ + readlink(join(packageDir, "node_modules", "self")), + readlink(join(packageDir, "packages", "pkg1", "node_modules", "root")), + file(join(packageDir, "node_modules", "self", "package.json")).json(), + ]), + ).toEqual([ + join(".bun", "root-file-dep@root", "node_modules", "root-file-dep"), + join("..", "..", "..", "node_modules", ".bun", "root-file-dep@root", "node_modules", "root-file-dep"), + await file(packageJson).json(), + ]); +}); + describe("isolated workspaces", () => { test("basic", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); From c8cb7713fc7e42df73502c04a681901ea74523b6 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 02:54:23 -0700 Subject: [PATCH 008/391] Fix Windows crash in process.title when console title is empty (#23184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a segmentation fault on Windows 11 when accessing `process.title` in certain scenarios (e.g., when fetching system information or making Discord webhook requests). ## Root Cause The crash occurred in libuv's `uv_get_process_title()` at `util.c:413` in the `strlen()` call. The issue is that `uv__get_process_title()` could return success (0) but leave `process_title` as NULL in edge cases where: 1. `GetConsoleTitleW()` returns an empty string 2. `uv__convert_utf16_to_utf8()` succeeds but doesn't allocate memory for the empty string 3. The subsequent `assert(process_title)` doesn't catch this in release builds 4. `strlen(process_title)` crashes with a null pointer dereference ## Changes Added defensive checks in `BunProcess.cpp`: 1. Initialize the title buffer to an empty string before calling `uv_get_process_title()` 2. Check if the buffer is empty after the call returns 3. Fall back to "bun" if the title is empty or the call fails ## Testing Added regression test in `test/regression/issue/23183.test.ts` that verifies: - `process.title` doesn't crash when accessed - Returns a valid string (either the console title or "bun") Fixes #23183 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/BunProcess.cpp | 3 +- test/regression/issue/23183.test.ts | 50 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/23183.test.ts diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index fff95e88d5..b243763dcd 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -3622,7 +3622,8 @@ JSC_DEFINE_CUSTOM_GETTER(processTitle, (JSC::JSGlobalObject * globalObject, JSC: #else auto& vm = JSC::getVM(globalObject); char title[1024]; - if (uv_get_process_title(title, sizeof(title)) != 0) { + title[0] = '\0'; // Initialize buffer to empty string + if (uv_get_process_title(title, sizeof(title)) != 0 || title[0] == '\0') { return JSValue::encode(jsString(vm, String("bun"_s))); } diff --git a/test/regression/issue/23183.test.ts b/test/regression/issue/23183.test.ts new file mode 100644 index 0000000000..a2a76cea4f --- /dev/null +++ b/test/regression/issue/23183.test.ts @@ -0,0 +1,50 @@ +// https://github.com/oven-sh/bun/issues/23183 +// Test that accessing process.title doesn't crash on Windows +import { test, expect } from "bun:test"; +import { bunExe, bunEnv, isWindows } from "harness"; +import { join } from "path"; + +test("process.title should not crash on Windows", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log(typeof process.title)"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + Bun.readableStreamToText(proc.stdout), + Bun.readableStreamToText(proc.stderr), + proc.exited, + ]); + + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("string"); +}); + +test("process.title should return a non-empty string or fallback to 'bun'", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log(process.title)"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + Bun.readableStreamToText(proc.stdout), + Bun.readableStreamToText(proc.stderr), + proc.exited, + ]); + + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + const title = stdout.trim(); + expect(title.length).toBeGreaterThan(0); + if (isWindows) { + // On Windows, we should get either a valid console title or "bun" + expect(typeof title).toBe("string"); + } else { + expect(title).toBe("bun"); + } +}); From a9b383bac5e15ac591cc76104634f80d26847938 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 15:55:57 -0700 Subject: [PATCH 009/391] fix(crypto): hkdf callback should pass null (not undefined) on success (#23216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixed crypto.hkdf callback to pass `null` instead of `undefined` for the error parameter on success - Added regression test to verify the fix ## Details Fixes #23211 Node.js convention requires crypto callbacks to receive `null` as the error parameter on success, but Bun was passing `undefined`. This caused compatibility issues with code that relies on strict null checks (e.g., [matter.js](https://github.com/matter-js/matter.js/blob/fdbec2cf88b3c810037d1df845b0244e566df1e2/packages/general/src/crypto/NodeJsStyleCrypto.ts#L169)). ### Changes - Updated `CryptoHkdf.cpp` to pass `jsNull()` instead of `jsUndefined()` for the error parameter in the success callback - Added regression test in `test/regression/issue/23211.test.ts` ## Test plan - [x] Added regression test that verifies callback receives `null` on success - [x] Test passes with the fix - [x] Ran existing crypto tests (no failures) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway --- .../bindings/node/crypto/CryptoHkdf.cpp | 2 +- .../js/node/crypto/hkdf-callback-null.test.ts | 23 +++++++++++++++++++ test/regression/issue/23183.test.ts | 5 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 test/js/node/crypto/hkdf-callback-null.test.ts diff --git a/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp b/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp index 267eee4a17..653e14ff0c 100644 --- a/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp +++ b/src/bun.js/bindings/node/crypto/CryptoHkdf.cpp @@ -97,7 +97,7 @@ void HkdfJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback Bun__EventLoop__runCallback2(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), - JSValue::encode(jsUndefined()), + JSValue::encode(jsNull()), JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), buf.releaseNonNull()))); } diff --git a/test/js/node/crypto/hkdf-callback-null.test.ts b/test/js/node/crypto/hkdf-callback-null.test.ts new file mode 100644 index 0000000000..f7d234e3ff --- /dev/null +++ b/test/js/node/crypto/hkdf-callback-null.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from "bun:test"; +import crypto from "node:crypto"; + +// Test that callback receives null (not undefined) for error on success +// https://github.com/oven-sh/bun/issues/23211 +test("crypto.hkdf callback should pass null (not undefined) on success", async () => { + const secret = new Uint8Array([7, 158, 216, 197, 25, 77, 201, 5, 73, 119]); + const salt = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]); + const info = new Uint8Array([67, 111, 109, 112, 114, 101, 115, 115, 101, 100]); + const length = 8; + + const promise = new Promise((resolve, reject) => { + crypto.hkdf("sha256", secret, salt, info, length, (error, key) => { + // Node.js passes null for error on success, not undefined + expect(error).toBeNull(); + expect(error).not.toBeUndefined(); + expect(key).toBeInstanceOf(ArrayBuffer); + resolve(true); + }); + }); + + await promise; +}); diff --git a/test/regression/issue/23183.test.ts b/test/regression/issue/23183.test.ts index a2a76cea4f..b5f280613d 100644 --- a/test/regression/issue/23183.test.ts +++ b/test/regression/issue/23183.test.ts @@ -1,8 +1,7 @@ // https://github.com/oven-sh/bun/issues/23183 // Test that accessing process.title doesn't crash on Windows -import { test, expect } from "bun:test"; -import { bunExe, bunEnv, isWindows } from "harness"; -import { join } from "path"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows } from "harness"; test("process.title should not crash on Windows", async () => { const proc = Bun.spawn({ From ddfc3f7fbc0dac8121ac1755a20b6260cb6ec9d5 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 3 Oct 2025 16:13:06 -0700 Subject: [PATCH 010/391] Add `.cloneUpgrade()` to fix clone-upgrade (#23201) ### What does this PR do? Replaces '.upgrade()' with '.cloneUpgrade()'. '.upgrade()' is confusing and `.clone().upgrade()` was causing a leak. Caught by https://github.com/oven-sh/bun/pull/23199#discussion_r2400667320 ### How did you verify your code works? --- src/bun.js/test/bun_test.zig | 14 ++++++-------- src/bun.js/test/expect.zig | 18 +++++++++++++----- src/bun.js/test/expect/toMatchSnapshot.zig | 3 ++- .../expect/toThrowErrorMatchingSnapshot.zig | 4 ++-- src/bun.js/test/snapshot.zig | 4 +++- src/ptr/shared.zig | 6 ++---- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index a66516086a..a5ba01a625 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -311,10 +311,8 @@ pub const BunTest = struct { bun.destroy(this); buntest_weak.deinit(); } - pub fn bunTest(this: *RefData) ?*BunTest { - var buntest_strong = this.buntest_weak.clone().upgrade() orelse return null; - defer buntest_strong.deinit(); - return buntest_strong.get(); + pub fn bunTest(this: *RefData) ?BunTestPtr { + return this.buntest_weak.upgrade() orelse return null; } }; pub fn getCurrentStateData(this: *BunTest) RefDataValue { @@ -380,7 +378,7 @@ pub const BunTest = struct { const refdata: *RefData = this_ptr.asPromisePtr(RefData); defer refdata.deref(); const has_one_ref = refdata.ref_count.hasOneRef(); - var this_strong = refdata.buntest_weak.clone().upgrade() orelse return group.log("bunTestThenOrCatch -> the BunTest is no longer active", .{}); + var this_strong = refdata.buntest_weak.upgrade() orelse return group.log("bunTestThenOrCatch -> the BunTest is no longer active", .{}); defer this_strong.deinit(); const this = this_strong.get(); @@ -434,7 +432,7 @@ pub const BunTest = struct { if (!should_run) return .js_undefined; - var strong = ref_in.buntest_weak.clone().upgrade() orelse return .js_undefined; + var strong = ref_in.buntest_weak.upgrade() orelse return .js_undefined; defer strong.deinit(); const buntest = strong.get(); buntest.addResult(ref_in.phase); @@ -467,7 +465,7 @@ pub const BunTest = struct { errdefer bun.destroy(done_callback_test); const task = jsc.ManagedTask.New(RunTestsTask, RunTestsTask.call).init(done_callback_test); const vm = globalThis.bunVM(); - var strong = weak.clone().upgrade() orelse { + var strong = weak.upgrade() orelse { if (bun.Environment.ci_assert) bun.assert(false); // shouldn't be calling runNextTick after moving on to the next file return; // but just in case }; @@ -483,7 +481,7 @@ pub const BunTest = struct { pub fn call(this: *RunTestsTask) void { defer bun.destroy(this); defer this.weak.deinit(); - var strong = this.weak.clone().upgrade() orelse return; + var strong = this.weak.upgrade() orelse return; defer strong.deinit(); BunTest.run(strong, this.globalThis) catch |e| { strong.get().onUncaughtException(this.globalThis, this.globalThis.takeException(e), false, this.phase); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 700c6413aa..5a132d23c9 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -32,7 +32,9 @@ pub const Expect = struct { pub fn incrementExpectCallCounter(this: *Expect) void { const parent = this.parent orelse return; // not in bun:test - const buntest = parent.bunTest() orelse return; // the test file this expect() call was for is no longer + var buntest_strong = parent.bunTest() orelse return; // the test file this expect() call was for is no longer + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); if (parent.phase.sequence(buntest)) |sequence| { // found active sequence sequence.expect_call_count +|= 1; @@ -44,7 +46,7 @@ pub const Expect = struct { } } - pub fn bunTest(this: *Expect) ?*bun.jsc.Jest.bun_test.BunTest { + pub fn bunTest(this: *Expect) ?bun.jsc.Jest.bun_test.BunTestPtr { const parent = this.parent orelse return null; return parent.bunTest(); } @@ -275,7 +277,9 @@ pub const Expect = struct { pub fn getSnapshotName(this: *Expect, allocator: std.mem.Allocator, hint: string) ![]const u8 { const parent = this.parent orelse return error.NoTest; - const buntest = parent.bunTest() orelse return error.TestNotActive; + var buntest_strong = parent.bunTest() orelse return error.TestNotActive; + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); const execution_entry = parent.phase.entry(buntest) orelse return error.SnapshotInConcurrentGroup; const test_name = execution_entry.base.name orelse "(unnamed)"; @@ -748,10 +752,12 @@ pub const Expect = struct { } } } - const buntest = this.bunTest() orelse { + var buntest_strong = this.bunTest() orelse { const signature = comptime getSignature(fn_name, "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); // 1. find the src loc of the snapshot const srcloc = callFrame.getCallerSrcLoc(globalThis); @@ -825,7 +831,9 @@ pub const Expect = struct { const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint) catch |err| { var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis }; defer formatter.deinit(); - const buntest = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{}); + var buntest_strong = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{}); + defer buntest_strong.deinit(); + const buntest = buntest_strong.get(); const test_file_path = Jest.runner.?.files.get(buntest.file_id).source.path.text; return switch (err) { error.FailedToOpenSnapshotFile => globalThis.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}), diff --git a/src/bun.js/test/expect/toMatchSnapshot.zig b/src/bun.js/test/expect/toMatchSnapshot.zig index 3cf63e77af..30de93411d 100644 --- a/src/bun.js/test/expect/toMatchSnapshot.zig +++ b/src/bun.js/test/expect/toMatchSnapshot.zig @@ -12,10 +12,11 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - _ = this.bunTest() orelse { + var buntest_strong = this.bunTest() orelse { const signature = comptime getSignature("toMatchSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; + defer buntest_strong.deinit(); var hint_string: ZigString = ZigString.Empty; var property_matchers: ?JSValue = null; diff --git a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig index 31757b6f6e..5162a14557 100644 --- a/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig +++ b/src/bun.js/test/expect/toThrowErrorMatchingSnapshot.zig @@ -12,11 +12,11 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } - const bunTest = this.bunTest() orelse { + var bunTest_strong = this.bunTest() orelse { const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true); return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); }; - _ = bunTest; // ? + defer bunTest_strong.deinit(); var hint_string: ZigString = ZigString.Empty; switch (arguments.len) { diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 7562cad792..07300f6915 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -53,7 +53,9 @@ pub const Snapshots = struct { return .{ count_entry.key_ptr.*, count_entry.value_ptr.* }; } pub fn getOrPut(this: *Snapshots, expect: *Expect, target_value: []const u8, hint: string) !?string { - const bunTest = expect.bunTest() orelse return error.SnapshotFailed; + var buntest_strong = expect.bunTest() orelse return error.SnapshotFailed; + defer buntest_strong.deinit(); + const bunTest = buntest_strong.get(); switch (try this.getSnapshotFile(bunTest.file_id)) { .result => {}, .err => |err| { diff --git a/src/ptr/shared.zig b/src/ptr/shared.zig index f9ff6b6951..dfdd096a99 100644 --- a/src/ptr/shared.zig +++ b/src/ptr/shared.zig @@ -285,16 +285,13 @@ fn Weak(comptime Pointer: type, comptime options: Options) type { const SharedNonOptional = WithOptions(NonOptionalPointer, options); - /// Upgrades this weak pointer into a normal shared pointer. - /// - /// This method invalidates `self`. + /// Clones this weak pointer and upgrades it to a shared pointer. You still need to `deinit` the weak pointer. pub fn upgrade(self: Self) ?SharedNonOptional { const data = if (comptime info.isOptional()) self.getData() orelse return null else self.getData(); if (!data.tryIncrementStrong()) return null; - data.decrementWeak(); return .{ .#pointer = &data.value }; } @@ -455,6 +452,7 @@ fn FullData(comptime Child: type, comptime options: Options) type { // .acq_rel because we need to make sure other threads are done using the object before // we free it. if ((comptime !options.allow_weak) or self.weak_count.decrement() == 0) { + if (bun.Environment.ci_assert) bun.assert(self.strong_count.get(.monotonic) == 0); self.destroy(); } } From f14f3b03bb97703d2c81e0a377fe84d990aaf14e Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 3 Oct 2025 17:10:28 -0700 Subject: [PATCH 011/391] Add new bindings generator; port `SSLConfig` (#23169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new generator for JS → Zig bindings. The bulk of the conversion is done in C++, after which the data is transformed into an FFI-safe representation, passed to Zig, and then finally transformed into idiomatic Zig types. In its current form, the new bindings generator supports: * Signed and unsigned integers * Floats (plus a “finite” variant that disallows NaN and infinities) * Strings * ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView) * Blob * Optional types * Nullable types (allows null, whereas Optional only allows undefined) * Arrays * User-defined string enumerations * User-defined unions (fields can optionally be named to provide a better experience in Zig) * Null and undefined, for use in unions (can more efficiently represent optional/nullable unions than wrapping a union in an optional) * User-defined dictionaries (arbitrary key-value pairs; expects a JS object and parses it into a struct) * Default values for dictionary members * Alternative names for dictionary members (e.g., to support both `serverName` and `servername` without taking up twice the space) * Descriptive error messages * Automatic `fromJS` functions in Zig for dictionaries * Automatic `deinit` functions for the generated Zig types Although this bindings generator has many features not present in `bindgen.ts`, it does not yet implement all of `bindgen.ts`'s functionality, so for the time being, it has been named `bindgenv2`, and its configuration is specified in `.bindv2.ts` files. Once all `bindgen.ts`'s functionality has been incorporated, it will be renamed. This PR ports `SSLConfig` to use the new bindings generator; see `SSLConfig.bindv2.ts`. (For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323, STAB-1324) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith --- .gitignore | 1 + .prettierrc | 6 + build.zig | 5 + bun.lock | 12 +- cmake/Options.cmake | 5 + cmake/Sources.json | 8 + cmake/targets/BuildBun.cmake | 55 ++ package.json | 10 +- packages/bun-types/bun.d.ts | 4 - packages/bun-usockets/src/libusockets.h | 6 +- src/allocators/mimalloc.zig | 3 +- src/bake/DevServer.bind.ts | 2 +- src/bake/tsconfig.json | 1 + src/bun.js.zig | 1 + src/bun.js/Strong.zig | 2 +- src/bun.js/api/bun/socket.zig | 74 +- src/bun.js/api/bun/socket/Handlers.zig | 25 +- src/bun.js/api/bun/socket/Listener.zig | 44 +- src/bun.js/api/bun/ssl_wrapper.zig | 2 +- src/bun.js/api/server.zig | 2 +- src/bun.js/api/server/SSLConfig.bindv2.ts | 90 ++ src/bun.js/api/server/SSLConfig.zig | 871 ++++++------------ src/bun.js/api/server/ServerConfig.zig | 2 +- src/bun.js/api/sql.classes.ts | 4 +- src/bun.js/bindgen.zig | 254 +++++ src/bun.js/bindings/Bindgen.h | 11 + src/bun.js/bindings/Bindgen/ExternTraits.h | 152 +++ src/bun.js/bindings/Bindgen/ExternUnion.h | 89 ++ .../bindings/Bindgen/ExternVectorTraits.h | 149 +++ src/bun.js/bindings/Bindgen/IDLConvert.h | 19 + src/bun.js/bindings/Bindgen/IDLConvertBase.h | 74 ++ src/bun.js/bindings/Bindgen/IDLTypes.h | 31 + src/bun.js/bindings/Bindgen/Macros.h | 36 + src/bun.js/bindings/BunIDLConvert.h | 280 ++++++ src/bun.js/bindings/BunIDLConvertBase.h | 77 ++ src/bun.js/bindings/BunIDLConvertBlob.h | 36 + src/bun.js/bindings/BunIDLConvertContext.h | 252 +++++ src/bun.js/bindings/BunIDLConvertNumbers.h | 174 ++++ src/bun.js/bindings/BunIDLHumanReadable.h | 139 +++ src/bun.js/bindings/BunIDLTypes.h | 87 ++ src/bun.js/bindings/ConcatCStrings.h | 78 ++ src/bun.js/bindings/IDLTypes.h | 56 +- src/bun.js/bindings/JSValue.zig | 21 +- src/bun.js/bindings/MimallocWTFMalloc.h | 103 +++ .../bindings/{Strong.cpp => StrongRef.cpp} | 1 + src/bun.js/bindings/StrongRef.h | 23 + src/bun.js/bindings/ZigGlobalObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 102 +- src/bun.js/bindings/headers-handwritten.h | 3 +- src/bun.js/bindings/headers.h | 2 +- src/bun.js/bindings/webcore/JSDOMConvert.h | 1 + .../bindings/webcore/JSDOMConvertBase.h | 2 + .../bindings/webcore/JSDOMConvertDictionary.h | 12 +- .../webcore/JSDOMConvertEnumeration.h | 36 + .../bindings/webcore/JSDOMConvertNullable.h | 29 + .../bindings/webcore/JSDOMConvertOptional.h | 105 +++ .../bindings/webcore/JSDOMConvertSequences.h | 356 +++++-- src/bun.js/bindings/webcore/JSDOMURL.cpp | 0 src/bun.js/jsc.zig | 3 + src/bun.js/jsc/array_buffer.zig | 48 +- src/bun.zig | 2 +- src/codegen/bindgen-lib.ts | 5 +- src/codegen/bindgenv2/internal/any.ts | 42 + src/codegen/bindgenv2/internal/array.ts | 32 + src/codegen/bindgenv2/internal/base.ts | 134 +++ src/codegen/bindgenv2/internal/dictionary.ts | 451 +++++++++ src/codegen/bindgenv2/internal/enumeration.ts | 182 ++++ src/codegen/bindgenv2/internal/interfaces.ts | 40 + src/codegen/bindgenv2/internal/optional.ts | 84 ++ src/codegen/bindgenv2/internal/primitives.ts | 125 +++ src/codegen/bindgenv2/internal/string.ts | 21 + src/codegen/bindgenv2/internal/union.ts | 185 ++++ src/codegen/bindgenv2/lib.ts | 10 + src/codegen/bindgenv2/script.ts | 185 ++++ src/codegen/bindgenv2/tsconfig.json | 11 + src/codegen/bundle-modules.ts | 4 +- src/codegen/class-definitions.ts | 4 +- src/codegen/helpers.ts | 17 +- src/codegen/replacements.ts | 4 +- src/deps/uws/SocketContext.zig | 6 +- src/meta.zig | 2 + src/meta/tagged_union.zig | 236 +++++ src/napi/napi.zig | 14 +- src/node-fallbacks/build-fallbacks.ts | 6 +- src/string.zig | 7 +- src/string/{WTFStringImpl.zig => wtf.zig} | 0 src/tsconfig.json | 5 +- test/js/bun/http/bun-server.test.ts | 4 +- test/js/bun/http/serve.test.ts | 6 +- test/js/bun/net/tcp-server.test.ts | 4 +- tsconfig.base.json | 2 +- 91 files changed, 5015 insertions(+), 895 deletions(-) create mode 100644 src/bun.js/api/server/SSLConfig.bindv2.ts create mode 100644 src/bun.js/bindgen.zig create mode 100644 src/bun.js/bindings/Bindgen.h create mode 100644 src/bun.js/bindings/Bindgen/ExternTraits.h create mode 100644 src/bun.js/bindings/Bindgen/ExternUnion.h create mode 100644 src/bun.js/bindings/Bindgen/ExternVectorTraits.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvert.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvertBase.h create mode 100644 src/bun.js/bindings/Bindgen/IDLTypes.h create mode 100644 src/bun.js/bindings/Bindgen/Macros.h create mode 100644 src/bun.js/bindings/BunIDLConvert.h create mode 100644 src/bun.js/bindings/BunIDLConvertBase.h create mode 100644 src/bun.js/bindings/BunIDLConvertBlob.h create mode 100644 src/bun.js/bindings/BunIDLConvertContext.h create mode 100644 src/bun.js/bindings/BunIDLConvertNumbers.h create mode 100644 src/bun.js/bindings/BunIDLHumanReadable.h create mode 100644 src/bun.js/bindings/BunIDLTypes.h create mode 100644 src/bun.js/bindings/ConcatCStrings.h create mode 100644 src/bun.js/bindings/MimallocWTFMalloc.h rename src/bun.js/bindings/{Strong.cpp => StrongRef.cpp} (98%) create mode 100644 src/bun.js/bindings/StrongRef.h create mode 100644 src/bun.js/bindings/webcore/JSDOMConvertOptional.h mode change 100755 => 100644 src/bun.js/bindings/webcore/JSDOMURL.cpp create mode 100644 src/codegen/bindgenv2/internal/any.ts create mode 100644 src/codegen/bindgenv2/internal/array.ts create mode 100644 src/codegen/bindgenv2/internal/base.ts create mode 100644 src/codegen/bindgenv2/internal/dictionary.ts create mode 100644 src/codegen/bindgenv2/internal/enumeration.ts create mode 100644 src/codegen/bindgenv2/internal/interfaces.ts create mode 100644 src/codegen/bindgenv2/internal/optional.ts create mode 100644 src/codegen/bindgenv2/internal/primitives.ts create mode 100644 src/codegen/bindgenv2/internal/string.ts create mode 100644 src/codegen/bindgenv2/internal/union.ts create mode 100644 src/codegen/bindgenv2/lib.ts create mode 100755 src/codegen/bindgenv2/script.ts create mode 100644 src/codegen/bindgenv2/tsconfig.json create mode 100644 src/meta/tagged_union.zig rename src/string/{WTFStringImpl.zig => wtf.zig} (100%) diff --git a/.gitignore b/.gitignore index 3f71c2acc9..b0c2fa643d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env .envrc .eslintcache +.gdb_history .idea .next .ninja_deps diff --git a/.prettierrc b/.prettierrc index c9da1bd439..14285ca704 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,6 +19,12 @@ "options": { "printWidth": 80 } + }, + { + "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], + "options": { + "printWidth": 100 + } } ] } diff --git a/build.zig b/build.zig index fa76a2138c..0f45dce6e1 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,7 @@ const BunBuildOptions = struct { enable_logs: bool = false, enable_asan: bool, enable_valgrind: bool, + use_mimalloc: bool, tracy_callstack_depth: u16, reported_nodejs_version: Version, /// To make iterating on some '@embedFile's faster, we load them at runtime @@ -97,6 +98,7 @@ const BunBuildOptions = struct { opts.addOption(bool, "enable_logs", this.enable_logs); opts.addOption(bool, "enable_asan", this.enable_asan); opts.addOption(bool, "enable_valgrind", this.enable_valgrind); + opts.addOption(bool, "use_mimalloc", this.use_mimalloc); opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version})); opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm); opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis); @@ -270,6 +272,7 @@ pub fn build(b: *Build) !void { .enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false, .enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false, .enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false, + .use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false, .llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1, }; @@ -500,6 +503,7 @@ fn addMultiCheck( .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, .enable_valgrind = root_build_options.enable_valgrind, + .use_mimalloc = root_build_options.use_mimalloc, .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; @@ -720,6 +724,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { // Generated code exposed as individual modules. inline for (.{ .{ .file = "ZigGeneratedClasses.zig", .import = "ZigGeneratedClasses" }, + .{ .file = "bindgen_generated.zig", .import = "bindgen_generated" }, .{ .file = "ResolvedSourceTag.zig", .import = "ResolvedSourceTag" }, .{ .file = "ErrorCode.zig", .import = "ErrorCode" }, .{ .file = "runtime.out.js", .enable = opts.shouldEmbedCode() }, diff --git a/bun.lock b/bun.lock index be4ab107ae..fedf606d75 100644 --- a/bun.lock +++ b/bun.lock @@ -8,14 +8,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2", }, }, @@ -284,7 +284,7 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 1e9b664321..93a3698563 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -202,4 +202,9 @@ optionx(USE_WEBKIT_ICU BOOL "Use the ICU libraries from WebKit" DEFAULT ${DEFAUL optionx(ERROR_LIMIT STRING "Maximum number of errors to show when compiling C++ code" DEFAULT "100") +# This is not an `option` because setting this variable to OFF is experimental +# and unsupported. This replaces the `use_mimalloc` variable previously in +# bun.zig, and enables C++ code to also be aware of the option. +set(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR ON) + list(APPEND CMAKE_ARGS -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) diff --git a/cmake/Sources.json b/cmake/Sources.json index cd86d86989..5ae4930693 100644 --- a/cmake/Sources.json +++ b/cmake/Sources.json @@ -31,6 +31,14 @@ "output": "BindgenSources.txt", "paths": ["src/**/*.bind.ts"] }, + { + "output": "BindgenV2Sources.txt", + "paths": ["src/**/*.bindv2.ts"] + }, + { + "output": "BindgenV2InternalSources.txt", + "paths": ["src/codegen/bindgenv2/**/*.ts"] + }, { "output": "ZigSources.txt", "paths": ["src/**/*.zig"] diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 31b007050c..945f8074fb 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -395,6 +395,54 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGENV2_SCRIPT ${CWD}/src/codegen/bindgenv2/script.ts) + +absolute_sources(BUN_BINDGENV2_SOURCES ${CWD}/cmake/sources/BindgenV2Sources.txt) +# These sources include the script itself. +absolute_sources(BUN_BINDGENV2_INTERNAL_SOURCES + ${CWD}/cmake/sources/BindgenV2InternalSources.txt) +string(REPLACE ";" "," BUN_BINDGENV2_SOURCES_COMMA_SEPARATED + "${BUN_BINDGENV2_SOURCES}") + +execute_process( + COMMAND ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=list-outputs + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + --codegen-path=${CODEGEN_PATH} + RESULT_VARIABLE bindgen_result + OUTPUT_VARIABLE bindgen_outputs +) +if(${bindgen_result}) + message(FATAL_ERROR "bindgenv2/script.ts exited with non-zero status") +endif() +foreach(output IN LISTS bindgen_outputs) + if(output MATCHES "\.cpp$") + list(APPEND BUN_BINDGENV2_CPP_OUTPUTS ${output}) + elseif(output MATCHES "\.zig$") + list(APPEND BUN_BINDGENV2_ZIG_OUTPUTS ${output}) + else() + message(FATAL_ERROR "unexpected bindgen output: [${output}]") + endif() +endforeach() + +register_command( + TARGET + bun-bindgen-v2 + COMMENT + "Generating bindings (v2)" + COMMAND + ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=generate + --codegen-path=${CODEGEN_PATH} + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + SOURCES + ${BUN_BINDGENV2_SOURCES} + ${BUN_BINDGENV2_INTERNAL_SOURCES} + OUTPUTS + ${BUN_BINDGENV2_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} +) + set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) absolute_sources(BUN_BINDGEN_SOURCES ${CWD}/cmake/sources/BindgenSources.txt) @@ -573,6 +621,7 @@ set(BUN_ZIG_GENERATED_SOURCES ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} ) # In debug builds, these are not embedded, but rather referenced at runtime. @@ -636,6 +685,7 @@ register_command( -Denable_logs=$,true,false> -Denable_asan=$,true,false> -Denable_valgrind=$,true,false> + -Duse_mimalloc=$,true,false> -Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS} -Dversion=${VERSION} -Dreported_nodejs_version=${NODEJS_VERSION} @@ -712,6 +762,7 @@ list(APPEND BUN_CPP_SOURCES ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGENV2_CPP_OUTPUTS} ) if(WIN32) @@ -849,6 +900,10 @@ if(WIN32) ) endif() +if(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR) + target_compile_definitions(${bun} PRIVATE USE_MIMALLOC=1) +endif() + target_compile_definitions(${bun} PRIVATE _HAS_EXCEPTIONS=0 LIBUS_USE_OPENSSL=1 diff --git a/package.json b/package.json index 737d6c33f4..372a9d596e 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2" }, "resolutions": { diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 406e3d8466..3425900c86 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3707,10 +3707,6 @@ declare module "bun" { */ secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options - keyFile?: string; - - certFile?: string; - ALPNProtocols?: string | BufferSource; ciphers?: string; diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index a5156f700c..0e746a0388 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -226,11 +226,11 @@ struct us_bun_socket_context_options_t { const char *ca_file_name; const char *ssl_ciphers; int ssl_prefer_low_memory_usage; /* Todo: rename to prefer_low_memory_usage and apply for TCP as well */ - const char **key; + const char * const *key; unsigned int key_count; - const char **cert; + const char * const *cert; unsigned int cert_count; - const char **ca; + const char * const *ca; unsigned int ca_count; unsigned int secure_options; int reject_unauthorized; diff --git a/src/allocators/mimalloc.zig b/src/allocators/mimalloc.zig index a486fe2abf..3d9e9b2e07 100644 --- a/src/allocators/mimalloc.zig +++ b/src/allocators/mimalloc.zig @@ -216,8 +216,7 @@ pub extern fn mi_new_reallocn(p: ?*anyopaque, newcount: usize, size: usize) ?*an pub const MI_SMALL_WSIZE_MAX = @as(c_int, 128); pub const MI_SMALL_SIZE_MAX = MI_SMALL_WSIZE_MAX * @import("std").zig.c_translation.sizeof(?*anyopaque); pub const MI_ALIGNMENT_MAX = (@as(c_int, 16) * @as(c_int, 1024)) * @as(c_ulong, 1024); - -const MI_MAX_ALIGN_SIZE = 16; +pub const MI_MAX_ALIGN_SIZE = 16; pub fn mustUseAlignedAlloc(alignment: std.mem.Alignment) bool { return alignment.toByteUnits() > MI_MAX_ALIGN_SIZE; diff --git a/src/bake/DevServer.bind.ts b/src/bake/DevServer.bind.ts index d56758a4a6..df89b41feb 100644 --- a/src/bake/DevServer.bind.ts +++ b/src/bake/DevServer.bind.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { fn, t } from "../codegen/bindgen-lib"; +import { fn, t } from "bindgen"; export const getDeinitCountForTesting = fn({ args: {}, ret: t.usize, diff --git a/src/bake/tsconfig.json b/src/bake/tsconfig.json index 11e0dce4dd..8ab4d5832d 100644 --- a/src/bake/tsconfig.json +++ b/src/bake/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "baseUrl": ".", "paths": { "bun-framework-react/*": ["./bun-framework-react/*"], "bindgen": ["../codegen/bindgen-lib"] diff --git a/src/bun.js.zig b/src/bun.js.zig index 13ca71282d..fb59390ea0 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -1,6 +1,7 @@ pub const jsc = @import("./bun.js/jsc.zig"); pub const webcore = @import("./bun.js/webcore.zig"); pub const api = @import("./bun.js/api.zig"); +pub const bindgen = @import("./bun.js/bindgen.zig"); pub const Run = struct { ctx: Command.Context, diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index a23b68627a..5c098b88eb 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -114,7 +114,7 @@ pub const Optional = struct { } }; -const Impl = opaque { +pub const Impl = opaque { pub fn init(global: *jsc.JSGlobalObject, value: jsc.JSValue) *Impl { jsc.markBinding(@src()); return Bun__StrongRef__new(global, value); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index fb18864c97..8fa141fb59 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1412,23 +1412,12 @@ pub fn NewSocket(comptime ssl: bool) type { } var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; - defer { - if (!success) { - if (ssl_opts) |*ssl_config| { - ssl_config.deinit(); - } - } - } if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } @@ -1436,9 +1425,10 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); + defer socket_config.deinit(); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1449,14 +1439,7 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - var socket_config = ssl_opts.?; - ssl_opts = null; - defer socket_config.deinit(); const options = socket_config.asUSockets(); - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const ext_size = @sizeOf(WrappedSocket); var handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); @@ -1470,8 +1453,14 @@ pub fn NewSocket(comptime ssl: bool) type { .socket = TLSSocket.Socket.detached, .connection = if (this.connection) |c| c.clone() else null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS .flags = .{ .is_active = false, @@ -1955,19 +1944,15 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1975,11 +1960,6 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C default_data.ensureStillAlive(); } - const socket_config = ssl_opts.?; - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const is_server = false; // A duplex socket is always handled as a client var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); @@ -1994,8 +1974,14 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .socket = TLSSocket.Socket.detached, .connection = null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS }); const tls_js_value = tls.getThisValue(globalObject); @@ -2006,7 +1992,7 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .tls = tls, .vm = globalObject.bunVM(), .task = undefined, - .ssl_config = socket_config, + .ssl_config = socket_config.*, }); tls.ref(); diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index b87975827e..b874d9f4f2 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -210,7 +210,7 @@ pub const SocketConfig = struct { hostname_or_unix: jsc.ZigString.Slice, port: ?u16 = null, fd: ?bun.FileDescriptor = null, - ssl: ?jsc.API.ServerConfig.SSLConfig = null, + ssl: ?SSLConfig = null, handlers: Handlers, default_data: jsc.JSValue = .zero, exclusive: bool = false, @@ -246,26 +246,18 @@ pub const SocketConfig = struct { var reusePort = false; var ipv6Only = false; - var ssl: ?jsc.API.ServerConfig.SSLConfig = null; + var ssl: ?SSLConfig = null; var default_data = JSValue.zero; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls)) |ssl_config| { - ssl = ssl_config; - } + if (!tls.isBoolean()) { + ssl = try SSLConfig.fromJS(vm, globalObject, tls); + } else if (tls.toBoolean()) { + ssl = SSLConfig.zero; } } - errdefer { - if (ssl != null) { - ssl.?.deinit(); - } - } + errdefer bun.memory.deinit(&ssl); hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { @@ -382,9 +374,10 @@ const bun = @import("bun"); const Environment = bun.Environment; const strings = bun.strings; const uws = bun.uws; +const Listener = bun.api.Listener; +const SSLConfig = bun.api.ServerConfig.SSLConfig; const jsc = bun.jsc; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const BinaryType = jsc.ArrayBuffer.BinaryType; -const Listener = jsc.API.Listener; diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 755bcb16b6..84afd1b5e8 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -132,7 +132,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa const connection: Listener.UnixOrHost = .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() }; if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } } var socket = Listener{ @@ -156,9 +156,10 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa this.* = socket; //TODO: server_name is not supported on named pipes, I belive its , lets wait for someone to ask for it + const ssl_ptr = if (ssl) |*s| s else null; this.listener = .{ // we need to add support for the backlog parameter on listen here we use the default value of nodejs - .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl, this) catch { + .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl_ptr, this) catch { this.deinit(); return globalObject.throwInvalidArguments("Failed to listen at {s}", .{pipe_name}); }, @@ -172,8 +173,8 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa } } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -203,7 +204,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } uws.NewSocketHandler(true).configure( @@ -411,10 +412,12 @@ pub fn addServerName(this: *Listener, global: *jsc.JSGlobalObject, hostname: JSV return global.throwInvalidArguments("hostname pattern cannot be empty", .{}); } - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { + if (try SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { // to keep nodejs compatibility, we allow to replace the server name this.socket_context.?.removeServerName(true, server_name); this.socket_context.?.addServerName(true, server_name, ssl_config.asUSockets()); + var ssl_config_mut = ssl_config; + ssl_config_mut.deinit(); } return .js_undefined; @@ -569,7 +572,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock var protos: ?[]const u8 = null; var server_name: ?[]const u8 = null; const ssl_enabled = ssl != null; - defer if (ssl != null) ssl.?.deinit(); + defer if (ssl) |*some_ssl| some_ssl.deinit(); vm.eventLoop().ensureWaker(); @@ -704,8 +707,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -726,7 +729,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } if (ssl.?.server_name) |s| { server_name = bun.handleOom(bun.default_allocator.dupe(u8, s[0..bun.len(s)])); @@ -907,7 +910,13 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { this.uvPipe.close(onPipeClosed); } - pub fn listen(globalThis: *jsc.JSGlobalObject, path: []const u8, backlog: i32, ssl_config: ?jsc.API.ServerConfig.SSLConfig, listener: *Listener) !*WindowsNamedPipeListeningContext { + pub fn listen( + globalThis: *jsc.JSGlobalObject, + path: []const u8, + backlog: i32, + ssl_config: ?*const SSLConfig, + listener: *Listener, + ) !*WindowsNamedPipeListeningContext { const this = WindowsNamedPipeListeningContext.new(.{ .globalThis = globalThis, .vm = globalThis.bunVM(), @@ -917,7 +926,7 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { if (ssl_config) |ssl_options| { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options @@ -984,13 +993,18 @@ const bun = @import("bun"); const Async = bun.Async; const Environment = bun.Environment; const Output = bun.Output; -const api = bun.api; const default_allocator = bun.default_allocator; const strings = bun.strings; const uws = bun.uws; const BoringSSL = bun.BoringSSL.c; const uv = bun.windows.libuv; +const api = bun.api; +const Handlers = bun.api.SocketHandlers; +const TCPSocket = bun.api.TCPSocket; +const TLSSocket = bun.api.TLSSocket; +const SSLConfig = bun.api.ServerConfig.SSLConfig; + const NewSocket = api.socket.NewSocket; const SocketConfig = api.socket.SocketConfig; const WindowsNamedPipeContext = api.socket.WindowsNamedPipeContext; @@ -1000,7 +1014,3 @@ const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const NodePath = jsc.Node.path; - -const Handlers = jsc.API.SocketHandlers; -const TCPSocket = jsc.API.TCPSocket; -const TLSSocket = jsc.API.TLSSocket; diff --git a/src/bun.js/api/bun/ssl_wrapper.zig b/src/bun.js/api/bun/ssl_wrapper.zig index 829c4dea43..819107fa9e 100644 --- a/src/bun.js/api/bun/ssl_wrapper.zig +++ b/src/bun.js/api/bun/ssl_wrapper.zig @@ -93,7 +93,7 @@ pub fn SSLWrapper(comptime T: type) type { pub fn init(ssl_options: jsc.API.ServerConfig.SSLConfig, is_client: bool, handlers: Handlers) !This { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index c90540d795..1448dc2e52 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2741,7 +2741,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d // apply SNI routes if any if (this.config.sni) |*sni| { for (sni.slice()) |*sni_ssl_config| { - const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name); + const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name.?); if (sni_servername.len > 0) { app.addServerNameWithOptions(sni_servername, sni_ssl_config.asUSockets()) catch { if (!globalThis.hasException()) { diff --git a/src/bun.js/api/server/SSLConfig.bindv2.ts b/src/bun.js/api/server/SSLConfig.bindv2.ts new file mode 100644 index 0000000000..04a3f0b0f1 --- /dev/null +++ b/src/bun.js/api/server/SSLConfig.bindv2.ts @@ -0,0 +1,90 @@ +import * as b from "bindgenv2"; + +export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, +}); + +export const SSLConfigFile = b.union("SSLConfigFile", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, + array: b.Array(SSLConfigSingleFile), +}); + +export const ALPNProtocols = b.union("ALPNProtocols", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, +}); + +export const SSLConfig = b.dictionary( + { + name: "SSLConfig", + userFacingName: "TLSOptions", + generateConversionFunction: true, + }, + { + passphrase: b.String.nullable, + dhParamsFile: { + type: b.String.nullable, + internalName: "dh_params_file", + }, + serverName: { + type: b.String.nullable, + internalName: "server_name", + altNames: ["servername"], + }, + lowMemoryMode: { + type: b.bool, + default: false, + internalName: "low_memory_mode", + }, + rejectUnauthorized: { + type: b.bool.nullable, + internalName: "reject_unauthorized", + }, + requestCert: { + type: b.bool, + default: false, + internalName: "request_cert", + }, + ca: SSLConfigFile, + cert: SSLConfigFile, + key: SSLConfigFile, + secureOptions: { + type: b.u32, + default: 0, + internalName: "secure_options", + }, + keyFile: { + type: b.String.nullable, + internalName: "key_file", + }, + certFile: { + type: b.String.nullable, + internalName: "cert_file", + }, + caFile: { + type: b.String.nullable, + internalName: "ca_file", + }, + ALPNProtocols: { + type: ALPNProtocols, + internalName: "alpn_protocols", + }, + ciphers: b.String.nullable, + clientRenegotiationLimit: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_limit", + }, + clientRenegotiationWindow: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_window", + }, + }, +); diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index fe309c8d4e..3d36e04b5c 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -1,65 +1,60 @@ const SSLConfig = @This(); -server_name: [*c]const u8 = null, +server_name: ?[*:0]const u8 = null, -key_file_name: [*c]const u8 = null, -cert_file_name: [*c]const u8 = null, +key_file_name: ?[*:0]const u8 = null, +cert_file_name: ?[*:0]const u8 = null, -ca_file_name: [*c]const u8 = null, -dh_params_file_name: [*c]const u8 = null, +ca_file_name: ?[*:0]const u8 = null, +dh_params_file_name: ?[*:0]const u8 = null, -passphrase: [*c]const u8 = null, +passphrase: ?[*:0]const u8 = null, -key: ?[][*c]const u8 = null, -key_count: u32 = 0, - -cert: ?[][*c]const u8 = null, -cert_count: u32 = 0, - -ca: ?[][*c]const u8 = null, -ca_count: u32 = 0, +key: ?[][*:0]const u8 = null, +cert: ?[][*:0]const u8 = null, +ca: ?[][*:0]const u8 = null, secure_options: u32 = 0, request_cert: i32 = 0, reject_unauthorized: i32 = 0, ssl_ciphers: ?[*:0]const u8 = null, protos: ?[*:0]const u8 = null, -protos_len: usize = 0, client_renegotiation_limit: u32 = 0, client_renegotiation_window: u32 = 0, requires_custom_request_ctx: bool = false, is_using_default_ciphers: bool = true, low_memory_mode: bool = false, -const BlobFileContentResult = struct { - data: [:0]const u8, - - fn init(comptime fieldname: []const u8, js_obj: jsc.JSValue, global: *jsc.JSGlobalObject) bun.JSError!?BlobFileContentResult { - { - const body = try jsc.WebCore.Body.Value.fromJS(global, js_obj); - if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) { - var fs: jsc.Node.fs.NodeFS = .{}; - const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated); - switch (read) { - .err => { - return global.throwValue(read.err.toJS(global)); - }, - else => { - const str = read.result.null_terminated; - if (str.len > 0) { - return .{ .data = str }; - } - return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{}); - }, - } - } - } - - return null; - } +const ReadFromBlobError = bun.JSError || error{ + NullStore, + NotAFile, + EmptyFile, }; -pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { +fn readFromBlob( + global: *jsc.JSGlobalObject, + blob: *bun.webcore.Blob, +) ReadFromBlobError![:0]const u8 { + const store = blob.store orelse return error.NullStore; + const file = switch (store.data) { + .file => |f| f, + else => return error.NotAFile, + }; + var fs: jsc.Node.fs.NodeFS = .{}; + const maybe = fs.readFileWithOptions( + .{ .path = file.pathlike }, + .sync, + .null_terminated, + ); + const result = switch (maybe) { + .result => |result| result, + .err => |err| return global.throwValue(err.toJS(global)), + }; + if (result.null_terminated.len == 0) return error.EmptyFile; + return bun.default_allocator.dupeZ(u8, result.null_terminated); +} + +pub fn asUSockets(this: *const SSLConfig) uws.SocketContext.BunSocketContextOptions { var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{}; if (this.key_file_name != null) @@ -76,15 +71,15 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { if (this.key) |key| { ctx_opts.key = key.ptr; - ctx_opts.key_count = this.key_count; + ctx_opts.key_count = @intCast(key.len); } if (this.cert) |cert| { ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = this.cert_count; + ctx_opts.cert_count = @intCast(cert.len); } if (this.ca) |ca| { ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = this.ca_count; + ctx_opts.ca_count = @intCast(ca.len); } if (this.ssl_ciphers != null) { @@ -96,595 +91,295 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { return ctx_opts; } -pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool { - { //strings - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "ssl_ciphers", - "protos", - }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != null and rhs != null) { - if (!stringsEqual(lhs, rhs)) - return false; - } else if (lhs != null or rhs != null) { - return false; - } - } - } - - { - //numbers - const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != rhs) - return false; - } - } - - { - // complex fields - const fields = .{ "key", "ca", "cert" }; - inline for (fields) |field| { - const lhs_count = @field(thisConfig, field ++ "_count"); - const rhs_count = @field(otherConfig, field ++ "_count"); - if (lhs_count != rhs_count) - return false; - if (lhs_count > 0) { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - for (0..lhs_count) |i| { - if (!stringsEqual(lhs.?[i], rhs.?[i])) - return false; +pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool { + inline for (comptime std.meta.fieldNames(SSLConfig)) |field| { + const first = @field(this, field); + const second = @field(other, field); + switch (@FieldType(SSLConfig, field)) { + ?[*:0]const u8 => { + const a = first orelse return second == null; + const b = second orelse return false; + if (!stringsEqual(a, b)) return false; + }, + ?[][*:0]const u8 => { + const slice1 = first orelse return second == null; + const slice2 = second orelse return false; + if (slice1.len != slice2.len) return false; + for (slice1, slice2) |a, b| { + if (!stringsEqual(a, b)) return false; } - } + }, + else => if (first != second) return false, } } - return true; } -fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool { +fn stringsEqual(a: [*:0]const u8, b: [*:0]const u8) bool { const lhs = bun.asByteSlice(a); const rhs = bun.asByteSlice(b); return strings.eqlLong(lhs, rhs, true); } -pub fn deinit(this: *SSLConfig) void { - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - } - - inline for (fields) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - @field(this, field) = ""; - } - } - - if (this.cert) |cert| { - for (0..this.cert_count) |i| { - const slice = std.mem.span(cert[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(cert); - this.cert = null; - } - - if (this.key) |key| { - for (0..this.key_count) |i| { - const slice = std.mem.span(key[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(key); - this.key = null; - } - - if (this.ca) |ca| { - for (0..this.ca_count) |i| { - const slice = std.mem.span(ca[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(ca); - this.ca = null; +fn freeStrings(slice: *?[][*:0]const u8) void { + const inner = slice.* orelse return; + for (inner) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); } + bun.default_allocator.free(inner); + slice.* = null; } + +fn freeString(string: *?[*:0]const u8) void { + const inner = string.* orelse return; + bun.freeSensitive(bun.default_allocator, std.mem.span(inner)); + string.* = null; +} + +pub fn deinit(this: *SSLConfig) void { + bun.meta.useAllFields(SSLConfig, .{ + .server_name = freeString(&this.server_name), + .key_file_name = freeString(&this.key_file_name), + .cert_file_name = freeString(&this.cert_file_name), + .ca_file_name = freeString(&this.ca_file_name), + .dh_params_file_name = freeString(&this.dh_params_file_name), + .passphrase = freeString(&this.passphrase), + .key = freeStrings(&this.key), + .cert = freeStrings(&this.cert), + .ca = freeStrings(&this.ca), + .secure_options = {}, + .request_cert = {}, + .reject_unauthorized = {}, + .ssl_ciphers = freeString(&this.ssl_ciphers), + .protos = freeString(&this.protos), + .client_renegotiation_limit = {}, + .client_renegotiation_window = {}, + .requires_custom_request_ctx = {}, + .is_using_default_ciphers = {}, + .low_memory_mode = {}, + }); +} + +fn cloneStrings(slice: ?[][*:0]const u8) ?[][*:0]const u8 { + const inner = slice orelse return null; + const result = bun.handleOom(bun.default_allocator.alloc([*:0]const u8, inner.len)); + for (inner, result) |string, *out| { + out.* = bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string))); + } + return result; +} + +fn cloneString(string: ?[*:0]const u8) ?[*:0]const u8 { + return bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string orelse return null))); +} + pub fn clone(this: *const SSLConfig) SSLConfig { - var cloned: SSLConfig = .{ + return .{ + .server_name = cloneString(this.server_name), + .key_file_name = cloneString(this.key_file_name), + .cert_file_name = cloneString(this.cert_file_name), + .ca_file_name = cloneString(this.ca_file_name), + .dh_params_file_name = cloneString(this.dh_params_file_name), + .passphrase = cloneString(this.passphrase), + .key = cloneStrings(this.key), + .cert = cloneStrings(this.cert), + .ca = cloneStrings(this.ca), .secure_options = this.secure_options, .request_cert = this.request_cert, .reject_unauthorized = this.reject_unauthorized, + .ssl_ciphers = cloneString(this.ssl_ciphers), + .protos = cloneString(this.protos), .client_renegotiation_limit = this.client_renegotiation_limit, .client_renegotiation_window = this.client_renegotiation_window, .requires_custom_request_ctx = this.requires_custom_request_ctx, .is_using_default_ciphers = this.is_using_default_ciphers, .low_memory_mode = this.low_memory_mode, - .protos_len = this.protos_len, }; - const fields_cloned_by_memcopy = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - cloned.ssl_ciphers = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned.ssl_ciphers = null; - } - } - } - - inline for (fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - @field(cloned, field) = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } - } - - const array_fields_cloned_by_memcopy = .{ - "cert", - "key", - "ca", - }; - inline for (array_fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |array| { - const cloned_array = bun.handleOom(bun.default_allocator.alloc([*c]const u8, @field(this, field ++ "_count"))); - @field(cloned, field) = cloned_array; - @field(cloned, field ++ "_count") = @field(this, field ++ "_count"); - for (0..@field(this, field ++ "_count")) |i| { - const slice = std.mem.span(array[i]); - if (slice.len > 0) { - cloned_array[i] = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned_array[i] = ""; - } - } - } - } - return cloned; } pub const zero = SSLConfig{}; -pub fn fromJS(vm: *jsc.VirtualMachine, global: *jsc.JSGlobalObject, obj: jsc.JSValue) bun.JSError!?SSLConfig { - var result = zero; +pub fn fromJS( + vm: *jsc.VirtualMachine, + global: *jsc.JSGlobalObject, + value: jsc.JSValue, +) bun.JSError!?SSLConfig { + var generated: jsc.generated.SSLConfig = try .fromJS(global, value); + defer generated.deinit(); + var result: SSLConfig = zero; errdefer result.deinit(); - - var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - - if (!obj.isObject()) { - return global.throwInvalidArguments("tls option expects an object", .{}); - } - var any = false; - result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized()); - - // Required - if (try obj.getTruthy(global, "keyFile")) |key_file_name| { - var sliced = try key_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access keyFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "key")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("key", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all keys - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.key = native_array; - } - - result.key_count = valid_count; - } - } else if (try BlobFileContentResult.init("key", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.key = native_array; - result.key_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.key = native_array; - result.key_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getTruthy(global, "certFile")) |cert_file_name| { - var sliced = try cert_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access certFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| { - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - result.protos = try bun.default_allocator.dupeZ(u8, sliced); - result.protos_len = sliced.len; - } - - any = true; - result.requires_custom_request_ctx = true; - } else { - return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); - } - } - - if (try obj.getTruthy(global, "cert")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("cert", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.cert = native_array; - } - - result.cert_count = valid_count; - } - } else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.cert = native_array; - result.cert_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.cert = native_array; - result.cert_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| { - result.request_cert = if (request_cert) 1 else 0; + if (generated.passphrase.get()) |passphrase| { + result.passphrase = passphrase.toOwnedSliceZ(bun.default_allocator); any = true; } - - if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| { - result.reject_unauthorized = if (reject_unauthorized) 1 else 0; + if (generated.dh_params_file.get()) |dh_params_file| { + result.dh_params_file_name = try handlePath(global, "dhParamsFile", dh_params_file); any = true; } - - if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { - var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice()); - result.is_using_default_ciphers = false; - any = true; - result.requires_custom_request_ctx = true; - } - } - if (result.is_using_default_ciphers) { - result.ssl_ciphers = global.bunVM().rareData().tlsDefaultCiphers() orelse null; + if (generated.server_name.get()) |server_name| { + result.server_name = server_name.toOwnedSliceZ(bun.default_allocator); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| { - var sliced = try server_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - any = true; - result.requires_custom_request_ctx = true; - } + result.low_memory_mode = generated.low_memory_mode; + result.reject_unauthorized = @intFromBool( + generated.reject_unauthorized orelse vm.getTLSRejectUnauthorized(), + ); + result.request_cert = @intFromBool(generated.request_cert); + result.secure_options = generated.secure_options; + any = any or + result.low_memory_mode or + generated.reject_unauthorized != null or + generated.request_cert or + result.secure_options != 0; + + result.ca = try handleFileForField(global, "ca", &generated.ca); + result.cert = try handleFileForField(global, "cert", &generated.cert); + result.key = try handleFileForField(global, "key", &generated.key); + result.requires_custom_request_ctx = result.requires_custom_request_ctx or + result.ca != null or + result.cert != null or + result.key != null; + + if (generated.key_file.get()) |key_file| { + result.key_file_name = try handlePath(global, "keyFile", key_file); + result.requires_custom_request_ctx = true; + } + if (generated.cert_file.get()) |cert_file| { + result.cert_file_name = try handlePath(global, "certFile", cert_file); + result.requires_custom_request_ctx = true; + } + if (generated.ca_file.get()) |ca_file| { + result.ca_file_name = try handlePath(global, "caFile", ca_file); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "ca")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("ca", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all CA's - result.cert = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.ca = native_array; - } - - result.ca_count = valid_count; - } - } else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.ca = native_array; - result.ca_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.ca = native_array; - result.ca_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.ca = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } + const protocols = switch (generated.alpn_protocols) { + .none => null, + .string => |*ref| ref.get().toOwnedSliceZ(bun.default_allocator), + .buffer => |*ref| blk: { + const buffer: jsc.ArrayBuffer = ref.get().asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + }; + if (protocols) |some_protocols| { + result.protos = some_protocols; + result.requires_custom_request_ctx = true; + } + if (generated.ciphers.get()) |ciphers| { + result.ssl_ciphers = ciphers.toOwnedSliceZ(bun.default_allocator); + result.is_using_default_ciphers = false; + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "caFile")) |ca_file_name| { - var sliced = try ca_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid caFile path", .{}); - } - } + result.client_renegotiation_limit = generated.client_renegotiation_limit; + result.client_renegotiation_window = generated.client_renegotiation_window; + any = any or + result.requires_custom_request_ctx or + result.client_renegotiation_limit != 0 or + generated.client_renegotiation_window != 0; + + // We don't need to deinit `result` if `any` is false. + return if (any) result else null; +} + +fn handlePath( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + string: bun.string.WTFStringImpl, +) bun.JSError![:0]const u8 { + const name = string.toOwnedSliceZ(bun.default_allocator); + errdefer bun.freeSensitive(bun.default_allocator, name); + if (std.posix.system.access(name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments( + std.fmt.comptimePrint("Unable to access {s} path", .{field}), + .{}, + ); } - // Optional - if (any) { - if (try obj.getTruthy(global, "secureOptions")) |secure_options| { - if (secure_options.isNumber()) { - result.secure_options = secure_options.toU32(); - } - } + return name; +} - if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| { - if (client_renegotiation_limit.isNumber()) { - result.client_renegotiation_limit = client_renegotiation_limit.toU32(); - } - } +fn handleFileForField( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + file: *const jsc.generated.SSLConfigFile, +) bun.JSError!?[][*:0]const u8 { + return handleFile(global, file) catch |err| switch (err) { + error.JSError => return error.JSError, + error.OutOfMemory => return error.OutOfMemory, + error.EmptyFile => return global.throwInvalidArguments( + std.fmt.comptimePrint("TLSOptions.{s} is an empty file", .{field}), + .{}, + ), + error.NullStore, error.NotAFile => return global.throwInvalidArguments( + std.fmt.comptimePrint( + "TLSOptions.{s} is not a valid BunFile (non-BunFile `Blob`s are not supported)", + .{field}, + ), + .{}, + ), + }; +} - if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| { - if (client_renegotiation_window.isNumber()) { - result.client_renegotiation_window = client_renegotiation_window.toU32(); - } - } - - if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { - var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid dhParamsFile path", .{}); - } - } - } - - if (try obj.getTruthy(global, "passphrase")) |passphrase| { - var sliced = try passphrase.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice()); - } - } - - if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| { - if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { - result.low_memory_mode = low_memory_mode.toBoolean(); - any = true; - } else { - return global.throw("Expected lowMemoryMode to be a boolean", .{}); - } - } - } - - if (!any) - return null; +fn handleFile( + global: *jsc.JSGlobalObject, + file: *const jsc.generated.SSLConfigFile, +) ReadFromBlobError!?[][*:0]const u8 { + const single = try handleSingleFile(global, switch (file.*) { + .none => return null, + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + .array => |*list| return try handleFileArray(global, list.items()), + }); + errdefer bun.freeSensitive(bun.default_allocator, single); + const result = try bun.default_allocator.alloc([*:0]const u8, 1); + result[0] = single; return result; } +fn handleFileArray( + global: *jsc.JSGlobalObject, + elements: []const jsc.generated.SSLConfigSingleFile, +) ReadFromBlobError!?[][*:0]const u8 { + if (elements.len == 0) return null; + var result: bun.collections.ArrayListDefault([*:0]const u8) = try .initCapacity(elements.len); + errdefer { + for (result.items()) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); + } + result.deinit(); + } + for (elements) |*elem| { + result.appendAssumeCapacity(try handleSingleFile(global, switch (elem.*) { + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + })); + } + return try result.toOwnedSlice(); +} + +fn handleSingleFile( + global: *jsc.JSGlobalObject, + file: union(enum) { + string: bun.string.WTFStringImpl, + buffer: *jsc.JSCArrayBuffer, + file: *bun.webcore.Blob, + }, +) ReadFromBlobError![:0]const u8 { + return switch (file) { + .string => |string| string.toOwnedSliceZ(bun.default_allocator), + .buffer => |jsc_buffer| blk: { + const buffer: jsc.ArrayBuffer = jsc_buffer.asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + .file => |blob| try readFromBlob(global, blob), + }; +} + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 5e347924d5..8a1caca83d 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -931,7 +931,7 @@ pub fn fromJS( if (args.ssl_config == null) { args.ssl_config = ssl_config; } else { - if (ssl_config.server_name == null or std.mem.span(ssl_config.server_name).len == 0) { + if ((ssl_config.server_name orelse "")[0] == 0) { defer ssl_config.deinit(); return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); } diff --git a/src/bun.js/api/sql.classes.ts b/src/bun.js/api/sql.classes.ts index 3fdfe17a8d..ee1405ca47 100644 --- a/src/bun.js/api/sql.classes.ts +++ b/src/bun.js/api/sql.classes.ts @@ -1,7 +1,7 @@ -import { define } from "../../codegen/class-definitions"; +import { ClassDefinition, define } from "../../codegen/class-definitions"; const types = ["PostgresSQL", "MySQL"]; -const classes = []; +const classes: ClassDefinition[] = []; for (const type of types) { classes.push( define({ diff --git a/src/bun.js/bindgen.zig b/src/bun.js/bindgen.zig new file mode 100644 index 0000000000..be303cb6ff --- /dev/null +++ b/src/bun.js/bindgen.zig @@ -0,0 +1,254 @@ +pub fn BindgenTrivial(comptime T: type) type { + return struct { + pub const ZigType = T; + pub const ExternType = T; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return extern_value; + } + }; +} + +pub const BindgenBool = BindgenTrivial(bool); +pub const BindgenU8 = BindgenTrivial(u8); +pub const BindgenI8 = BindgenTrivial(i8); +pub const BindgenU16 = BindgenTrivial(u16); +pub const BindgenI16 = BindgenTrivial(i16); +pub const BindgenU32 = BindgenTrivial(u32); +pub const BindgenI32 = BindgenTrivial(i32); +pub const BindgenU64 = BindgenTrivial(u64); +pub const BindgenI64 = BindgenTrivial(i64); +pub const BindgenF64 = BindgenTrivial(f64); +pub const BindgenRawAny = BindgenTrivial(jsc.JSValue); + +pub const BindgenStrongAny = struct { + pub const ZigType = jsc.Strong; + pub const ExternType = ?*jsc.Strong.Impl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .{ .impl = extern_value.? }; + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .{ .impl = extern_value }; + } +}; + +/// This represents both `IDLNull` and `IDLMonostateUndefined`. +pub const BindgenNull = struct { + pub const ZigType = void; + pub const ExternType = u8; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + _ = extern_value; + } +}; + +pub fn BindgenOptional(comptime Child: type) type { + return struct { + pub const ZigType = if (@hasDecl(Child, "OptionalZigType")) + Child.OptionalZigType + else + ?Child.ZigType; + + pub const ExternType = if (@hasDecl(Child, "OptionalExternType")) + Child.OptionalExternType + else + ExternTaggedUnion(&.{ u8, Child.ExternType }); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + if (comptime @hasDecl(Child, "OptionalExternType")) { + return Child.convertOptionalFromExtern(extern_value); + } + if (extern_value.tag == 0) { + return null; + } + bun.assert_eql(extern_value.tag, 1); + return Child.convertFromExtern(extern_value.data.@"1"); + } + }; +} + +pub const BindgenString = struct { + pub const ZigType = bun.string.WTFString; + pub const ExternType = ?bun.string.WTFStringImpl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } +}; + +pub fn BindgenUnion(comptime children: []const type) type { + var tagged_field_types: [children.len]type = undefined; + var untagged_field_types: [children.len]type = undefined; + for (&tagged_field_types, &untagged_field_types, children) |*tagged, *untagged, *child| { + tagged.* = child.ZigType; + untagged.* = child.ExternType; + } + + const tagged_field_types_const = tagged_field_types; + const untagged_field_types_const = untagged_field_types; + const zig_type = bun.meta.TaggedUnion(&tagged_field_types_const); + const extern_type = ExternTaggedUnion(&untagged_field_types_const); + + return struct { + pub const ZigType = zig_type; + pub const ExternType = extern_type; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const tag: std.meta.Tag(ZigType) = @enumFromInt(extern_value.tag); + return switch (tag) { + inline else => |t| @unionInit( + ZigType, + @tagName(t), + children[@intFromEnum(t)].convertFromExtern( + @field(extern_value.data, @tagName(t)), + ), + ), + }; + } + }; +} + +pub fn ExternTaggedUnion(comptime field_types: []const type) type { + if (comptime field_types.len > std.math.maxInt(u8)) { + @compileError("too many union fields"); + } + return extern struct { + data: ExternUnion(field_types), + tag: u8, + }; +} + +fn ExternUnion(comptime field_types: []const type) type { + var info = @typeInfo(bun.meta.TaggedUnion(field_types)); + info.@"union".tag_type = null; + info.@"union".layout = .@"extern"; + info.@"union".decls = &.{}; + return @Type(info); +} + +pub fn BindgenArray(comptime Child: type) type { + return struct { + pub const ZigType = bun.collections.ArrayListDefault(Child.ZigType); + pub const ExternType = ExternArrayList(Child.ExternType); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const length: usize = @intCast(extern_value.length); + const capacity: usize = @intCast(extern_value.capacity); + + const data = extern_value.data orelse return .init(); + bun.assertf( + length <= capacity, + "length ({d}) should not exceed capacity ({d})", + .{ length, capacity }, + ); + var unmanaged: std.ArrayListUnmanaged(Child.ExternType) = .{ + .items = data[0..length], + .capacity = capacity, + }; + + if (comptime !bun.use_mimalloc) { + // Don't reuse memory in this case; it would be freed by the wrong allocator. + } else if (comptime Child.ZigType == Child.ExternType) { + return .fromUnmanaged(.{}, unmanaged); + } else if (comptime @sizeOf(Child.ZigType) <= @sizeOf(Child.ExternType) and + @alignOf(Child.ZigType) <= bun.allocators.mimalloc.MI_MAX_ALIGN_SIZE) + { + // We can reuse the allocation, but we still need to convert the elements. + var storage: []u8 = @ptrCast(unmanaged.allocatedSlice()); + + // Convert the elements. + for (0..length) |i| { + // Zig doesn't have a formal aliasing model, so we should be maximally + // pessimistic. + var old_elem: Child.ExternType = undefined; + @memcpy( + std.mem.asBytes(&old_elem), + storage[i * @sizeOf(Child.ExternType) ..][0..@sizeOf(Child.ExternType)], + ); + const new_elem = Child.convertFromExtern(old_elem); + @memcpy( + storage[i * @sizeOf(Child.ZigType) ..][0..@sizeOf(Child.ZigType)], + std.mem.asBytes(&new_elem), + ); + } + + const new_size_is_multiple = + comptime @sizeOf(Child.ExternType) % @sizeOf(Child.ZigType) == 0; + const new_capacity = if (comptime new_size_is_multiple) + capacity * (@sizeOf(Child.ExternType) / @sizeOf(Child.ZigType)) + else blk: { + const new_capacity = storage.len / @sizeOf(Child.ZigType); + const new_alloc_size = new_capacity * @sizeOf(Child.ZigType); + if (new_alloc_size != storage.len) { + // Allocation isn't a multiple of `@sizeOf(Child.ZigType)`; we have to + // resize it. + storage = bun.handleOom( + bun.default_allocator.realloc(storage, new_alloc_size), + ); + } + break :blk new_capacity; + }; + + const items_ptr: [*]Child.ZigType = @ptrCast(@alignCast(storage.ptr)); + const new_unmanaged: std.ArrayListUnmanaged(Child.ZigType) = .{ + .items = items_ptr[0..length], + .capacity = new_capacity, + }; + return .fromUnmanaged(.{}, new_unmanaged); + } + + defer unmanaged.deinit( + if (bun.use_mimalloc) bun.default_allocator else std.heap.raw_c_allocator, + ); + var result = bun.handleOom(ZigType.initCapacity(length)); + for (unmanaged.items) |*item| { + result.appendAssumeCapacity(Child.convertFromExtern(item.*)); + } + return result; + } + }; +} + +fn ExternArrayList(comptime Child: type) type { + return extern struct { + data: ?[*]Child, + length: c_uint, + capacity: c_uint, + }; +} + +fn BindgenExternalShared(comptime T: type) type { + return struct { + pub const ZigType = bun.ptr.ExternalShared(T); + pub const ExternType = ?*T; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } + }; +} + +pub const BindgenArrayBuffer = BindgenExternalShared(jsc.JSCArrayBuffer); +pub const BindgenBlob = BindgenExternalShared(webcore.Blob); + +const bun = @import("bun"); +const std = @import("std"); + +const jsc = bun.bun_js.jsc; +const webcore = bun.bun_js.webcore; diff --git a/src/bun.js/bindings/Bindgen.h b/src/bun.js/bindings/Bindgen.h new file mode 100644 index 0000000000..e1bcb166af --- /dev/null +++ b/src/bun.js/bindings/Bindgen.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "Bindgen/ExternTraits.h" +#include "Bindgen/IDLTypes.h" +#include "Bindgen/IDLConvertBase.h" diff --git a/src/bun.js/bindings/Bindgen/ExternTraits.h b/src/bun.js/bindings/Bindgen/ExternTraits.h new file mode 100644 index 0000000000..af9d63f8db --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternTraits.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ExternUnion.h" + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct TrivialExtern { + using ExternType = T; + + static ExternType convertToExtern(T&& cppValue) + { + return std::move(cppValue); + } +}; + +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; + +enum ExternNullPtr : std::uint8_t {}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::nullptr_t cppValue) + { + return ExternType { 0 }; + } +}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::monostate cppValue) + { + return ExternType { 0 }; + } +}; + +template +struct ExternVariant { + ExternUnion data; + std::uint8_t tag; + + static_assert(sizeof...(Args) > 0); + static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); + + explicit ExternVariant(std::variant&& variant) + : tag(static_cast(variant.index())) + { + data.initFromVariant(std::move(variant)); + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType...>; + + static ExternType convertToExtern(std::variant&& cppValue) + { + using VariantOfExtern = std::variant::ExternType...>; + return ExternType { std::visit([](auto&& arg) -> VariantOfExtern { + using ArgType = std::decay_t; + return { ExternTraits::convertToExtern(std::move(arg)) }; + }, + std::move(cppValue)) }; + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType>; + + static ExternType convertToExtern(std::optional&& cppValue) + { + using StdVariant = std::variant::ExternType>; + if (!cppValue) { + return ExternType { StdVariant { ExternNullPtr {} } }; + } + return ExternType { StdVariant { ExternTraits::convertToExtern(std::move(*cppValue)) } }; + } +}; + +template<> struct ExternTraits { + using ExternType = WTF::StringImpl*; + + static ExternType convertToExtern(WTF::String&& cppValue) + { + return cppValue.releaseImpl().leakRef(); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::EncodedJSValue; + + static ExternType convertToExtern(JSC::JSValue cppValue) + { + return JSC::JSValue::encode(cppValue); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::JSValue*; + + static ExternType convertToExtern(Bun::StrongRef&& cppValue) + { + return cppValue.release(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::Ref&& cppValue) + { + return &cppValue.leakRef(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::RefPtr&& cppValue) + { + return cppValue.leakRef(); + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/ExternUnion.h b/src/bun.js/bindings/Bindgen/ExternUnion.h new file mode 100644 index 0000000000..9a92950fdb --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternUnion.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include +#include "Macros.h" + +#define BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, ...) \ + template \ + union ExternUnion { \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + void initFromVariant( \ + std::variant&& variant) \ + { \ + const std::size_t index = variant.index(); \ + std::visit([this, index](auto&& arg) { \ + using Arg = std::decay_t; \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + }, \ + std::move(variant)); \ + } \ + } + +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_TEMPLATE_PARAM(Type) , typename Type +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD(Type) \ + static_assert(std::is_trivially_copyable_v); \ + Type alternative##Type; +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT(Type) \ + if constexpr (std::is_same_v) { \ + if (index == ::Bun::Bindgen::Detail::indexOf##Type) { \ + alternative##Type = std::move(arg); \ + return; \ + } \ + } + +namespace Bun::Bindgen { +namespace Detail { +// For use in macros. +static constexpr std::size_t indexOfT0 = 0; +static constexpr std::size_t indexOfT1 = 1; +static constexpr std::size_t indexOfT2 = 2; +static constexpr std::size_t indexOfT3 = 3; +static constexpr std::size_t indexOfT4 = 4; +static constexpr std::size_t indexOfT5 = 5; +static constexpr std::size_t indexOfT6 = 6; +static constexpr std::size_t indexOfT7 = 7; +static constexpr std::size_t indexOfT8 = 8; +static constexpr std::size_t indexOfT9 = 9; +static constexpr std::size_t indexOfT10 = 10; +static constexpr std::size_t indexOfT11 = 11; +static constexpr std::size_t indexOfT12 = 12; +static constexpr std::size_t indexOfT13 = 13; +static constexpr std::size_t indexOfT14 = 14; +static constexpr std::size_t indexOfT15 = 15; +} + +template +union ExternUnion; + +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); +} diff --git a/src/bun.js/bindings/Bindgen/ExternVectorTraits.h b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h new file mode 100644 index 0000000000..f6f62bc8f6 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h @@ -0,0 +1,149 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#if ASAN_ENABLED && __has_include() +#include +#endif + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct ExternVector { + T* data; + // WTF::Vector stores the length and capacity as `unsigned`. We can save space by using that + // instead of `std::size_t` here. + unsigned length; + unsigned capacity; +}; + +namespace Detail { +template +void asanSetBufferSizeToFullCapacity(T* buffer, std::size_t length, std::size_t capacity) +{ +#if ASAN_ENABLED + // Without this, ASan will complain if Zig touches memory in the range + // [storage + length, storage + capacity), which will always happen when freeing the + // memory in Debug mode when Zig writes 0xaa to it. + __sanitizer_annotate_contiguous_container( + buffer, // beg + buffer + capacity, // end + buffer + length, // old_mid + buffer + capacity // new_mid + ); +#endif +} +} + +template +struct ExternTraits> { +private: + using CPPType = WTF::Vector; + using ExternElement = ExternTraits::ExternType; + +public: + using ExternType = ExternVector; + + static ExternType convertToExtern(CPPType&& cppValue) + { + if constexpr (std::is_same_v) { + // We can reuse the allocation. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // This prevents the contents from being freed or destructed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + T* const buffer = vec->mutableSpan().data(); + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + + return ExternType { + .data = vec->mutableSpan().data(), + .length = static_cast(length), + .capacity = static_cast(capacity), + }; + } else if constexpr (sizeof(ExternElement) <= sizeof(T) + && alignof(ExternElement) <= MimallocMalloc::maxAlign) { + + // We can reuse the allocation, but we still need to convert the elements. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // Prevent the memory from being freed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + const std::size_t allocSize = capacity * sizeof(T); + + T* const buffer = vec->mutableSpan().data(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + std::byte* storage = reinterpret_cast(buffer); + + // Convert the elements. + for (std::size_t i = 0; i < length; ++i) { + T* oldPtr = std::launder(reinterpret_cast(storage + i * sizeof(T))); + ExternElement newElem { ExternTraits::convertToExtern(std::move(*oldPtr)) }; + oldPtr->~T(); + new (storage + i * sizeof(ExternElement)) ExternElement { std::move(newElem) }; + } + + std::size_t newCapacity {}; + std::size_t newAllocSize {}; + + static constexpr bool newSizeIsMultiple = sizeof(T) % sizeof(ExternElement) == 0; + if (newSizeIsMultiple) { + newCapacity = capacity * (sizeof(T) / sizeof(ExternElement)); + newAllocSize = allocSize; + } else { + newCapacity = allocSize / sizeof(ExternElement); + newAllocSize = newCapacity * sizeof(ExternElement); + if (newAllocSize != allocSize) { + static_assert(std::is_trivially_copyable_v); + storage = static_cast( + MimallocMalloc::realloc(storage, newCapacity * sizeof(ExternElement))); + } + } + +#if __cpp_lib_start_lifetime_as >= 202207L + ExternElement* data = std::start_lifetime_as_array(storage, newCapacity); +#else + // We need to start the lifetime of an object of type "array of `capacity` + // `ExternElement`" without invalidating the object representation. Without + // `std::start_lifetime_as_array`, one way to do this is to use a no-op `memmove`, + // which implicitly creates objects, plus `std::launder` to obtain a pointer to + // the created object. + std::memmove(storage, storage, newAllocSize); + ExternElement* data = std::launder(reinterpret_cast(storage)); +#endif + return ExternType { + .data = data, + .length = static_cast(length), + .capacity = static_cast(newCapacity), + }; + } + + const std::size_t length = cppValue.size(); + const std::size_t newAllocSize = sizeof(ExternElement) * length; + ExternElement* memory = reinterpret_cast( + alignof(ExternElement) > MimallocMalloc::maxAlign + ? MimallocMalloc::alignedMalloc(newAllocSize, alignof(ExternElement)) + : MimallocMalloc::malloc(newAllocSize)); + for (std::size_t i = 0; i < length; ++i) { + new (memory + i) ExternElement { + ExternTraits::convertToExtern(std::move(cppValue[i])), + }; + } + return ExternType { + .data = memory, + .length = static_cast(length), + .capacity = static_cast(length), + }; + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLConvert.h b/src/bun.js/bindings/Bindgen/IDLConvert.h new file mode 100644 index 0000000000..669b74c2d8 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvert.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include "IDLTypes.h" +#include "IDLConvertBase.h" + +namespace Bun { +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("any"); +}; +} + +template<> struct WebCore::Converter + : WebCore::DefaultConverter { + + static Bun::StrongRef convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return Bun::StrongRef { Bun__StrongRef__new(&globalObject, JSC::JSValue::encode(value)) }; + } +}; diff --git a/src/bun.js/bindings/Bindgen/IDLConvertBase.h b/src/bun.js/bindings/Bindgen/IDLConvertBase.h new file mode 100644 index 0000000000..e73a664c1f --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvertBase.h @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include + +namespace Bun::Bindgen { + +namespace Detail { + +template +struct ContextBase : Bun::IDLConversionContextBase { + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError( + &global, + scope, + ErrorCode::ERR_INVALID_ARG_TYPE, + std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError(&global, scope, ErrorCode::ERR_OUT_OF_RANGE, std::forward(message)); + } +}; + +template +struct ElementOf : ContextBase> { + using ElementContext = ElementOf>; + + explicit ElementOf(Parent parent) + : m_parent(std::move(parent)) + { + } + + auto source() + { + return WTF::makeString("element of "_s, m_parent.source()); + } + +private: + Parent m_parent; +}; + +} + +// Conversion context where the name of the value being converted is specified as an +// ASCIILiteral. Calls Bun::throwError. +struct LiteralConversionContext : Detail::ContextBase { + using ElementContext = Detail::ElementOf; + + explicit consteval LiteralConversionContext(WTF::ASCIILiteral name) + : m_name(name) + { + } + + auto source() + { + return m_name; + } + +private: + const WTF::ASCIILiteral m_name; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLTypes.h b/src/bun.js/bindings/Bindgen/IDLTypes.h new file mode 100644 index 0000000000..d0275d9fb0 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLTypes.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include + +namespace Bun::Bindgen { + +// See also: Bun::IDLRawAny +struct IDLStrongAny : WebCore::IDLType { + using NullableType = Bun::StrongRef; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +template +struct IsIDLStrongAny : std::integral_constant::value> {}; + +// Dictionaries that contain raw `JSValue`s must live on the stack. +template +struct IDLStackOnlyDictionary : WebCore::IDLType { + using SequenceStorageType = void; + using ParameterType = const T&; + using NullableParameterType = const T&; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/Macros.h b/src/bun.js/bindings/Bindgen/Macros.h new file mode 100644 index 0000000000..f70d2d8120 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/Macros.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +#define BUN_BINDGEN_DETAIL_FOREACH(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH2(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH2(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH3(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH3(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH4(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH4(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH5(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH5(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH6(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH6(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH7(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH7(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH8(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH8(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH9(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH9(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH10(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH10(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH11(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH11(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH12(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH12(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH13(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH13(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH14(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH14(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH15(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH15(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH16(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH16(macro, arg, ...) macro(arg) \ + __VA_OPT__(static_assert(false, "Bindgen/Macros.h: too many items")) diff --git a/src/bun.js/bindings/BunIDLConvert.h b/src/bun.js/bindings/BunIDLConvert.h new file mode 100644 index 0000000000..4aa4963f7f --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvert.h @@ -0,0 +1,280 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "BunIDLConvertNumbers.h" +#include "BunIDLHumanReadable.h" +#include "JSDOMConvert.h" +#include +#include +#include + +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static JSC::JSValue convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return value; + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) { + return nullptr; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNull(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) { + return std::monostate {}; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotUndefined(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isBoolean()) { + return value.asBoolean(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBoolean(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return value.toWTFString(&globalObject); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotString(globalObject, scope); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + template + static std::optional::ImplementationType> tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (JSC::isJSArray(value)) { + return Bun::convert::Base>(globalObject, value, ctx); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNotArray(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { + return jsBuffer; + } + if (auto* jsView = JSC::jsDynamicCast(value)) { + return jsView->unsharedBuffer(); + } + if (auto* jsDataView = JSC::jsDynamicCast(value)) { + return jsDataView->unsharedBuffer(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBufferSource(globalObject, scope); + } +}; + +template +struct WebCore::Converter> + : Bun::DefaultTryConverter> { +private: + using Base = Bun::DefaultTryConverter>; + +public: + using typename Base::ReturnType; + + static constexpr bool conversionHasSideEffects + = (WebCore::Converter::conversionHasSideEffects || ...); + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + using Last = std::tuple_element_t>; + if constexpr (requires { + WebCore::Converter::tryConvert(globalObject, value, ctx); + }) { + return Base::convert(globalObject, value, ctx); + } else { + return convertWithInfallibleLast( + globalObject, + value, + ctx, + std::make_index_sequence {}); + } + } + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + }; + (tryAlternative.template operator()() || ...); + return result; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNoMatchInUnion(globalObject, scope); + } + +private: + template + static ReturnType convertWithInfallibleLast( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx, + std::index_sequence) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + using T = std::tuple_element_t>; + if constexpr (index == sizeof...(IDL) - 1) { + auto alternativeResult = Bun::convertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + result = ReturnType { std::move(alternativeResult) }; + return true; + } else { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + } + }; + bool done = (tryAlternative.template operator()() || ...); + ASSERT(done); + if (!result.has_value()) { + // Exception + return {}; + } + return std::move(*result); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertBase.h b/src/bun.js/bindings/BunIDLConvertBase.h new file mode 100644 index 0000000000..7d53546df0 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBase.h @@ -0,0 +1,77 @@ +#pragma once +#include "BunIDLConvertContext.h" +#include "JSDOMConvertBase.h" +#include +#include +#include +#include + +namespace Bun { + +template +typename WebCore::Converter::ReturnType convertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::convert(globalObject, value, ctx); + } else { + return WebCore::Converter::convert(globalObject, value); + } +} + +template +std::optional::ReturnType> tryConvertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } else { + return WebCore::Converter::tryConvert(globalObject, value); + } +} + +template +struct DefaultContextConverter : WebCore::DefaultConverter { + using typename WebCore::DefaultConverter::ReturnType; + + static constexpr bool takesContext = true; + + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::convert(globalObject, value, ctx); + } +}; + +template +struct DefaultTryConverter : DefaultContextConverter { + using typename DefaultContextConverter::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, {}); + if (result.has_value()) { + return std::move(*result); + } + WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + return ReturnType {}; + } + + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertBlob.h b/src/bun.js/bindings/BunIDLConvertBlob.h new file mode 100644 index 0000000000..2e57c6f453 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBlob.h @@ -0,0 +1,36 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "blob.h" +#include "ZigGeneratedClasses.h" + +namespace Bun { +struct IDLBlobRef : IDLBunInterface {}; +} + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (auto* jsBlob = JSC::jsDynamicCast(value)) { + if (void* wrapped = jsBlob->wrapped()) { + return static_cast(wrapped); + } + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBlob(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertContext.h b/src/bun.js/bindings/BunIDLConvertContext.h new file mode 100644 index 0000000000..5dde10a730 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertContext.h @@ -0,0 +1,252 @@ +#pragma once +#include "BunIDLHumanReadable.h" +#include +#include +#include + +namespace Bun { + +namespace Detail { +struct IDLConversionContextMarker {}; +} + +template +concept IDLConversionContext = std::derived_from; + +namespace Detail { +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; +} + +template +struct IDLConversionContextBase : Detail::IDLConversionContextMarker { + void throwRequired(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is required"_s); + } + + void throwNumberNotFinite(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be finite (received "_s, value, ')')); + } + + void throwNumberNotInteger(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be an integer (received "_s, value, ')')); + } + + template + void throwIntegerOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Int value, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + "] (received "_s, + value, + ')')); + } + + template + void throwBigIntOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + ']')); + } + + void throwNotNumber(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a number"_s); + } + + void throwNotString(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a string"_s); + } + + void throwNotBoolean(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a boolean"_s); + } + + void throwNotObject(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an object"_s); + } + + void throwNotNull(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "null"_s); + } + + void throwNotUndefined(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "undefined"_s); + } + + void throwNotBufferSource(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an ArrayBuffer or TypedArray"_s); + } + + void throwNotBlob(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a Blob"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an array"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe( + global, + scope, + WTF::makeString("an array of "_s, idlHumanReadableName())); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwRangeErrorWithPredicate(global, scope, "is not a valid enumeration value"_s); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, idlHumanReadableName()); + } + + template + requires(sizeof...(Alternatives) > 0) + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + using Union = Detail::IDLUnionForDiagnostic::Type; + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be of type "_s, idlHumanReadableName())); + } + + template + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is of an unsupported type"_s); + } + + template + void throwTypeMustBe( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& expectedNounPhrase) + { + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be "_s, std::forward(expectedNounPhrase))); + } + + template + void throwTypeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericTypeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwRangeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericRangeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwTypeError(&global, scope, std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwRangeError(&global, scope, std::forward(message)); + } + + using ElementContext = Derived; + + // When converting a sequence, the result of this function will be used as the context for + // converting each element of the sequence. + auto contextForElement() + { + return typename Derived::ElementContext { derived() }; + } + +private: + Derived& derived() { return *static_cast(this); } +}; + +// Default conversion context: throws a plain TypeError or RangeError with the message +// "value must be ...". See also Bindgen::LiteralConversionContext, which uses Bun::throwError. +struct DefaultConversionContext : IDLConversionContextBase { + WTF::ASCIILiteral source() { return "value"_s; } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertNumbers.h b/src/bun.js/bindings/BunIDLConvertNumbers.h new file mode 100644 index 0000000000..d2e5025220 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertNumbers.h @@ -0,0 +1,174 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include +#include +#include +#include +#include + +namespace Bun::Detail { +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::int64_t minInt = std::numeric_limits::min(); + static constexpr std::int64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigInt64(value)); + } + return std::nullopt; +} + +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::uint64_t minInt = 0; + static constexpr std::uint64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigUInt64(value)); + } + return std::nullopt; +} +} + +template + requires(sizeof(T) <= sizeof(std::uint64_t)) +struct WebCore::Converter> + : Bun::DefaultTryConverter> { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + static constexpr auto minInt = std::numeric_limits::min(); + static constexpr auto maxInt = std::numeric_limits::max(); + static constexpr auto maxSafeInteger = 9007199254740991LL; + + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (value.isInt32()) { + auto intValue = value.asInt32(); + if (intValue >= minInt && intValue <= maxInt) { + return intValue; + } + ctx.throwIntegerOutOfRange(globalObject, scope, intValue, minInt, maxInt); + return {}; + } + + using Largest = std::conditional_t, std::int64_t, std::uint64_t>; + if (value.isBigInt()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + return *result; + } + if constexpr (maxInt < std::numeric_limits::max()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); + } + } + ctx.throwBigIntOutOfRange(globalObject, scope, minInt, maxInt); + return {}; + } + + if (!value.isNumber()) { + return std::nullopt; + } + + double number = value.asNumber(); + if (number > maxSafeInteger || number < -maxSafeInteger) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + auto intVal = static_cast(number); + if (intVal != number) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + if constexpr (maxInt >= static_cast(maxSafeInteger)) { + if (std::signed_integral || intVal >= 0) { + return static_cast(intVal); + } + } else if (intVal >= static_cast(minInt) + && intVal <= static_cast(maxInt)) { + return static_cast(intVal); + } + ctx.throwIntegerOutOfRange(globalObject, scope, intVal, minInt, maxInt); + return {}; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isNumber()) { + return value.asNumber(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!value.isNumber()) { + return std::nullopt; + } + double number = value.asNumber(); + if (std::isnan(number) || std::isinf(number)) { + ctx.throwNumberNotFinite(globalObject, scope, number); + return std::nullopt; + } + return number; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLHumanReadable.h b/src/bun.js/bindings/BunIDLHumanReadable.h new file mode 100644 index 0000000000..91e322d8d5 --- /dev/null +++ b/src/bun.js/bindings/BunIDLHumanReadable.h @@ -0,0 +1,139 @@ +#pragma once +#include "BunIDLTypes.h" +#include "ConcatCStrings.h" +#include +#include +#include + +namespace Bun { + +template +struct IDLHumanReadableName; + +template +concept HasIDLHumanReadableName = requires { IDLHumanReadableName::humanReadableName; }; + +struct BaseIDLHumanReadableName { + static constexpr bool isDisjunction = false; + static constexpr bool hasPreposition = false; +}; + +template +static constexpr WTF::ASCIILiteral idlHumanReadableName() +{ + static_assert(IDLHumanReadableName::humanReadableName.back() == '\0'); + return WTF::ASCIILiteral::fromLiteralUnsafe( + IDLHumanReadableName::humanReadableName.data()); +} + +namespace Detail { +template +static constexpr auto nestedHumanReadableName() +{ + static constexpr auto& name = IDLHumanReadableName::humanReadableName; + if constexpr (IDLHumanReadableName::isDisjunction) { + return Bun::concatCStrings("<", name, ">"); + } else { + return name; + } +} + +template +static constexpr auto separatorForHumanReadableBinaryDisjunction() +{ + if constexpr (IDLHumanReadableName::hasPreposition) { + return std::to_array(", or "); + } else { + return std::to_array(" or "); + } +} +} + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("null"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("undefined"); +}; + +template + requires std::derived_from +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("boolean"); +}; + +template + requires WebCore::IsIDLInteger::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("integer"); +}; + +template + requires WebCore::IsIDLFloatingPoint::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("number"); +}; + +template + requires WebCore::IsIDLString::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("string"); +}; + +// Will generally be overridden by each specific enumeration type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("enumeration (string)"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "null"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "undefined"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool hasPreposition = true; + static constexpr auto humanReadableName + = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); +}; + +// Will generally be overridden by each specific dictionary type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("dictionary (object)"); +}; + +template +struct IDLHumanReadableName> : IDLHumanReadableName {}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = sizeof...(IDL) > 1; + static constexpr auto humanReadableName + = Bun::joinCStringsAsList(Detail::nestedHumanReadableName()...); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("Blob"); +}; + +} diff --git a/src/bun.js/bindings/BunIDLTypes.h b/src/bun.js/bindings/BunIDLTypes.h new file mode 100644 index 0000000000..aab971072b --- /dev/null +++ b/src/bun.js/bindings/BunIDLTypes.h @@ -0,0 +1,87 @@ +#pragma once +#include "IDLTypes.h" +#include +#include +#include +#include +#include + +namespace WTF { +struct CrashOnOverflow; +} + +namespace Bun { + +struct MimallocMalloc; + +// Like `IDLAny`, but always stored as a raw `JSValue`. This should only be +// used in contexts where the `JSValue` will be stored on the stack. +struct IDLRawAny : WebCore::IDLType { + // Storage in a sequence is explicitly unsupported, as this would create a + // `Vector`, whose contents are invisible to the GC. + using SequenceStorageType = void; + using NullableType = JSC::JSValue; + using NullableParameterType = JSC::JSValue; + using NullableInnerParameterType = JSC::JSValue; + static NullableType nullValue() { return JSC::jsUndefined(); } + static bool isNullValue(const NullableType& value) { return value.isUndefined(); } + static ImplementationType extractValueFromNullable(const NullableType& value) { return value; } + static constexpr auto humanReadableName() { return std::to_array("any"); } +}; + +// For use in unions, to represent a nullable union. +struct IDLStrictNull : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("null"); } +}; + +// For use in unions, to represent an optional union. +struct IDLStrictUndefined : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("undefined"); } +}; + +template +struct IDLStrictInteger : WebCore::IDLInteger {}; +struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; +struct IDLFiniteDouble : WebCore::IDLDouble {}; +struct IDLStrictBoolean : WebCore::IDLBoolean {}; +struct IDLStrictString : WebCore::IDLDOMString {}; + +template +struct IDLOrderedUnion : WebCore::IDLType> {}; + +namespace Detail { +template +using IDLMimallocSequence = WebCore::IDLSequence< + IDL, + WTF::Vector< + typename IDL::SequenceStorageType, + 0, + WTF::CrashOnOverflow, + 16, + MimallocMalloc>>; +} + +template +struct IDLArray : Detail::IDLMimallocSequence { + using Base = Detail::IDLMimallocSequence; +}; + +template> +struct IDLBunInterface : WebCore::IDLType, RefDerefTraits>> { + using NullableType = WTF::RefPtr, RefDerefTraits>; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +struct IDLArrayBufferRef : IDLBunInterface {}; + +// Defined in BunIDLConvertBlob.h +struct IDLBlobRef; + +} diff --git a/src/bun.js/bindings/ConcatCStrings.h b/src/bun.js/bindings/ConcatCStrings.h new file mode 100644 index 0000000000..2b0a65cd73 --- /dev/null +++ b/src/bun.js/bindings/ConcatCStrings.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include + +namespace Bun { + +namespace Detail { +template +static constexpr bool isCharArray = false; + +template +static constexpr bool isCharArray = true; + +template +static constexpr bool isCharArray> = true; + +// Intentionally not defined, to force consteval to fail. +void stringIsNotNullTerminated(); +} + +template + requires(Detail::isCharArray> && ...) +consteval auto concatCStrings(T&&... nullTerminatedCharArrays) +{ + std::array) - 1) + ...) + 1> result; + auto it = result.begin(); + auto append = [&it](auto&& arg) { + if (std::end(arg)[-1] != '\0') { + // This will cause consteval to fail. + Detail::stringIsNotNullTerminated(); + } + it = std::copy(std::begin(arg), std::end(arg) - 1, it); + }; + (append(nullTerminatedCharArrays), ...); + result.back() = '\0'; + return result; +} + +namespace Detail { +template +consteval auto listSeparatorForIndex() +{ + if constexpr (length == 2) { + return std::to_array(" or "); + } else if constexpr (index == length - 1) { + return std::to_array(", or "); + } else { + return std::to_array(", "); + } +} + +template +consteval auto joinCStringsAsList(std::index_sequence, T&& first, Rest&&... rest) +{ + return concatCStrings( + first, + concatCStrings( + listSeparatorForIndex(), + std::forward(rest))...); +} +} + +template + requires(Detail::isCharArray> && ...) +consteval auto joinCStringsAsList(T&&... nullTerminatedCharArrays) +{ + if constexpr (sizeof...(T) == 0) { + return std::to_array(""); + } else { + return Detail::joinCStringsAsList( + std::make_index_sequence {}, + std::forward(nullTerminatedCharArrays)...); + } +} + +} diff --git a/src/bun.js/bindings/IDLTypes.h b/src/bun.js/bindings/IDLTypes.h index 3ebea7e596..4e9cbd03c0 100644 --- a/src/bun.js/bindings/IDLTypes.h +++ b/src/bun.js/bindings/IDLTypes.h @@ -28,6 +28,7 @@ #include "StringAdaptors.h" #include #include +#include #include #include #include @@ -76,6 +77,7 @@ struct IDLType { static NullableType nullValue() { return std::nullopt; } static bool isNullValue(const NullableType& value) { return !value; } static ImplementationType extractValueFromNullable(const NullableType& value) { return value.value(); } + static ImplementationType extractValueFromNullable(NullableType&& value) { return std::move(value.value()); } template using NullableTypeWithLessPadding = Markable; template @@ -84,6 +86,8 @@ struct IDLType { static bool isNullType(const NullableTypeWithLessPadding& value) { return !value; } template static ImplementationType extractValueFromNullable(const NullableTypeWithLessPadding& value) { return value.value(); } + template + static ImplementationType extractValueFromNullable(NullableTypeWithLessPadding&& value) { return std::move(value.value()); } }; // IDLUnsupportedType is a special type that serves as a base class for currently unsupported types. @@ -94,8 +98,12 @@ struct IDLUnsupportedType : IDLType { struct IDLNull : IDLType { }; +// See also: Bun::IDLRawAny, Bun::Bindgen::IDLStrongAny struct IDLAny : IDLType> { - using SequenceStorageType = JSC::JSValue; + // SequenceStorageType must be left as JSC::Strong; otherwise + // IDLSequence would yield a Vector, whose contents + // are invisible to the GC. + // [do not uncomment] using SequenceStorageType = JSC::JSValue; using ParameterType = JSC::JSValue; using NullableParameterType = JSC::JSValue; @@ -247,18 +255,23 @@ template struct IDLNullable : IDLType { template static inline auto extractValueFromNullable(U&& value) -> decltype(T::extractValueFromNullable(std::forward(value))) { return T::extractValueFromNullable(std::forward(value)); } }; -template struct IDLSequence : IDLType> { - using InnerType = T; - - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; +// Like `IDLNullable`, but does not permit `null`, only `undefined`. +template struct IDLOptional : IDLNullable { }; -template struct IDLFrozenArray : IDLType> { +template> +struct IDLSequence : IDLType { using InnerType = T; - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; + using ParameterType = const VectorType&; + using NullableParameterType = const std::optional&; +}; + +template struct IDLFrozenArray : IDLType> { + using InnerType = T; + + using ParameterType = const Vector&; + using NullableParameterType = const std::optional>&; }; template struct IDLRecord : IDLType>> { @@ -282,6 +295,31 @@ template struct IDLUnion : IDLType> { using TypeList = brigand::list; + // If `SequenceStorageType` and `ImplementationType` are different for any + // type in `Ts`, this union should not be allowed to be stored in a + // sequence. Sequence elements are stored on the heap (in a `Vector`), so + // if `SequenceStorageType` and `ImplementationType` differ for some type, + // this is an indication that the `ImplementationType` should not be stored + // on the heap (e.g., because it is or contains a raw `JSValue`). When this + // is the case, we indicate that the union itself should not be stored on + // the heap by defining its `SequenceStorageType` as void. + // + // Note that we cannot define `SequenceStorageType` as + // `std::variant`, as this would cause + // sequence conversion to fail to compile, because + // `std::variant` is not convertible to + // `std::variant`. + // + // A potential avenue for future work would be to extend the IDL type + // traits interface to allow defining custom conversions from + // `ImplementationType` to `SequenceStorageType`, and to properly propagate + // `SequenceStorageType` in other types like `IDLDictionary`; however, one + // should keep in mind that some types may still disallow heap storage + // entirely by defining `SequenceStorageType` as void. + using SequenceStorageType = std::conditional_t< + (std::is_same_v && ...), + std::variant, + void>; using ParameterType = const std::variant&; using NullableParameterType = const std::optional>&; }; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index a8afc337f4..76b66be73b 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1133,25 +1133,16 @@ pub const JSValue = enum(i64) { return bun.cpp.JSC__JSValue__toMatch(this, global, other); } - extern fn JSC__JSValue__asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; - pub fn asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool { - return JSC__JSValue__asArrayBuffer_(this, global, out); - } + extern fn JSC__JSValue__asArrayBuffer(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; pub fn asArrayBuffer(this: JSValue, global: *JSGlobalObject) ?ArrayBuffer { - var out: ArrayBuffer = .{ - .offset = 0, - .len = 0, - .byte_len = 0, - .shared = false, - .typed_array_type = .Uint8Array, - }; - - if (this.asArrayBuffer_(global, &out)) { - out.value = this; + var out: ArrayBuffer = undefined; + // `ptr` might not get set if the ArrayBuffer is empty, so make sure it starts out with a + // defined value. + out.ptr = &.{}; + if (JSC__JSValue__asArrayBuffer(this, global, &out)) { return out; } - return null; } extern fn JSC__JSValue__fromInt64NoTruncate(globalObject: *JSGlobalObject, i: i64) JSValue; diff --git a/src/bun.js/bindings/MimallocWTFMalloc.h b/src/bun.js/bindings/MimallocWTFMalloc.h new file mode 100644 index 0000000000..32e43713ba --- /dev/null +++ b/src/bun.js/bindings/MimallocWTFMalloc.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include +#include +#include +#include "mimalloc.h" +#include "mimalloc/types.h" + +namespace Bun { +// For use with WTF types like WTF::Vector. +struct MimallocMalloc { +#if USE(MIMALLOC) + static constexpr std::size_t maxAlign = MI_MAX_ALIGN_SIZE; +#else + static constexpr std::size_t maxAlign = alignof(std::max_align_t); +#endif + + static void* malloc(std::size_t size) + { + void* result = tryMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_malloc(size); +#else + return std::malloc(size); +#endif + } + + static void* zeroedMalloc(std::size_t size) + { + void* result = tryZeroedMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryZeroedMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_zalloc(size); +#else + return std::calloc(size, 1); +#endif + } + + static void* alignedMalloc(std::size_t size, std::size_t alignment) + { + void* result = tryAlignedMalloc(size, alignment); + if (!result) CRASH(); + return result; + } + + static void* tryAlignedMalloc(std::size_t size, std::size_t alignment) + { + ASSERT(alignment > 0); + ASSERT((alignment & (alignment - 1)) == 0); // ensure power of two + ASSERT(((alignment - 1) & size) == 0); // ensure size multiple of alignment +#if USE(MIMALLOC) + return mi_malloc_aligned(size, alignment); +#elif !OS(WINDOWS) + return std::aligned_alloc(alignment, size); +#else + LOG_ERROR("cannot allocate memory with alignment %zu", alignment); + return nullptr; +#endif + } + + static void* realloc(void* p, std::size_t size) + { + void* result = tryRealloc(p, size); + if (!result) CRASH(); + return result; + } + + static void* tryRealloc(void* p, std::size_t size) + { +#if USE(MIMALLOC) + return mi_realloc(p, size); +#else + return std::realloc(p, size); +#endif + } + + static void free(void* p) + { +#if USE(MIMALLOC) + mi_free(p); +#else + std::free(p); +#endif + } + + static constexpr ALWAYS_INLINE std::size_t nextCapacity(std::size_t capacity) + { + return std::max(capacity + capacity / 2, capacity + 1); + } +}; +} diff --git a/src/bun.js/bindings/Strong.cpp b/src/bun.js/bindings/StrongRef.cpp similarity index 98% rename from src/bun.js/bindings/Strong.cpp rename to src/bun.js/bindings/StrongRef.cpp index d4ce228f4a..8466df5cfa 100644 --- a/src/bun.js/bindings/Strong.cpp +++ b/src/bun.js/bindings/StrongRef.cpp @@ -1,4 +1,5 @@ #include "root.h" +#include "StrongRef.h" #include #include #include "BunClientData.h" diff --git a/src/bun.js/bindings/StrongRef.h b/src/bun.js/bindings/StrongRef.h new file mode 100644 index 0000000000..9726d6895a --- /dev/null +++ b/src/bun.js/bindings/StrongRef.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +extern "C" void Bun__StrongRef__delete(JSC::JSValue* _Nonnull handleSlot); +extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot); +extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" void Bun__StrongRef__clear(JSC::JSValue* _Nonnull handleSlot); + +namespace Bun { + +struct StrongRefDeleter { + // `std::unique_ptr` will never call this with a null pointer. + void operator()(JSC::JSValue* _Nonnull handleSlot) + { + Bun__StrongRef__delete(handleSlot); + } +}; + +using StrongRef = std::unique_ptr; + +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 21141e1935..76d2538f59 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -58,6 +58,7 @@ #include "AddEventListenerOptions.h" #include "AsyncContextFrame.h" #include "BunClientData.h" +#include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" #include "BunPlugin.h" @@ -2080,10 +2081,9 @@ extern "C" bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, RETURN_IF_EXCEPTION(scope, false); if (!returnedValue) return false; - auto results = Detail::SequenceConverter::convert(*lexicalGlobalObject, *returnedValue); + auto results = convert>>(*lexicalGlobalObject, *returnedValue); RETURN_IF_EXCEPTION(scope, false); - ASSERT(results.size() == 2); *possibleReadableStream1 = JSValue::encode(results[0]); *possibleReadableStream2 = JSValue::encode(results[1]); return true; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index df1ddfe46e..e41c9d6979 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -32,6 +32,7 @@ #include "WebCoreJSBuiltins.h" #include "JavaScriptCore/AggregateError.h" +#include "JavaScriptCore/ArrayBufferView.h" #include "JavaScriptCore/BytecodeIndex.h" #include "JavaScriptCore/CodeBlock.h" #include "JavaScriptCore/Completion.h" @@ -3023,16 +3024,19 @@ JSC::EncodedJSValue JSC__JSValue__values(JSC::JSGlobalObject* globalObject, JSC: return JSValue::encode(JSC::objectValues(vm, globalObject, value)); } -bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, - Bun__ArrayBuffer* arg2) +bool JSC__JSValue__asArrayBuffer( + JSC::EncodedJSValue encodedValue, + JSC::JSGlobalObject* globalObject, + Bun__ArrayBuffer* out) { - ASSERT_NO_PENDING_EXCEPTION(arg1); - JSC::JSValue value = JSC::JSValue::decode(JSValue0); + ASSERT_NO_PENDING_EXCEPTION(globalObject); + JSC::JSValue value = JSC::JSValue::decode(encodedValue); if (!value || !value.isCell()) [[unlikely]] { return false; } auto type = value.asCell()->type(); + void* data = nullptr; switch (type) { case JSC::JSType::Uint8ArrayType: @@ -3048,60 +3052,56 @@ bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObj case JSC::JSType::Float64ArrayType: case JSC::JSType::BigInt64ArrayType: case JSC::JSType::BigUint64ArrayType: { - JSC::JSArrayBufferView* typedArray = JSC::jsCast(value); - arg2->len = typedArray->length(); - arg2->byte_len = typedArray->byteLength(); - // the offset is already set by vector() - // https://github.com/oven-sh/bun/issues/561 - arg2->offset = 0; - arg2->cell_type = type; - arg2->ptr = (char*)typedArray->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; + JSC::JSArrayBufferView* view = JSC::jsCast(value); + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = type; + out->shared = view->isShared(); + break; } case JSC::JSType::ArrayBufferType: { - JSC::ArrayBuffer* typedArray = JSC::jsCast(value)->impl(); - arg2->len = typedArray->byteLength(); - arg2->byte_len = typedArray->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)typedArray->data(); - arg2->shared = typedArray->isShared(); - arg2->_value = JSValue::encode(value); - return true; + JSC::ArrayBuffer* buffer = JSC::jsCast(value)->impl(); + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + break; } case JSC::JSType::ObjectType: case JSC::JSType::FinalObjectType: { if (JSC::JSArrayBufferView* view = JSC::jsDynamicCast(value)) { - arg2->len = view->length(); - arg2->byte_len = view->byteLength(); - arg2->offset = 0; - arg2->cell_type = view->type(); - arg2->ptr = (char*)view->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; - } - - if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = view->type(); + out->shared = view->isShared(); + } else if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { JSC::ArrayBuffer* buffer = jsBuffer->impl(); if (!buffer) return false; - arg2->len = buffer->byteLength(); - arg2->byte_len = buffer->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)buffer->data(); - arg2->_value = JSValue::encode(value); - return true; + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + } else { + return false; } break; } default: { - break; + return false; } } - - return false; + out->_value = JSValue::encode(value); + if (data) { + // Avoid setting `ptr` to null; the corresponding Zig field is a non-optional pointer. + // The caller should have already set `ptr` to a zero-length array. + out->ptr = static_cast(data); + } + return true; } CPP_DECL JSC::EncodedJSValue JSC__JSValue__createEmptyArray(JSC::JSGlobalObject* arg0, size_t length) @@ -6885,3 +6885,19 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC: return lineColumn.line; } + +extern "C" void JSC__ArrayBuffer__ref(JSC::ArrayBuffer* self) { self->ref(); } +extern "C" void JSC__ArrayBuffer__deref(JSC::ArrayBuffer* self) { self->deref(); } +extern "C" void JSC__ArrayBuffer__asBunArrayBuffer(JSC::ArrayBuffer* self, Bun__ArrayBuffer* out) +{ + const std::size_t byteLength = self->byteLength(); + if (void* data = self->data()) { + // Avoid setting `ptr` to null; it's a non-optional pointer in Zig. + out->ptr = static_cast(data); + } + out->len = byteLength; + out->byte_len = byteLength; + out->_value = 0; + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = self->isShared(); +} diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 5020140860..b5f56ffa0a 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -322,11 +322,10 @@ BunString toStringView(WTF::StringView view); typedef struct { char* ptr; - size_t offset; size_t len; size_t byte_len; - uint8_t cell_type; int64_t _value; + uint8_t cell_type; bool shared; } Bun__ArrayBuffer; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index f02d203054..59c5a0b4a0 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -195,7 +195,7 @@ CPP_DECL uint32_t JSC__JSMap__size(JSC::JSMap* arg0, JSC::JSGlobalObject* arg1); #pragma mark - JSC::JSValue CPP_DECL void JSC__JSValue__then(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2, SYSV_ABI JSC::EncodedJSValue(* ArgFn3)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1), SYSV_ABI JSC::EncodedJSValue(* ArgFn4)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); -CPP_DECL bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); +CPP_DECL bool JSC__JSValue__asArrayBuffer(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); CPP_DECL unsigned char JSC__JSValue__asBigIntCompare(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2); CPP_DECL JSC::JSCell* JSC__JSValue__asCell(JSC::EncodedJSValue JSValue0); CPP_DECL JSC::JSInternalPromise* JSC__JSValue__asInternalPromise(JSC::EncodedJSValue JSValue0); diff --git a/src/bun.js/bindings/webcore/JSDOMConvert.h b/src/bun.js/bindings/webcore/JSDOMConvert.h index 24c95835e2..c8ebcbac44 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvert.h +++ b/src/bun.js/bindings/webcore/JSDOMConvert.h @@ -39,6 +39,7 @@ #include "JSDOMConvertNullable.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertObject.h" +#include "JSDOMConvertOptional.h" #include "JSDOMConvertRecord.h" #include "JSDOMConvertSequences.h" #include "JSDOMConvertSerializedScriptValue.h" diff --git a/src/bun.js/bindings/webcore/JSDOMConvertBase.h b/src/bun.js/bindings/webcore/JSDOMConvertBase.h index cd4b872c28..05233c9cc3 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertBase.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertBase.h @@ -268,6 +268,8 @@ template struct DefaultConverter { // is something having a converter that does JSC::JSValue::toBoolean. // toBoolean() in JS can't call arbitrary functions. static constexpr bool conversionHasSideEffects = true; + + static constexpr bool takesContext = false; }; // Conversion from JSValue -> Implementation for variadic arguments diff --git a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h index 87f1a1c468..305063eb34 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h @@ -31,11 +31,21 @@ namespace WebCore { // Specialized by generated code for IDL dictionary conversion. -template T convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); +template T convertDictionary(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value); template struct Converter> : DefaultConverter> { using ReturnType = T; + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value) + { + if (value.isObject()) { + return convert(lexicalGlobalObject, value); + } + return std::nullopt; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { return convertDictionary(lexicalGlobalObject, value); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h index 71df8e0298..52ef8d2bf9 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h @@ -28,6 +28,7 @@ #include "IDLTypes.h" #include "JSDOMConvertBase.h" #include "JSDOMGlobalObject.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -41,7 +42,42 @@ template ASCIILiteral expectedEnumerationValues(); template JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, T); template struct Converter> : DefaultConverter> { + static constexpr bool takesContext = true; + + // `tryConvert` for enumerations is strict: it returns null if the value is not a string. + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return parseEnumeration(lexicalGlobalObject, value); + } + return std::nullopt; + } + + // When converting with Context, the conversion is stricter: non-strings are disallowed. + template + static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + if (!value.isString()) { + ctx.throwNotString(lexicalGlobalObject, throwScope); + return {}; + } + auto result = parseEnumeration(lexicalGlobalObject, value); + RETURN_IF_EXCEPTION(throwScope, {}); + if (result.has_value()) { + return std::move(*result); + } + ctx.template throwBadEnumValue>(lexicalGlobalObject, throwScope); + return {}; + } + template + requires(!Bun::IDLConversionContext>) static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = JSC::getVM(&lexicalGlobalObject); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h index 549d126bb4..40821ca8d7 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h @@ -30,6 +30,7 @@ #include "JSDOMConvertInterface.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertStrings.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -58,6 +59,10 @@ struct NullableConversionType { template struct Converter> : DefaultConverter> { using ReturnType = typename Detail::NullableConversionType::Type; + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + // 1. If Type(V) is not Object, and the conversion to an IDL value is being performed // due to V being assigned to an attribute whose type is a nullable callback function // that is annotated with [LegacyTreatNonObjectAsNull], then return the IDL nullable @@ -68,6 +73,29 @@ template struct Converter> : DefaultConverter + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { if (value.isUndefinedOrNull()) @@ -87,6 +115,7 @@ template struct Converter> : DefaultConverter::convert(lexicalGlobalObject, value, globalObject); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) { if (value.isUndefinedOrNull()) diff --git a/src/bun.js/bindings/webcore/JSDOMConvertOptional.h b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h new file mode 100644 index 0000000000..6052fd9ff6 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "IDLTypes.h" +#include "JSDOMConvertNullable.h" + +namespace WebCore { + +template struct Converter> : DefaultConverter> { + using ReturnType = typename Converter>::ReturnType; + + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject); + } + template + requires(!Bun::IDLConversionContext>) + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject, std::forward(exceptionThrower)); + } +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h index 36818b187a..8b93310323 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h @@ -33,43 +33,187 @@ #include #include #include +#include +#include +#include +#include "BunIDLConvertBase.h" namespace WebCore { namespace Detail { -template +template +struct SequenceTraits; + +template +struct SequenceTraits< + IDLType, + Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>> { + + using VectorType = Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!sequence.tryReserveCapacity(size)) { + // FIXME: Is the right exception to throw? + throwTypeError(&lexicalGlobalObject, scope); + return; + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + reserveExact(lexicalGlobalObject, sequence, size); + } + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + ASSERT(index == sequence.size()); + if constexpr (std::is_same_v, JSC::JSValue>) { + // `JSValue` should not be stored on the heap. + sequence.append(JSC::Strong { JSC::getVM(&lexicalGlobalObject), element }); + } else { + sequence.append(std::forward(element)); + } + } +}; + +template +struct SequenceTraits> { + using VectorType = std::array; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (size != arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) {} + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (index >= arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + sequence[index] = std::forward(element); + } +}; + +template> struct GenericSequenceConverter { - using ReturnType = Vector; + using Traits = SequenceTraits; + using ReturnType = Traits::VectorType; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + return convert(lexicalGlobalObject, object, ReturnType(), ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object) { - return convert(lexicalGlobalObject, object, ReturnType()); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, ctx); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + auto elementCtx = ctx.contextForElement(); + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &elementCtx](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + auto scope = DECLARE_THROW_SCOPE(vm); + + // auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); + auto convertedValue = Bun::convertIDL(*lexicalGlobalObject, nextValue, elementCtx); + RETURN_IF_EXCEPTION(scope, ); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); + }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } + return WTFMove(result); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result) { - forEachInIterable(&lexicalGlobalObject, object, [&result](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { - auto scope = DECLARE_THROW_SCOPE(vm); - - auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); - RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); - }); - return WTFMove(result); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, WTFMove(result), ctx); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ReturnType result; - forEachInIterable(&lexicalGlobalObject, object, [&result, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + size_t index = 0; + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } @@ -80,13 +224,24 @@ struct GenericSequenceConverter { static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method, ReturnType&& result) { - forEachInIterable(lexicalGlobalObject, object, method, [&result](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + forEachInIterable(lexicalGlobalObject, object, method, [&result, &index](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(lexicalGlobalObject, nextValue); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } }; @@ -95,9 +250,10 @@ struct GenericSequenceConverter { // FIXME: This is only implemented for the IDLFloatingPointTypes and IDLLong. To add // support for more numeric types, add an overload of Converter::convert that // takes a JSGlobalObject, ThrowScope and double as its arguments. -template +template> struct NumericSequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArray* array, unsigned length, JSC::IndexingType indexingType, ReturnType&& result) @@ -107,9 +263,10 @@ struct NumericSequenceConverter { auto indexValue = array->butterfly()->contiguousInt32().at(array, i).get(); ASSERT(!indexValue || indexValue.isInt32()); if (!indexValue) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else - result.append(indexValue.asInt32()); + Traits::append(lexicalGlobalObject, result, i, indexValue.asInt32()); + RETURN_IF_EXCEPTION(scope, {}); } return WTFMove(result); } @@ -119,12 +276,13 @@ struct NumericSequenceConverter { for (unsigned i = 0; i < length; i++) { double doubleValue = array->butterfly()->contiguousDouble().at(array, i); if (std::isnan(doubleValue)) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else { auto convertedValue = Converter::convert(lexicalGlobalObject, scope, doubleValue); RETURN_IF_EXCEPTION(scope, {}); - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, convertedValue); + RETURN_IF_EXCEPTION(scope, {}); } } return WTFMove(result); @@ -150,20 +308,23 @@ struct NumericSequenceConverter { unsigned length = array->length(); ReturnType result; + // If we're not an int32/double array, it's possible that converting a // JSValue to a number could cause the iterator protocol to change, hence, // we may need more capacity, or less. In such cases, we use the length // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); @@ -189,50 +350,52 @@ struct NumericSequenceConverter { // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, method, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); } }; -template +template> struct SequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; - static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + template + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, Ctx& ctx) { auto& vm = lexicalGlobalObject.vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; + auto elementCtx = ctx.contextForElement(); if (indexingType == JSC::ContiguousShape) { for (unsigned i = 0; i < length; i++) { auto indexValue = array->butterfly()->contiguous().at(array, i).get(); if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } @@ -244,15 +407,22 @@ struct SequenceConverter { if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + { + auto ctx = Bun::DefaultConversionContext {}; + return convertArray(lexicalGlobalObject, array, ctx); + } + template + requires(!Bun::IDLConversionContext>) static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = lexicalGlobalObject.vm(); @@ -260,11 +430,8 @@ struct SequenceConverter { unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; @@ -276,8 +443,8 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } @@ -291,37 +458,59 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } + template + static ReturnType convertObject(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (Converter::conversionHasSideEffects) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + if (!JSC::isJSArray(object)) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + JSC::JSArray* array = JSC::asArray(object); + if (!array->isIteratorProtocolFastAndNonObservable()) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array, ctx))); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (auto* object = value.getObject()) { + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); + } + ctx.throwTypeMustBe(lexicalGlobalObject, scope, "a sequence"_s); + return {}; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { auto& vm = JSC::getVM(&lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if (!value.isObject()) { - throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); - return {}; + if (auto* object = value.getObject()) { + auto ctx = Bun::DefaultConversionContext {}; + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); } - - JSC::JSObject* object = JSC::asObject(value); - if (Converter::conversionHasSideEffects) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - if (!JSC::isJSArray(object)) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - JSC::JSArray* array = JSC::asArray(object); - if (!array->isIteratorProtocolFastAndNonObservable()) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array))); + throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); + return {}; } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower(), @@ -442,22 +631,31 @@ struct SequenceConverter { } -template struct Converter> : DefaultConverter> { - using ReturnType = typename Detail::SequenceConverter::ReturnType; +template +struct Converter> : DefaultConverter> { + using ReturnType = typename Detail::SequenceConverter::ReturnType; + + static constexpr bool takesContext = true; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); + return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); } template static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); } }; diff --git a/src/bun.js/bindings/webcore/JSDOMURL.cpp b/src/bun.js/bindings/webcore/JSDOMURL.cpp old mode 100755 new mode 100644 diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 4ad4818864..0e42512c45 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -44,6 +44,7 @@ pub const AnyPromise = @import("./bindings/AnyPromise.zig").AnyPromise; pub const array_buffer = @import("./jsc/array_buffer.zig"); pub const ArrayBuffer = array_buffer.ArrayBuffer; pub const MarkedArrayBuffer = array_buffer.MarkedArrayBuffer; +pub const JSCArrayBuffer = array_buffer.JSCArrayBuffer; pub const CachedBytecode = @import("./bindings/CachedBytecode.zig").CachedBytecode; pub const CallFrame = @import("./bindings/CallFrame.zig").CallFrame; pub const CommonAbortReason = @import("./bindings/CommonAbortReason.zig").CommonAbortReason; @@ -276,5 +277,7 @@ pub const math = struct { } }; +pub const generated = @import("bindgen_generated"); + const bun = @import("bun"); const std = @import("std"); diff --git a/src/bun.js/jsc/array_buffer.zig b/src/bun.js/jsc/array_buffer.zig index 9751b476b6..19b8cde91e 100644 --- a/src/bun.js/jsc/array_buffer.zig +++ b/src/bun.js/jsc/array_buffer.zig @@ -1,10 +1,9 @@ pub const ArrayBuffer = extern struct { ptr: [*]u8 = &[0]u8{}, - offset: usize = 0, len: usize = 0, byte_len: usize = 0, - typed_array_type: jsc.JSValue.JSType = .Cell, value: jsc.JSValue = jsc.JSValue.zero, + typed_array_type: jsc.JSValue.JSType = .Cell, shared: bool = false, // require('buffer').kMaxLength. @@ -132,7 +131,7 @@ pub const ArrayBuffer = extern struct { } }; - pub const empty = ArrayBuffer{ .offset = 0, .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = undefined }; + pub const empty = ArrayBuffer{ .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = &.{} }; pub const name = "Bun__ArrayBuffer"; pub const Stream = std.io.FixedBufferStream([]u8); @@ -186,11 +185,7 @@ pub const ArrayBuffer = extern struct { extern "c" fn Bun__createArrayBufferForCopy(*jsc.JSGlobalObject, ptr: ?*const anyopaque, len: usize) jsc.JSValue; pub fn fromTypedArray(ctx: *jsc.JSGlobalObject, value: jsc.JSValue) ArrayBuffer { - var out: ArrayBuffer = .{}; - const was = value.asArrayBuffer_(ctx, &out); - bun.assert(was); - out.value = value; - return out; + return value.asArrayBuffer(ctx).?; } extern "c" fn JSArrayBuffer__fromDefaultAllocator(*jsc.JSGlobalObject, ptr: [*]u8, len: usize) jsc.JSValue; @@ -207,7 +202,7 @@ pub const ArrayBuffer = extern struct { } pub fn fromBytes(bytes: []u8, typed_array_type: jsc.JSValue.JSType) ArrayBuffer { - return ArrayBuffer{ .offset = 0, .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; + return ArrayBuffer{ .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; } pub fn toJSUnchecked(this: ArrayBuffer, ctx: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { @@ -320,7 +315,7 @@ pub const ArrayBuffer = extern struct { /// new ArrayBuffer(view.buffer, view.byteOffset, view.byteLength) /// ``` pub inline fn byteSlice(this: *const @This()) []u8 { - return this.ptr[this.offset..][0..this.byte_len]; + return this.ptr[0..this.byte_len]; } /// The equivalent of @@ -331,15 +326,19 @@ pub const ArrayBuffer = extern struct { pub const slice = byteSlice; pub inline fn asU16(this: *const @This()) []u16 { - return std.mem.bytesAsSlice(u16, @as([*]u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU16Unaligned()); } pub inline fn asU16Unaligned(this: *const @This()) []align(1) u16 { - return std.mem.bytesAsSlice(u16, @as([*]align(1) u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u16) * @sizeOf(u16)]); } pub inline fn asU32(this: *const @This()) []u32 { - return std.mem.bytesAsSlice(u32, @as([*]u32, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU32Unaligned()); + } + + pub inline fn asU32Unaligned(this: *const @This()) []align(1) u32 { + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u32) * @sizeOf(u32)]); } pub const BinaryType = enum(u4) { @@ -652,6 +651,29 @@ pub fn makeTypedArrayWithBytesNoCopy(globalObject: *jsc.JSGlobalObject, arrayTyp return bun.jsc.fromJSHostCall(globalObject, @src(), Bun__makeTypedArrayWithBytesNoCopy, .{ globalObject, arrayType, ptr, len, deallocator, deallocatorContext }); } +/// Corresponds to `JSC::ArrayBuffer`. +pub const JSCArrayBuffer = opaque { + const Self = @This(); + + extern fn JSC__ArrayBuffer__asBunArrayBuffer(self: *Self, out: *ArrayBuffer) void; + extern fn JSC__ArrayBuffer__ref(self: *Self) void; + extern fn JSC__ArrayBuffer__deref(self: *Self) void; + + pub const Ref = bun.ptr.ExternalShared(Self); + + pub const external_shared_descriptor = struct { + pub const ref = JSC__ArrayBuffer__ref; + pub const deref = JSC__ArrayBuffer__deref; + }; + + pub fn asArrayBuffer(self: *Self) ArrayBuffer { + var out: ArrayBuffer = undefined; + out.ptr = &.{}; // `ptr` might not get set if the ArrayBuffer is empty + JSC__ArrayBuffer__asBunArrayBuffer(self, &out); + return out; + } +}; + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.zig b/src/bun.zig index a47f334f01..7e24954e94 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -8,7 +8,7 @@ const bun = @This(); pub const Environment = @import("./env.zig"); -pub const use_mimalloc = true; +pub const use_mimalloc = @import("build_options").use_mimalloc; pub const default_allocator: std.mem.Allocator = allocators.c_allocator; /// Zero-sized type whose `allocator` method returns `default_allocator`. pub const DefaultAllocator = allocators.Default; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index d7133b7b69..37a7d6cddb 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -33,7 +33,8 @@ export type Type< type TypeFlag = boolean | "opt-nonnull" | null; -interface BaseTypeProps { +// This needs to be exported to avoid error TS4023. +export interface BaseTypeProps { [isType]: true | [T, K]; /** * Optional means the value may be omitted from a parameter definition. @@ -334,7 +335,7 @@ interface FuncOptionsWithVariant extends FuncMetadata { variants: FuncVariant[]; } type FuncWithoutOverloads = FuncMetadata & FuncVariant; -type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; +export type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; export interface FuncMetadata { /** diff --git a/src/codegen/bindgenv2/internal/any.ts b/src/codegen/bindgenv2/internal/any.ts new file mode 100644 index 0000000000..8ca3199762 --- /dev/null +++ b/src/codegen/bindgenv2/internal/any.ts @@ -0,0 +1,42 @@ +import { CodeStyle, Type } from "./base"; + +export const RawAny: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLRawAny"; + } + get bindgenType() { + return "bindgen.BindgenRawAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSValue"; + } + toCpp(value: any): string { + throw RangeError("`RawAny` cannot have a default value"); + } +})(); + +export const StrongAny: Type = new (class extends Type { + get idlType() { + return "::Bun::Bindgen::IDLStrongAny"; + } + get bindgenType() { + return "bindgen.BindgenStrongAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.Strong"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("`StrongAny` cannot have a default value"); + } +})(); + +export function isAny(type: Type): boolean { + return type === RawAny || type === StrongAny; +} + +export function hasRawAny(type: Type): boolean { + return type === RawAny || type.dependencies.some(hasRawAny); +} diff --git a/src/codegen/bindgenv2/internal/array.ts b/src/codegen/bindgenv2/internal/array.ts new file mode 100644 index 0000000000..51444b8c6c --- /dev/null +++ b/src/codegen/bindgenv2/internal/array.ts @@ -0,0 +1,32 @@ +import { hasRawAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class ArrayType extends Type {} + +export function Array(elemType: Type): ArrayType { + if (hasRawAny(elemType)) { + throw RangeError("arrays cannot contain `RawAny` (use `StrongAny`)"); + } + return new (class extends ArrayType { + get idlType() { + return `::Bun::IDLArray<${elemType.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenArray(${elemType.bindgenType})`; + } + zigType(style?: CodeStyle) { + return `bun.collections.ArrayListDefault(${elemType.zigType(style)})`; + } + toCpp(value: any[]): string { + const args = `${value.map(elem => elemType.toCpp(elem)).join(", ")}`; + return `${this.idlType}::ImplementationType { ${args} }`; + } + get dependencies() { + return [elemType]; + } + getHeaders(result: Set): void { + result.add("Bindgen/ExternVectorTraits.h"); + elemType.getHeaders(result); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/base.ts b/src/codegen/bindgenv2/internal/base.ts new file mode 100644 index 0000000000..c696a6ebd7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/base.ts @@ -0,0 +1,134 @@ +import util from "node:util"; +import type { NullableType, OptionalType } from "./optional"; + +/** Default is "compact". */ +export type CodeStyle = "compact" | "pretty"; + +export abstract class Type { + get optional(): OptionalType { + return require("./optional").optional(this); + } + + get nullable(): NullableType { + return require("./optional").nullable(this); + } + + abstract readonly idlType: string; + abstract readonly bindgenType: string; + + /** + * This can be overridden to make the generated code clearer. If overridden, it must return an + * expression that evaluates to the same type as `${this.bindgenType}.ZigType`; it should not + * actually change the type. + */ + zigType(style?: CodeStyle): string { + return this.bindgenType + ".ZigType"; + } + + /** This must be overridden if bindgen.zig defines a custom `OptionalZigType`. */ + optionalZigType(style?: CodeStyle): string { + return `?${this.zigType(style)}`; + } + + /** Converts a JS value into a C++ expression. Used for default values. */ + abstract toCpp(value: any): string; + + /** Other types that this type contains or otherwise depends on. */ + get dependencies(): readonly Type[] { + return []; + } + + /** Headers required by users of this type. */ + getHeaders(result: Set): void { + for (const type of this.dependencies) { + type.getHeaders(result); + } + } +} + +export abstract class NamedType extends Type { + abstract readonly name: string; + get cppHeader(): string | null { + return null; + } + get cppSource(): string | null { + return null; + } + get zigSource(): string | null { + return null; + } + // These getters are faster than `.cppHeader != null` etc. + get hasCppHeader(): boolean { + return false; + } + get hasCppSource(): boolean { + return false; + } + get hasZigSource(): boolean { + return false; + } + getHeaders(result: Set): void { + result.add(`Generated${this.name}.h`); + } +} + +export function validateName(name: string): void { + const reservedPrefixes = ["IDL", "Bindgen", "Extern", "Generated", "MemberType"]; + const reservedNames = ["Bun", "WTF", "JSC", "WebCore", "Self"]; + if (!/^[A-Z]/.test(name)) { + throw RangeError(`name must start with a capital letter: ${name}`); + } + if (/[^a-zA-Z0-9_]/.test(name)) { + throw RangeError(`name may only contain letters, numbers, and underscores: ${name}`); + } + if (reservedPrefixes.some(s => name.startsWith(s))) { + throw RangeError(`name starts with reserved prefix: ${name}`); + } + if (reservedNames.includes(name)) { + throw RangeError(`cannot use reserved name: ${name}`); + } +} + +export function headersForTypes(types: readonly Type[]): string[] { + const headers = new Set(); + for (const type of types) { + type.getHeaders(headers); + } + return Array.from(headers); +} + +export function dedent(text: string): string { + const commonIndent = Math.min( + ...Array.from(text.matchAll(/\n( *)[^ \n]/g) ?? []).map(m => m[1].length), + ); + text = text.trim(); + if (commonIndent > 0 && commonIndent !== Infinity) { + text = text.replaceAll("\n" + " ".repeat(commonIndent), "\n"); + } + return text.replace(/^ +$/gm, ""); +} + +/** Converts indents from 2 spaces to 4. */ +export function reindent(text: string): string { + return dedent(text).replace(/^ +/gm, "$&$&"); +} + +/** Does not indent the first line. */ +export function addIndent(amount: number, text: string): string { + return text.replaceAll("\n", "\n" + " ".repeat(amount)); +} + +export function joinIndented(amount: number, pieces: readonly string[]): string { + return addIndent(amount, pieces.map(dedent).join("\n")); +} + +export function toQuotedLiteral(value: string): string { + return `"${util.inspect(value).slice(1, -1).replaceAll('"', '\\"')}"`; +} + +export function toASCIILiteral(value: string): string { + if (value[Symbol.iterator]().some(c => c.charCodeAt(0) >= 128)) { + throw RangeError(`string must be ASCII: ${util.inspect(value)}`); + } + return `${toQuotedLiteral(value)}_s`; +} diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts new file mode 100644 index 0000000000..9244646941 --- /dev/null +++ b/src/codegen/bindgenv2/internal/dictionary.ts @@ -0,0 +1,451 @@ +import { hasRawAny, isAny } from "./any"; +import { + addIndent, + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, + Type, + validateName, +} from "./base"; +import * as optional from "./optional"; +import { isUnion } from "./union"; + +export interface DictionaryMember { + type: Type; + /** Optional default value to use when this member is missing or undefined. */ + default?: any; + /** The name used in generated Zig/C++ code. Defaults to the public JS name. */ + internalName?: string; + /** Alternative JavaScript names for this member. */ + altNames?: string[]; +} + +export interface DictionaryMembers { + readonly [name: string]: Type | DictionaryMember; +} + +export interface DictionaryInstance { + readonly [name: string]: any; +} + +export abstract class DictionaryType extends NamedType {} + +interface DictionaryOptions { + name: string; + /** Used in error messages. Defaults to `name`. */ + userFacingName?: string; + /** Whether to generate a Zig `fromJS` function. */ + generateConversionFunction?: boolean; +} + +export function dictionary( + nameOrOptions: string | DictionaryOptions, + members: DictionaryMembers, +): DictionaryType { + let name: string; + let userFacingName: string; + let generateConversionFunction = false; + if (typeof nameOrOptions === "string") { + name = nameOrOptions; + userFacingName = name; + } else { + name = nameOrOptions.name; + userFacingName = nameOrOptions.userFacingName ?? name; + generateConversionFunction = !!nameOrOptions.generateConversionFunction; + } + validateName(name); + const fullMembers = Object.entries(members).map( + ([name, value]) => new FullDictionaryMember(name, value), + ); + + return new (class extends DictionaryType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return fullMembers.map(m => m.type); + } + + toCpp(value: DictionaryInstance): string { + for (const memberName of Object.keys(value)) { + if (!(memberName in members)) throw RangeError(`unexpected key: ${memberName}`); + } + return reindent(`${name} { + ${joinIndented( + 8, + fullMembers.map(memberInfo => { + let memberValue; + if (Object.hasOwn(value, memberInfo.name)) { + memberValue = value[memberInfo.name]; + } else if (memberInfo.hasDefault) { + memberValue = memberInfo.default; + } else if (!permitsUndefined(memberInfo.type)) { + throw RangeError(`missing key: ${memberInfo.name}`); + } + const internalName = memberInfo.internalName; + return `.${internalName} = ${memberInfo.type.toCpp(memberValue)},`; + }), + )} + }`); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen.h" + #include "JSDOMConvertDictionary.h" + ${headersForTypes(Object.values(fullMembers).map(m => m.type)) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun { + namespace Bindgen { + namespace Generated { + struct ${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ${memberInfo.type.idlType}::ImplementationType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + }; + using IDL${name} = ::WebCore::IDLDictionary<${name}>; + struct Extern${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ExternTraits<${name}::MemberType${i}>::ExternType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + };${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result); + `); + return addIndent(8, "\n" + result); + })()} + } + + template<> struct ExternTraits { + using ExternType = Generated::Extern${name}; + static ExternType convertToExtern(Generated::${name}&& cppValue) + { + return ExternType { + ${joinIndented( + 14, + fullMembers.map((memberInfo, i) => { + const cppType = `Generated::${name}::MemberType${i}`; + const cppValue = `::std::move(cppValue.${memberInfo.internalName})`; + const rhs = `ExternTraits<${cppType}>::convertToExtern(${cppValue})`; + return `.${memberInfo.internalName} = ${rhs},`; + }), + )} + }; + } + }; + } + + template<> + struct IDLHumanReadableName<::WebCore::IDLDictionary> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = ::std::to_array(${toQuotedLiteral(userFacingName)}); + }; + } + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + + ${(() => { + if (!hasRawAny(this)) { + return ""; + } + const code = ` + template<> struct WebCore::IDLDictionary<::Bun::Bindgen::Generated::${name}> + : ::Bun::Bindgen::IDLStackOnlyDictionary<::Bun::Bindgen::Generated::${name}> {}; + `; + return joinIndented(8, [code]); + })()} + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include "Bindgen/IDLConvert.h" + #include + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + ::JSC::VM& vm = globalObject.vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto ctx = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(userFacingName)} }; + auto* object = value.getObject(); + if (!object) [[unlikely]] { + ctx.throwNotObject(globalObject, throwScope); + return {}; + } + ::Bun::Bindgen::Generated::${name} result; + ${joinIndented( + 10, + fullMembers.map((m, i) => memberConversion(userFacingName, m, i)), + )} + return result; + } + + ${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = ` + namespace Bun::Bindgen::Generated { + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result) + { + ::JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + ${name} convertedValue = ::WebCore::convert>( + *globalObject, + JSC::JSValue::decode(value) + ); + RETURN_IF_EXCEPTION(throwScope, false); + *result = ExternTraits<${name}>::convertToExtern(::std::move(convertedValue)); + return true; + } + } + `; + return joinIndented(8, [result]); + })()} + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = struct { + const Self = @This(); + + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *Self) void { + ${joinIndented( + 12, + fullMembers.map(memberInfo => { + return `bun.memory.deinit(&self.${memberInfo.internalName});`; + }), + )} + self.* = undefined; + }${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + pub fn fromJS(globalThis: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!Self { + var scope: jsc.ExceptionValidationScope = undefined; + scope.init(globalThis, @src()); + defer scope.deinit(); + var extern_result: Extern${name} = undefined; + const success = bindgenConvertJSTo${name}(globalThis, value, &extern_result); + scope.assertExceptionPresenceMatches(!success); + return if (success) + Bindgen${name}.convertFromExtern(extern_result) + else + error.JSError; + } + `); + return addIndent(10, "\n" + result); + })()} + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = Extern${name}; + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return .{ + ${joinIndented( + 14, + fullMembers.map(memberInfo => { + const internalName = memberInfo.internalName; + const bindgenType = memberInfo.type.bindgenType; + const rhs = `${bindgenType}.convertFromExtern(extern_value.${internalName})`; + return `.${internalName} = ${rhs},`; + }), + )} + }; + } + }; + + const Extern${name} = extern struct { + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.bindgenType}.ExternType,`; + }), + )} + }; + + extern fn bindgenConvertJSTo${name}( + globalObject: *jsc.JSGlobalObject, + value: jsc.JSValue, + result: *Extern${name}, + ) bool; + + const bindgen_generated = @import("bindgen_generated"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + const jsc = bun.bun_js.jsc; + `); + } + })(); +} + +class FullDictionaryMember { + names: string[]; + internalName: string; + type: Type; + hasDefault: boolean = false; + default?: any; + + constructor(name: string, member: Type | DictionaryMember) { + if (member instanceof Type) { + this.names = [name]; + this.internalName = name; + this.type = member; + } else { + this.names = [name, ...(member.altNames ?? [])]; + this.internalName = member.internalName ?? name; + this.type = member.type; + this.hasDefault = Object.hasOwn(member, "default"); + this.default = member.default; + } + } + + get name(): string { + return this.names[0]; + } +} + +function memberConversion( + userFacingDictName: string, + memberInfo: FullDictionaryMember, + memberIndex: number, +): string { + const i = memberIndex; + const internalName = memberInfo.internalName; + const idlType = memberInfo.type.idlType; + const qualifiedName = `${userFacingDictName}.${memberInfo.name}`; + + const start = ` + ::JSC::JSValue value${i}; + auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(qualifiedName)} }; + do { + ${joinIndented( + 6, + memberInfo.names.map((memberName, altNameIndex) => { + let result = ""; + if (altNameIndex > 0) { + result = `if (!value${i}.isUndefined()) break;\n`; + } + result += dedent(` + value${i} = object->get( + &globalObject, + ::JSC::Identifier::fromString(vm, ${toASCIILiteral(memberName)})); + RETURN_IF_EXCEPTION(throwScope, {}); + `); + return result; + }), + )} + } while (false); + `; + + let end: string; + if (memberInfo.hasDefault) { + end = ` + if (value${i}.isUndefined()) { + result.${internalName} = ${memberInfo.type.toCpp(memberInfo.default)}; + } else { + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + } + `; + } else if (permitsUndefined(memberInfo.type)) { + end = ` + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } else { + end = ` + if (value${i}.isUndefined()) { + ctx${i}.throwRequired(globalObject, throwScope); + return {}; + } + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } + const body = dedent(start) + "\n" + dedent(end); + return addIndent(2, "{\n" + body) + "\n}"; +} + +function basicPermitsUndefined(type: Type): boolean { + return ( + type instanceof optional.OptionalType || + type instanceof optional.NullableType || + type === optional.undefined || + type === optional.null || + isAny(type) + ); +} + +function permitsUndefined(type: Type): boolean { + if (isUnion(type)) { + return type.dependencies.some(basicPermitsUndefined); + } + return basicPermitsUndefined(type); +} diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts new file mode 100644 index 0000000000..8da8ed147a --- /dev/null +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -0,0 +1,182 @@ +import assert from "node:assert"; +import util from "node:util"; +import { + CodeStyle, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, +} from "./base"; + +abstract class EnumType extends NamedType {} + +export function enumeration(name: string, values: string[]): EnumType { + if (values.length === 0) { + throw RangeError("enum cannot be empty: " + name); + } + if (values.length > 1n << 32n) { + throw RangeError("too many enum values: " + name); + } + + const valueSet = new Set(); + const cppMemberSet = new Set(); + for (const value of values) { + if (valueSet.size === valueSet.add(value).size) { + throw RangeError(`duplicate enum value in ${name}: ${util.inspect(value)}`); + } + let cppName = "k"; + cppName += value + .split(/[^A-Za-z0-9]+/) + .filter(x => x) + .map(s => s[0].toUpperCase() + s.slice(1)) + .join(""); + if (cppMemberSet.size === cppMemberSet.add(cppName).size) { + let i = 2; + while (cppMemberSet.size === cppMemberSet.add(cppName + i).size) { + ++i; + } + } + } + const cppMembers = Array.from(cppMemberSet); + return new (class extends EnumType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + toCpp(value: string): string { + const index = values.indexOf(value); + if (index === -1) { + throw RangeError(`not a member of this enumeration: ${value}`); + } + return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + const quotedValues = values.map(v => `"${v}"`); + let humanReadableName; + if (quotedValues.length == 0) { + assert(false); // unreachable + } else if (quotedValues.length == 1) { + humanReadableName = quotedValues[0]; + } else if (quotedValues.length == 2) { + humanReadableName = quotedValues[0] + " or " + quotedValues[1]; + } else { + humanReadableName = + quotedValues.slice(0, -1).join(", ") + ", or " + quotedValues[quotedValues.length - 1]; + } + + return reindent(` + #pragma once + #include "Bindgen/ExternTraits.h" + #include "JSDOMConvertEnumeration.h" + + namespace Bun { + namespace Bindgen { + namespace Generated { + enum class ${name} : ::std::uint32_t { + ${joinIndented( + 10, + cppMembers.map(memberName => `${memberName},`), + )} + }; + using IDL${name} = ::WebCore::IDLEnumeration; + } + template<> struct ExternTraits : TrivialExtern {}; + } + template<> + struct IDLHumanReadableName<::WebCore::IDLEnumeration> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = std::to_array(${toQuotedLiteral(humanReadableName)}); + }; + } + + template<> std::optional + WebCore::parseEnumerationFromString( + const WTF::String&); + + template<> std::optional + WebCore::parseEnumeration( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + const qualifiedName = "Bun::Bindgen::Generated::" + name; + const pairType = `::std::pair<::WTF::ComparableASCIILiteral, ::${qualifiedName}>`; + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal) + { + static constexpr ::std::array<${pairType}, ${values.length}> mappings { + ${joinIndented( + 12, + values + .map<[string, number]>((value, i) => [value, i]) + .sort() + .map(([value, i]) => { + return `${pairType} { + ${toASCIILiteral(value)}, + ::${qualifiedName}::${cppMembers[i]}, + },`; + }), + )} + }; + static constexpr ::WTF::SortedArrayMap enumerationMapping { mappings }; + if (auto* enumerationValue = enumerationMapping.tryGet(stringVal)) [[likely]] { + return *enumerationValue; + } + return std::nullopt; + } + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumeration<${qualifiedName}>( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + return parseEnumerationFromString<::${qualifiedName}>( + value.toWTFString(&globalObject) + ); + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = enum(u32) { + ${joinIndented( + 10, + values.map(value => `@${toQuotedLiteral(value)},`), + )} + }; + + pub const Bindgen${name} = bindgen.BindgenTrivial(${name}); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/interfaces.ts b/src/codegen/bindgenv2/internal/interfaces.ts new file mode 100644 index 0000000000..21584ba0f4 --- /dev/null +++ b/src/codegen/bindgenv2/internal/interfaces.ts @@ -0,0 +1,40 @@ +import { CodeStyle, Type } from "./base"; + +export const ArrayBuffer = new (class extends Type { + get idlType() { + return `::Bun::IDLArrayBufferRef`; + } + get bindgenType() { + return `bindgen.BindgenArrayBuffer`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSCArrayBuffer.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `ArrayBuffer` are not supported"); + } +})(); + +export const Blob = new (class extends Type { + get idlType() { + return `::Bun::IDLBlobRef`; + } + get bindgenType() { + return `bindgen.BindgenBlob`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.webcore.Blob.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `Blob` are not supported"); + } + getHeaders(result: Set): void { + result.add("BunIDLConvertBlob.h"); + } +})(); diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts new file mode 100644 index 0000000000..74235b6fbe --- /dev/null +++ b/src/codegen/bindgenv2/internal/optional.ts @@ -0,0 +1,84 @@ +import { isAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class OptionalType extends Type {} + +export function optional(payload: Type): OptionalType { + if (isAny(payload)) { + throw RangeError("`Any` types are already optional"); + } + return new (class extends OptionalType { + get idlType() { + return `::WebCore::IDLOptional<${payload.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenOptional(${payload.bindgenType})`; + } + zigType(style?: CodeStyle) { + return payload.optionalZigType(style); + } + toCpp(value: any): string { + if (value === undefined) { + return `::WebCore::IDLOptional<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +export abstract class NullableType extends Type {} + +export function nullable(payload: Type): NullableType { + const AsOptional = optional(payload); + return new (class extends NullableType { + get idlType() { + return `::WebCore::IDLNullable<${payload.idlType}>`; + } + get bindgenType() { + return AsOptional.bindgenType; + } + zigType(style?: CodeStyle) { + return AsOptional.zigType(style); + } + toCpp(value: any): string { + if (value == null) { + return `::WebCore::IDLNullable<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +/** For use in unions, to represent an optional union. */ +const Undefined = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictUndefined`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: undefined): string { + return `{}`; + } +})(); + +/** For use in unions, to represent a nullable union. */ +const Null = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictNull`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: null): string { + return `nullptr`; + } +})(); + +export { Null as null, Undefined as undefined }; diff --git a/src/codegen/bindgenv2/internal/primitives.ts b/src/codegen/bindgenv2/internal/primitives.ts new file mode 100644 index 0000000000..72d24405d7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/primitives.ts @@ -0,0 +1,125 @@ +import assert from "node:assert"; +import util from "node:util"; +import { CodeStyle, Type } from "./base"; + +export const bool: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictBoolean"; + } + get bindgenType() { + return `bindgen.BindgenBool`; + } + zigType(style?: CodeStyle) { + return "bool"; + } + toCpp(value: boolean): string { + assert(typeof value === "boolean"); + return value ? "true" : "false"; + } +})(); + +function makeUnsignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::uint${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenU${width}`; + } + zigType(style?: CodeStyle) { + return `u${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + if (intValue < 0) throw RangeError("unsigned int cannot be negative"); + const max = 1n << BigInt(width); + if (intValue >= max) throw RangeError("integer out of range"); + return intValue.toString(); + } + })(); +} + +function makeSignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::int${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenI${width}`; + } + zigType(style?: CodeStyle) { + return `i${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + const max = 1n << BigInt(width - 1); + const min = -max; + if (intValue >= max || intValue < min) { + throw RangeError("integer out of range"); + } + if (width === 64 && intValue === min) { + return `(${intValue + 1n} - 1)`; + } + return intValue.toString(); + } + })(); +} + +export const u8: Type = makeUnsignedType(8); +export const u16: Type = makeUnsignedType(16); +export const u32: Type = makeUnsignedType(32); +export const u64: Type = makeUnsignedType(64); + +export const i8: Type = makeSignedType(8); +export const i16: Type = makeSignedType(16); +export const i32: Type = makeSignedType(32); +export const i64: Type = makeSignedType(64); + +export const f64: Type = new (class extends Type { + get finite() { + return finiteF64; + } + + get idlType() { + return "::Bun::IDLStrictDouble"; + } + get bindgenType() { + return `bindgen.BindgenF64`; + } + zigType(style?: CodeStyle) { + return `f64`; + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (Number.isNaN(value)) { + return "::std::numeric_limits::quiet_NaN()"; + } else if (value === Infinity) { + return "::std::numeric_limits::infinity()"; + } else if (value === -Infinity) { + return "-::std::numeric_limits::infinity()"; + } else { + return util.inspect(value); + } + } +})(); + +export const finiteF64: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLFiniteDouble"; + } + get bindgenType() { + return f64.bindgenType; + } + zigType(style?: CodeStyle) { + return f64.zigType(style); + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (!Number.isFinite(value)) throw RangeError("number must be finite"); + return util.inspect(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/string.ts b/src/codegen/bindgenv2/internal/string.ts new file mode 100644 index 0000000000..1363942d46 --- /dev/null +++ b/src/codegen/bindgenv2/internal/string.ts @@ -0,0 +1,21 @@ +import assert from "node:assert"; +import { CodeStyle, Type, toASCIILiteral } from "./base"; + +export const String: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictString"; + } + get bindgenType() { + return "bindgen.BindgenString"; + } + zigType(style?: CodeStyle) { + return "bun.string.WTFString"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: string): string { + assert(typeof value === "string"); + return toASCIILiteral(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/union.ts b/src/codegen/bindgenv2/internal/union.ts new file mode 100644 index 0000000000..e452f8b1f0 --- /dev/null +++ b/src/codegen/bindgenv2/internal/union.ts @@ -0,0 +1,185 @@ +import assert from "node:assert"; +import { + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + Type, + validateName, +} from "./base"; + +export interface NamedAlternatives { + readonly [name: string]: Type; +} + +export interface UnionInstance { + readonly type: Type; + readonly value: any; +} + +export abstract class AnonymousUnionType extends Type {} +export abstract class NamedUnionType extends NamedType {} + +export function isUnion(type: Type): boolean { + return type instanceof AnonymousUnionType || type instanceof NamedUnionType; +} + +export function union(alternatives: Type[]): AnonymousUnionType; +export function union(name: string, alternatives: NamedAlternatives): NamedUnionType; + +/** + * The order of types in this union is significant. Each type is tried in order, and the first one + * that successfully converts determines the active field in the corresponding Zig tagged union. + * + * This means that it is an error to specify `RawAny` or `StrongAny` as anything other than the + * last alternative, as conversion to any subsequent types would never be attempted. + */ +export function union( + alternativesOrName: Type[] | string, + maybeNamedAlternatives?: NamedAlternatives, +): AnonymousUnionType | NamedUnionType { + let alternatives: Type[]; + + function toCpp(value: UnionInstance): string { + assert(alternatives.includes(value.type)); + return `${value.type.idlType}::ImplementationType { ${value.type.toCpp(value.value)} }`; + } + + function getUnionType() { + return `::Bun::IDLOrderedUnion<${alternatives.map(a => a.idlType).join(", ")}>`; + } + + function validateAlternatives(name?: string) { + const suffix = name == null ? "" : `: ${name}`; + if (alternatives.length === 0) { + throw RangeError("union cannot be empty" + suffix); + } + } + + if (typeof alternativesOrName !== "string") { + alternatives = alternativesOrName.slice(); + validateAlternatives(); + // anonymous union (neither union nor fields are named) + return new (class extends AnonymousUnionType { + get idlType() { + return getUnionType(); + } + get bindgenType() { + return `bindgen.BindgenUnion(&.{ ${alternatives.map(a => a.bindgenType).join(", ")} })`; + } + zigType(style?: CodeStyle) { + if (style !== "pretty") { + return `bun.meta.TaggedUnion(&.{ ${alternatives.map(a => a.zigType()).join(", ")} })`; + } + return dedent(`bun.meta.TaggedUnion(&.{ + ${joinIndented( + 10, + alternatives.map(a => a.zigType("pretty") + ","), + )} + })`); + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + })(); + } + + assert(maybeNamedAlternatives !== undefined); + const namedAlternatives: NamedAlternatives = maybeNamedAlternatives; + const name: string = alternativesOrName; + validateName(name); + alternatives = Object.values(namedAlternatives); + validateAlternatives(name); + // named union (both union and fields are named) + return new (class extends NamedUnionType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen/IDLTypes.h" + ${headersForTypes(alternatives) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun::Bindgen::Generated { + using IDL${name} = ${getUnionType()}; + using ${name} = IDL${name}::ImplementationType; + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = union(enum) { + ${joinIndented( + 10, + Object.entries(namedAlternatives).map(([altName, altType]) => { + return `${altName}: ${altType.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *@This()) void { + switch (std.meta.activeTag(self.*)) { + inline else => |tag| bun.memory.deinit(&@field(self, @tagName(tag))), + } + self.* = undefined; + } + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = bindgen.ExternTaggedUnion(&.{ ${alternatives + .map(a => a.bindgenType + ".ExternType") + .join(", ")} }); + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return switch (extern_value.tag) { + ${joinIndented( + 14, + Object.entries(namedAlternatives).map(([altName, altType], i) => { + const bindgenType = altType.bindgenType; + const innerRhs = `${bindgenType}.convertFromExtern(extern_value.data.@"${i}")`; + return `${i} => .{ .${altName} = ${innerRhs} },`; + }), + )} + else => unreachable, + }; + } + }; + + const bindgen_generated = @import("bindgen_generated"); + const std = @import("std"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/lib.ts b/src/codegen/bindgenv2/lib.ts new file mode 100644 index 0000000000..ce92a319e9 --- /dev/null +++ b/src/codegen/bindgenv2/lib.ts @@ -0,0 +1,10 @@ +// organize-imports-ignore +export { bool, u8, u16, u32, u64, i8, i16, i32, i64, f64 } from "./internal/primitives"; +export { RawAny, StrongAny } from "./internal/any"; +export { String } from "./internal/string"; +export { optional, nullable, undefined, null } from "./internal/optional"; +export { union } from "./internal/union"; +export { dictionary } from "./internal/dictionary"; +export { enumeration } from "./internal/enumeration"; +export { Array } from "./internal/array"; +export { ArrayBuffer, Blob } from "./internal/interfaces"; diff --git a/src/codegen/bindgenv2/script.ts b/src/codegen/bindgenv2/script.ts new file mode 100755 index 0000000000..6d4e96e197 --- /dev/null +++ b/src/codegen/bindgenv2/script.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env bun +import * as helpers from "../helpers"; +import { NamedType, Type } from "./internal/base"; + +const USAGE = `\ +Usage: script.ts [options] + +Options (all required): + --command= Command to run (see below) + --sources= Comma-separated list of *.bindv2.ts files + --codegen-path= Path to build/*/codegen + +Commands: + list-outputs List files that will be generated, separated by semicolons (for CMake) + generate Generate all files +`; + +let codegenPath: string; +let sources: string[]; + +function getNamedExports(): NamedType[] { + return sources.flatMap(path => { + const exports = import.meta.require(path); + return Object.values(exports).filter(v => v instanceof NamedType); + }); +} + +function getNamedDependencies(type: Type, result: Set): void { + for (const dependency of type.dependencies) { + if (dependency instanceof NamedType) { + result.add(dependency); + } + getNamedDependencies(dependency, result); + } +} + +function cppHeaderPath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.h`; +} + +function cppSourcePath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.cpp`; +} + +function zigSourcePath(typeOrNamespace: NamedType | string): string { + let ns: string; + if (typeof typeOrNamespace === "string") { + ns = typeOrNamespace; + } else { + ns = toZigNamespace(typeOrNamespace.name); + } + return `${codegenPath}/bindgen_generated/${ns}.zig`; +} + +function toZigNamespace(name: string): string { + const result = name + .replace(/([^A-Z_])([A-Z])/g, "$1_$2") + .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") + .toLowerCase(); + if (result === name) { + return result + "_namespace"; + } + return result; +} + +function listOutputs(): void { + const outputs: string[] = [`${codegenPath}/bindgen_generated.zig`]; + for (const type of getNamedExports()) { + if (type.hasCppSource) outputs.push(cppSourcePath(type)); + if (type.hasZigSource) outputs.push(zigSourcePath(type)); + } + process.stdout.write(outputs.join(";")); +} + +function generate(): void { + const names = new Set(); + const zigRoot: string[] = []; + const zigRootInternal: string[] = []; + + const namedExports = getNamedExports(); + { + const namedDependencies = new Set(); + for (const type of namedExports) { + getNamedDependencies(type, namedDependencies); + } + const namedExportsSet = new Set(namedExports); + for (const type of namedDependencies) { + if (!namedExportsSet.has(type)) { + console.error(`error: named type must be exported: ${type.name}`); + process.exit(1); + } + } + const namedTypeNames = new Set(); + for (const type of namedExports) { + if (namedTypeNames.size == namedTypeNames.add(type.name).size) { + console.error(`error: multiple types with same name: ${type.name}`); + process.exit(1); + } + } + } + + for (const type of namedExports) { + const zigNamespace = toZigNamespace(type.name); + const size = names.size; + names.add(type.name); + names.add(zigNamespace); + if (names.size !== size + 2) { + console.error(`error: duplicate name: ${type.name}`); + process.exit(1); + } + + const cppHeader = type.cppHeader; + const cppSource = type.cppSource; + const zigSource = type.zigSource; + if (cppHeader) { + helpers.writeIfNotChanged(cppHeaderPath(type), cppHeader); + } + if (cppSource) { + helpers.writeIfNotChanged(cppSourcePath(type), cppSource); + } + if (zigSource) { + zigRoot.push( + `pub const ${zigNamespace} = @import("./bindgen_generated/${zigNamespace}.zig");`, + `pub const ${type.name} = ${zigNamespace}.${type.name};`, + "", + ); + zigRootInternal.push(`pub const ${type.name} = ${zigNamespace}.Bindgen${type.name};`); + helpers.writeIfNotChanged(zigSourcePath(zigNamespace), zigSource); + } + } + + helpers.writeIfNotChanged( + `${codegenPath}/bindgen_generated.zig`, + [ + ...zigRoot, + `pub const internal = struct {`, + ...zigRootInternal.map(s => " " + s), + `};`, + "", + ].join("\n"), + ); +} + +function main(): void { + const args = helpers.argParse(["command", "codegen-path", "sources", "help"]); + if (Object.keys(args).length === 0) { + process.stderr.write(USAGE); + process.exit(1); + } + const { command, "codegen-path": codegenPathArg, sources: sourcesArg, help } = args; + if (help != null) { + process.stdout.write(USAGE); + process.exit(0); + } + + if (typeof codegenPathArg !== "string") { + console.error("error: missing --codegen-path"); + process.exit(1); + } + codegenPath = codegenPathArg; + + if (typeof sourcesArg !== "string") { + console.error("error: missing --sources"); + process.exit(1); + } + sources = sourcesArg.split(",").filter(x => x); + + switch (command) { + case "list-outputs": + listOutputs(); + break; + case "generate": + generate(); + break; + default: + if (typeof command === "string") { + console.error("error: unknown command: " + command); + } else { + console.error("error: missing --command"); + } + process.exit(1); + } +} + +main(); diff --git a/src/codegen/bindgenv2/tsconfig.json b/src/codegen/bindgenv2/tsconfig.json new file mode 100644 index 0000000000..2f087e4473 --- /dev/null +++ b/src/codegen/bindgenv2/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "noUnusedLocals": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitAny": true, + "noImplicitThis": true, + "exactOptionalPropertyTypes": true + }, + "include": ["**/*.ts", "../helpers.ts"] +} diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 18a5941d02..a89b703fb5 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -114,7 +114,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Cannot use ESM import statement within builtin modules. Use require("${imp.path}") instead. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } } @@ -125,7 +125,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Using \`export default\` AND named exports together in builtin modules is unsupported. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } let importStatements: string[] = []; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 985cc01053..a1792fdb38 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -286,7 +286,7 @@ export function define( Object.entries(klass) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), @@ -294,7 +294,7 @@ export function define( Object.entries(proto) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index e6868ccd61..4a9665b4b4 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -132,15 +132,20 @@ export function pascalCase(string: string) { } export function argParse(keys: string[]): any { - const options = {}; + const options: { [key: string]: boolean | string } = {}; for (const arg of process.argv.slice(2)) { if (!arg.startsWith("--")) { - console.error("Unknown argument " + arg); + console.error("error: unknown argument: " + arg); process.exit(1); } - const split = arg.split("="); - const value = split[1] || "true"; - options[split[0].slice(2)] = value; + const splitPos = arg.indexOf("="); + let name = arg; + let value: boolean | string = true; + if (splitPos !== -1) { + name = arg.slice(0, splitPos); + value = arg.slice(splitPos + 1); + } + options[name.slice(2)] = value; } const unknown = new Set(Object.keys(options)); @@ -148,7 +153,7 @@ export function argParse(keys: string[]): any { unknown.delete(key); } for (const key of unknown) { - console.error("Unknown argument: --" + key); + console.error("error: unknown argument: --" + key); } if (unknown.size > 0) process.exit(1); return options; diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index fd1f3438ad..a0b43be968 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -253,7 +253,7 @@ export function applyReplacements(src: string, length: number) { } } - const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : undefined); + const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else if (name === "isPromiseFulfilled") { @@ -305,7 +305,7 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } - const id = registerNativeCall("bind", args[0], args[1], undefined); + const id = registerNativeCall("bind", args[0], args[1], null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else { diff --git a/src/deps/uws/SocketContext.zig b/src/deps/uws/SocketContext.zig index d2737f270f..2672402e7e 100644 --- a/src/deps/uws/SocketContext.zig +++ b/src/deps/uws/SocketContext.zig @@ -229,11 +229,11 @@ pub const SocketContext = opaque { ca_file_name: [*c]const u8 = null, ssl_ciphers: [*c]const u8 = null, ssl_prefer_low_memory_usage: i32 = 0, - key: ?[*]?[*:0]const u8 = null, + key: ?[*]const ?[*:0]const u8 = null, key_count: u32 = 0, - cert: ?[*]?[*:0]const u8 = null, + cert: ?[*]const ?[*:0]const u8 = null, cert_count: u32 = 0, - ca: ?[*]?[*:0]const u8 = null, + ca: ?[*]const ?[*:0]const u8 = null, ca_count: u32 = 0, secure_options: u32 = 0, reject_unauthorized: i32 = 0, diff --git a/src/meta.zig b/src/meta.zig index 964235a26f..89658ea945 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -195,6 +195,8 @@ fn CreateUniqueTuple(comptime N: comptime_int, comptime types: [N]type) type { }); } +pub const TaggedUnion = @import("./meta/tagged_union.zig").TaggedUnion; + pub fn hasStableMemoryLayout(comptime T: type) bool { const tyinfo = @typeInfo(T); return switch (tyinfo) { diff --git a/src/meta/tagged_union.zig b/src/meta/tagged_union.zig new file mode 100644 index 0000000000..0d32507926 --- /dev/null +++ b/src/meta/tagged_union.zig @@ -0,0 +1,236 @@ +fn deinitImpl(comptime Union: type, value: *Union) void { + switch (std.meta.activeTag(value.*)) { + inline else => |tag| bun.memory.deinit(&@field(value, @tagName(tag))), + } + value.* = undefined; +} + +/// Creates a tagged union with fields corresponding to `field_types`. The fields are named +/// @"0", @"1", @"2", etc. +pub fn TaggedUnion(comptime field_types: []const type) type { + // Types created with @Type can't contain decls, so in order to have a `deinit` method, we + // have to do it this way... + return switch (comptime field_types.len) { + 0 => @compileError("cannot create an empty tagged union"), + 1 => union(enum) { + @"0": field_types[0], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 2 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 3 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 4 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 5 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 6 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 7 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 8 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 9 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 10 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 11 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 12 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 13 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 14 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 15 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 16 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + @"15": field_types[15], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + else => @compileError("too many union fields"), + }; +} + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index d961c52a8f..779e0acaf3 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -832,7 +832,7 @@ pub export fn napi_get_typedarray_info( maybe_length: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_typedarray_info", .{}); const env = env_ orelse { @@ -859,7 +859,10 @@ pub export fn napi_get_typedarray_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), typedarray.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; @@ -881,7 +884,7 @@ pub export fn napi_get_dataview_info( maybe_bytelength: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_dataview_info", .{}); const env = env_ orelse { @@ -900,7 +903,10 @@ pub export fn napi_get_dataview_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), dataview.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } diff --git a/src/node-fallbacks/build-fallbacks.ts b/src/node-fallbacks/build-fallbacks.ts index bb5d23b6ee..8e06d0d548 100644 --- a/src/node-fallbacks/build-fallbacks.ts +++ b/src/node-fallbacks/build-fallbacks.ts @@ -5,13 +5,13 @@ import { basename, extname } from "path"; const allFiles = fs.readdirSync(".").filter(f => f.endsWith(".js")); const outdir = process.argv[2]; const builtins = Module.builtinModules; -let commands = []; +let commands: Promise[] = []; -let moduleFiles = []; +let moduleFiles: string[] = []; for (const name of allFiles) { const mod = basename(name, extname(name)).replaceAll(".", "/"); const file = allFiles.find(f => f.startsWith(mod)); - moduleFiles.push(file); + moduleFiles.push(file as string); } for (let fileIndex = 0; fileIndex < allFiles.length; fileIndex++) { diff --git a/src/string.zig b/src/string.zig index b7524f0792..17d70e05e5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -6,8 +6,9 @@ pub const PathString = @import("./string/PathString.zig").PathString; pub const SmolStr = @import("./string/SmolStr.zig").SmolStr; pub const StringBuilder = @import("./string/StringBuilder.zig"); pub const StringJoiner = @import("./string/StringJoiner.zig"); -pub const WTFStringImpl = @import("./string/WTFStringImpl.zig").WTFStringImpl; -pub const WTFStringImplStruct = @import("./string/WTFStringImpl.zig").WTFStringImplStruct; +pub const WTFString = @import("./string/wtf.zig").WTFString; +pub const WTFStringImpl = @import("./string/wtf.zig").WTFStringImpl; +pub const WTFStringImplStruct = @import("./string/wtf.zig").WTFStringImplStruct; pub const Tag = enum(u8) { /// String is not valid. Observed on some failed operations. @@ -47,7 +48,7 @@ pub const String = extern struct { pub const empty = String{ .tag = .Empty, .value = .{ .ZigString = .Empty } }; pub const dead = String{ .tag = .Dead, .value = .{ .Dead = {} } }; - pub const StringImplAllocator = @import("./string/WTFStringImpl.zig").StringImplAllocator; + pub const StringImplAllocator = @import("./string/wtf.zig").StringImplAllocator; pub fn toInt32(this: *const String) ?i32 { const val = bun.cpp.BunString__toInt32(this); diff --git a/src/string/WTFStringImpl.zig b/src/string/wtf.zig similarity index 100% rename from src/string/WTFStringImpl.zig rename to src/string/wtf.zig diff --git a/src/tsconfig.json b/src/tsconfig.json index 3f63af9f7a..9fd93876cd 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -4,10 +4,11 @@ // Path remapping "baseUrl": ".", "paths": { - "bindgen": ["./codegen/bindgen-lib.ts"] + "bindgen": ["./codegen/bindgen-lib.ts"], + "bindgenv2": ["./codegen/bindgenv2/lib.ts"] } }, "include": ["**/*.ts", "**/*.tsx"], // separate projects have extra settings that only apply in those scopes - "exclude": ["js", "bake"] + "exclude": ["js", "bake", "init", "create", "bun.js/bindings/libuv"] } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index c4b6c70273..68922965e7 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -110,7 +110,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -125,7 +125,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index d52b25ba71..b25fa22e19 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1611,7 +1611,7 @@ describe.concurrent("should error with invalid options", async () => { requestCert: "invalid", }, }); - }).toThrow('The "requestCert" property must be of type boolean, got string'); + }).toThrow("TLSOptions.requestCert must be a boolean"); }); it("rejectUnauthorized", () => { expect(() => { @@ -1624,7 +1624,7 @@ describe.concurrent("should error with invalid options", async () => { rejectUnauthorized: "invalid", }, }); - }).toThrow('The "rejectUnauthorized" property must be of type boolean, got string'); + }).toThrow("TLSOptions.rejectUnauthorized must be a boolean"); }); it("lowMemoryMode", () => { expect(() => { @@ -1638,7 +1638,7 @@ describe.concurrent("should error with invalid options", async () => { lowMemoryMode: "invalid", }, }); - }).toThrow("Expected lowMemoryMode to be a boolean"); + }).toThrow("TLSOptions.lowMemoryMode must be a boolean"); }); it("multiple missing server name", () => { expect(() => { diff --git a/test/js/bun/net/tcp-server.test.ts b/test/js/bun/net/tcp-server.test.ts index 6cd76e1ca5..b9e74a8267 100644 --- a/test/js/bun/net/tcp-server.test.ts +++ b/test/js/bun/net/tcp-server.test.ts @@ -71,7 +71,7 @@ it("should not allow invalid tls option", () => { hostname: "localhost", tls: value, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -89,7 +89,7 @@ it("should allow using false, null or undefined tls option", () => { hostname: "localhost", tls: value, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index a28d20e3fa..1da8c6b919 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,7 +24,7 @@ "noFallthroughCasesInSwitch": true, "isolatedModules": true, - // Stricter type-checking + // Less strict type-checking "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, From 2aa373ab63023d2eafbcacf98a8c74d78ee1ffbf Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 3 Oct 2025 17:13:06 -0700 Subject: [PATCH 012/391] Refactor: Split JSNodeHTTPServerSocket into separate files (#23203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Split `JSNodeHTTPServerSocket` and `JSNodeHTTPServerSocketPrototype` from `NodeHTTP.cpp` into dedicated files, following the same pattern as `JSDiffieHellman` in the crypto module. ## Changes - **Created 4 new files:** - `JSNodeHTTPServerSocket.h` - Class declaration - `JSNodeHTTPServerSocket.cpp` - Class implementation and methods - `JSNodeHTTPServerSocketPrototype.h` - Prototype declaration - `JSNodeHTTPServerSocketPrototype.cpp` - Prototype methods and property table - **Moved from NodeHTTP.cpp:** - All custom getters/setters (onclose, ondrain, ondata, etc.) - All host functions (close, write, end) - Event handlers (onClose, onDrain, onData) - Helper functions and templates - **Preserved:** - All extern C bindings for Zig interop - All existing functionality - Proper namespace and include structure - **Merged changes from main:** - Added `upgraded` flag for websocket support (from #23150) - Updated `clearSocketData` to handle WebSocketData - Added `onSocketUpgraded` callback handler ## Impact - Reduced `NodeHTTP.cpp` from ~1766 lines to 1010 lines (43% reduction) - Better code organization and maintainability - No functional changes ## Test plan - [x] Build compiles successfully - [x] `test/js/node/http/node-http.test.ts` passes (72/74 tests pass, same as before) - [x] `test/js/node/http/node-http-with-ws.test.ts` passes (websocket upgrade test) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/NodeHTTP.cpp | 765 +----------------- .../bindings/node/JSNodeHTTPServerSocket.cpp | 375 +++++++++ .../bindings/node/JSNodeHTTPServerSocket.h | 108 +++ .../node/JSNodeHTTPServerSocketPrototype.cpp | 330 ++++++++ .../node/JSNodeHTTPServerSocketPrototype.h | 45 ++ 5 files changed, 860 insertions(+), 763 deletions(-) create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocket.h create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp create mode 100644 src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 3b939e5929..116183e7f7 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -21,770 +21,14 @@ #include #include #include "JSSocketAddressDTO.h" - -extern "C" { -struct us_socket_stream_buffer_t { - char* list_ptr = nullptr; - size_t list_cap = 0; - size_t listLen = 0; - size_t total_bytes_written = 0; - size_t cursor = 0; - - size_t bufferedSize() const - { - return listLen - cursor; - } - size_t totalBytesWritten() const - { - return total_bytes_written; - } -}; -} - -extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); -extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); - -extern "C" void Bun__NodeHTTPResponse_setClosed(void* zigResponse); -extern "C" void Bun__NodeHTTPResponse_onClose(void* zigResponse, JSC::EncodedJSValue jsValue); -extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); -extern "C" void us_socket_free_stream_buffer(us_socket_stream_buffer_t* streamBuffer); +#include "node/JSNodeHTTPServerSocket.h" +#include "node/JSNodeHTTPServerSocketPrototype.h" namespace Bun { using namespace JSC; using namespace WebCore; -JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, PropertyName propertyName)) -{ - return false; -} - -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite); -JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress); - BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); -JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); -JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished); -// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function -static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { - { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, - { "ondrain"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnDrain, jsNodeHttpServerSocketSetterOnDrain } }, - { "ondata"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnData, jsNodeHttpServerSocketSetterOnData } }, - { "bytesWritten"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterBytesWritten, noOpSetter } }, - { "closed"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, - { "response"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, - { "duplex"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, - { "remoteAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, - { "localAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } }, - { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, - { "write"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketWrite, 2 } }, - { "end"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketEnd, 0 } }, - { "secureEstablished"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } }, -}; - -class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - - static JSNodeHTTPServerSocketPrototype* create(VM& vm, Structure* structure) - { - JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); - prototype->finishCreation(vm); - return prototype; - } - - DECLARE_INFO; - - static constexpr bool needsDestruction = false; - static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable; - - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSNodeHTTPServerSocketPrototype, Base); - return &vm.plainObjectSpace(); - } - -private: - JSNodeHTTPServerSocketPrototype(VM& vm, Structure* structure) - : Base(vm, structure) - { - } - - void finishCreation(VM& vm) - { - Base::finishCreation(vm); - ASSERT(inherits(info())); - reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); - this->structure()->setMayBePrototype(true); - } -}; - -class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { -public: - using Base = JSC::JSDestructibleObject; - us_socket_stream_buffer_t streamBuffer = {}; - us_socket_t* socket = nullptr; - unsigned is_ssl : 1 = 0; - unsigned ended : 1 = 0; - unsigned upgraded : 1 = 0; - JSC::Strong strongThis = {}; - - static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - { - auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); - object->finishCreation(vm); - return object; - } - - static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - { - auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); - return create(vm, structure, socket, is_ssl, response); - } - - static void destroy(JSC::JSCell* cell) - { - static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); - } - - template - static void clearSocketData(bool upgraded, us_socket_t* socket) - { - if (upgraded) { - auto* webSocket = (uWS::WebSocketData*)us_socket_ext(SSL, socket); - webSocket->socketData = nullptr; - } else { - auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); - httpResponseData->socketData = nullptr; - } - } - - void close() - { - if (socket) { - us_socket_close(is_ssl, socket, 0, nullptr); - } - } - - bool isClosed() const - { - return !socket || us_socket_is_closed(is_ssl, socket); - } - // This means: - // - [x] TLS - // - [x] Handshake has completed - // - [x] Handshake marked the connection as authorized - bool isAuthorized() const - { - // is secure means that tls was established successfully - if (!is_ssl || !socket) return false; - auto* context = us_socket_context(is_ssl, socket); - if (!context) return false; - auto* data = (uWS::HttpContextData*)us_socket_context_ext(is_ssl, context); - if (!data) return false; - return data->flags.isAuthorized; - } - ~JSNodeHTTPServerSocket() - { - if (socket) { - if (is_ssl) { - clearSocketData(this->upgraded, socket); - } else { - clearSocketData(this->upgraded, socket); - } - } - us_socket_free_stream_buffer(&streamBuffer); - } - - JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) - : JSC::JSDestructibleObject(vm, structure) - , socket(socket) - , is_ssl(is_ssl) - { - currentResponseObject.setEarlyValue(vm, this, response); - } - - mutable WriteBarrier functionToCallOnClose; - mutable WriteBarrier functionToCallOnDrain; - mutable WriteBarrier functionToCallOnData; - mutable WriteBarrier currentResponseObject; - mutable WriteBarrier m_remoteAddress; - mutable WriteBarrier m_localAddress; - mutable WriteBarrier m_duplex; - - DECLARE_INFO; - DECLARE_VISIT_CHILDREN; - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); - } - - void detach() - { - this->m_duplex.clear(); - this->currentResponseObject.clear(); - this->strongThis.clear(); - } - - void onClose() - { - - this->socket = nullptr; - if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_setClosed(res->m_ctx); - } - - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnClose) { - if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - this->detach(); - return; - } - - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnClose.get(); - if (!callbackObject) { - if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - thisObject->detach(); - return; - } - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { - Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); - } - - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - thisObject->detach(); - }); - } - } - - void onDrain() - { - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnDrain) { - return; - } - - auto bufferedSize = this->streamBuffer.bufferedSize(); - if (bufferedSize > 0) { - - auto* globalObject = defaultGlobalObject(this->globalObject()); - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); - us_socket_buffered_js_write(this->socket, this->is_ssl, this->ended, &this->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); - if (scope.exception()) { - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); - return; - } - bufferedSize = this->streamBuffer.bufferedSize(); - - if (bufferedSize > 0) { - // need to drain more - return; - } - } - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnDrain.get(); - if (!callbackObject) { - return; - } - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - }); - } - } - - void - onData(const char* data, int length, bool last) - { - // This function can be called during GC! - Zig::GlobalObject* globalObject = static_cast(this->globalObject()); - if (!functionToCallOnData) { - return; - } - - WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); - - if (scriptExecutionContext) { - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); - JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, std::span(reinterpret_cast(data), length)); - auto chunk = JSC::JSValue(buffer); - if (scope.exception()) { - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); - return; - } - gcProtect(chunk); - scriptExecutionContext->postTask([self = this, chunk = chunk, last = last](ScriptExecutionContext& context) { - WTF::NakedPtr exception; - auto* globalObject = defaultGlobalObject(context.globalObject()); - auto* thisObject = self; - auto* callbackObject = thisObject->functionToCallOnData.get(); - EnsureStillAliveScope ensureChunkStillAlive(chunk); - gcUnprotect(chunk); - if (!callbackObject) { - return; - } - - auto callData = JSC::getCallData(callbackObject); - MarkedArgumentBuffer args; - args.append(chunk); - args.append(JSC::jsBoolean(last)); - EnsureStillAliveScope ensureStillAlive(self); - - if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { - profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); - - if (auto* ptr = exception.get()) { - exception.clear(); - globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); - } - } - }); - } - } - - static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) - { - auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); - auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - - void finishCreation(JSC::VM& vm) - { - Base::finishCreation(vm); - } -}; - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsUndefined()); - } - if (thisObject->isClosed()) { - return JSValue::encode(JSC::jsUndefined()); - } - thisObject->close(); - - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsNumber(0)); - } - if (thisObject->isClosed() || thisObject->ended) { - return JSValue::encode(JSC::jsNumber(0)); - } - - return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(callFrame->argument(0)), JSValue::encode(callFrame->argument(1))); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto* thisObject = jsDynamicCast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - return JSValue::encode(JSC::jsUndefined()); - } - if (thisObject->isClosed()) { - return JSValue::encode(JSC::jsUndefined()); - } - - thisObject->ended = true; - auto bufferedSize = thisObject->streamBuffer.bufferedSize(); - if (bufferedSize == 0) { - return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); - } - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsBoolean(thisObject->isAuthorized())); -} -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_duplex) { - return JSValue::encode(thisObject->m_duplex.get()); - } - return JSValue::encode(JSC::jsNull()); -} - -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - if (auto* object = value.getObject()) { - thisObject->m_duplex.set(vm, thisObject, object); - - } else { - thisObject->m_duplex.clear(); - } - - return true; -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_remoteAddress) { - return JSValue::encode(thisObject->m_remoteAddress.get()); - } - - us_socket_t* socket = thisObject->socket; - if (!socket) { - return JSValue::encode(JSC::jsNull()); - } - - const char* address = nullptr; - int port = 0; - bool is_ipv6 = false; - - uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); - - if (address == nullptr) { - return JSValue::encode(JSC::jsNull()); - } - - auto addressString = WTF::String::fromUTF8(address); - if (addressString.isEmpty()) { - return JSValue::encode(JSC::jsNull()); - } - - auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); - thisObject->m_remoteAddress.set(vm, thisObject, object); - return JSValue::encode(object); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto& vm = globalObject->vm(); - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (thisObject->m_localAddress) { - return JSValue::encode(thisObject->m_localAddress.get()); - } - - us_socket_t* socket = thisObject->socket; - if (!socket) { - return JSValue::encode(JSC::jsNull()); - } - - const char* address = nullptr; - int port = 0; - bool is_ipv6 = false; - - uws_res_get_local_address_info(socket, &address, &port, &is_ipv6); - - if (address == nullptr) { - return JSValue::encode(JSC::jsNull()); - } - - auto addressString = WTF::String::fromUTF8(address); - if (addressString.isEmpty()) { - return JSValue::encode(JSC::jsNull()); - } - - auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); - thisObject->m_localAddress.set(vm, thisObject, object); - return JSValue::encode(object); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnClose) { - return JSValue::encode(thisObject->functionToCallOnClose.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnDrain) { - return JSValue::encode(thisObject->functionToCallOnDrain.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnDrain.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnDrain.set(vm, thisObject, value.getObject()); - return true; -} -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - - if (thisObject->functionToCallOnData) { - return JSValue::encode(thisObject->functionToCallOnData.get()); - } - - return JSValue::encode(JSC::jsUndefined()); -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnData.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnData.set(vm, thisObject, value.getObject()); - return true; -} -JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - JSValue value = JSC::JSValue::decode(encodedValue); - - if (value.isUndefined() || value.isNull()) { - thisObject->functionToCallOnClose.clear(); - return true; - } - - if (!value.isCallable()) { - return false; - } - - thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); - return true; -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - return JSValue::encode(JSC::jsNumber(thisObject->streamBuffer.totalBytesWritten())); -} - -JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) -{ - auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); - if (!thisObject->currentResponseObject) { - return JSValue::encode(JSC::jsNull()); - } - - return JSValue::encode(thisObject->currentResponseObject.get()); -} - -template -void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - JSNodeHTTPServerSocket* fn = jsCast(cell); - ASSERT_GC_OBJECT_INHERITS(fn, info()); - Base::visitChildren(fn, visitor); - - visitor.append(fn->currentResponseObject); - visitor.append(fn->functionToCallOnClose); - visitor.append(fn->functionToCallOnDrain); - visitor.append(fn->functionToCallOnData); - visitor.append(fn->m_remoteAddress); - visitor.append(fn->m_localAddress); - visitor.append(fn->m_duplex); -} - -DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); - -template -static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) -{ - auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); - return reinterpret_cast(httpResponseData->socketData); -} - -template -static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) -{ - auto* serverSocket = getNodeHTTPServerSocket(socket); - if (!serverSocket) { - return nullptr; - } - return serverSocket->currentResponseObject.get(); -} - -const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, - CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; - -const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, - CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; - -template -static void* getNodeHTTPResponsePtr(us_socket_t* socket) -{ - WebCore::JSNodeHTTPResponse* responseObject = getNodeHTTPResponse(socket); - if (!responseObject) { - return nullptr; - } - return responseObject->wrapped(); -} - -extern "C" EncodedJSValue Bun__getNodeHTTPResponseThisValue(bool is_ssl, us_socket_t* socket) -{ - if (is_ssl) { - return JSValue::encode(getNodeHTTPResponse(socket)); - } - return JSValue::encode(getNodeHTTPResponse(socket)); -} - -extern "C" EncodedJSValue Bun__getNodeHTTPServerSocketThisValue(bool is_ssl, us_socket_t* socket) -{ - if (is_ssl) { - return JSValue::encode(getNodeHTTPServerSocket(socket)); - } - return JSValue::encode(getNodeHTTPServerSocket(socket)); -} - -extern "C" void Bun__setNodeHTTPServerSocketUsSocketValue(EncodedJSValue thisValue, us_socket_t* socket) -{ - auto* response = jsCast(JSValue::decode(thisValue)); - response->socket = socket; -} - -extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocketForClientError(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject) -{ - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - RETURN_IF_EXCEPTION(scope, {}); - - if (isSSL) { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); - if (currentSocketDataPtr) { - return JSValue::encode(currentSocketDataPtr); - } - } else { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); - if (currentSocketDataPtr) { - return JSValue::encode(currentSocketDataPtr); - } - } - // socket without response because is not valid http - JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( - vm, - globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), - us_socket, - isSSL, nullptr); - if (isSSL) { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - response->getHttpResponseData()->socketData = socket; - } else { - uWS::HttpResponse* response = reinterpret_cast*>(us_socket); - response->getHttpResponseData()->socketData = socket; - } - RETURN_IF_EXCEPTION(scope, {}); - if (socket) { - socket->strongThis.set(vm, socket); - return JSValue::encode(socket); - } - - return JSValue::encode(JSC::jsNull()); -} - BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); @@ -1769,9 +1013,4 @@ extern "C" void WebCore__FetchHeaders__toUWSResponse(WebCore::FetchHeaders* arg0 } } -JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) -{ - return JSNodeHTTPServerSocket::createStructure(vm, globalObject); -} - } // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp new file mode 100644 index 0000000000..9006ddd1c0 --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.cpp @@ -0,0 +1,375 @@ +#include "JSNodeHTTPServerSocket.h" +#include "JSNodeHTTPServerSocketPrototype.h" +#include "ZigGlobalObject.h" +#include "ZigGeneratedClasses.h" +#include "DOMIsoSubspaces.h" +#include "ScriptExecutionContext.h" +#include "helpers.h" +#include "JSSocketAddressDTO.h" +#include +#include +#include +#include + +extern "C" void Bun__NodeHTTPResponse_setClosed(void* zigResponse); +extern "C" void Bun__NodeHTTPResponse_onClose(void* zigResponse, JSC::EncodedJSValue jsValue); +extern "C" void us_socket_free_stream_buffer(us_socket_stream_buffer_t* streamBuffer); +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); + +namespace Bun { + +using namespace JSC; +using namespace WebCore; + +const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; + +JSNodeHTTPServerSocket* JSNodeHTTPServerSocket::create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) +{ + auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); + object->finishCreation(vm); + return object; +} + +JSNodeHTTPServerSocket* JSNodeHTTPServerSocket::create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) +{ + auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); + return create(vm, structure, socket, is_ssl, response); +} + +template +void JSNodeHTTPServerSocket::clearSocketData(bool upgraded, us_socket_t* socket) +{ + if (upgraded) { + auto* webSocket = (uWS::WebSocketData*)us_socket_ext(SSL, socket); + webSocket->socketData = nullptr; + } else { + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + httpResponseData->socketData = nullptr; + } +} + +void JSNodeHTTPServerSocket::close() +{ + if (socket) { + us_socket_close(is_ssl, socket, 0, nullptr); + } +} + +bool JSNodeHTTPServerSocket::isClosed() const +{ + return !socket || us_socket_is_closed(is_ssl, socket); +} + +bool JSNodeHTTPServerSocket::isAuthorized() const +{ + // is secure means that tls was established successfully + if (!is_ssl || !socket) + return false; + auto* context = us_socket_context(is_ssl, socket); + if (!context) + return false; + auto* data = (uWS::HttpContextData*)us_socket_context_ext(is_ssl, context); + if (!data) + return false; + return data->flags.isAuthorized; +} + +JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket() +{ + if (socket) { + if (is_ssl) { + clearSocketData(this->upgraded, socket); + } else { + clearSocketData(this->upgraded, socket); + } + } + us_socket_free_stream_buffer(&streamBuffer); +} + +JSNodeHTTPServerSocket::JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) + : JSC::JSDestructibleObject(vm, structure) + , socket(socket) + , is_ssl(is_ssl) +{ + currentResponseObject.setEarlyValue(vm, this, response); +} + +void JSNodeHTTPServerSocket::detach() +{ + this->m_duplex.clear(); + this->currentResponseObject.clear(); + this->strongThis.clear(); +} + +void JSNodeHTTPServerSocket::onClose() +{ + this->socket = nullptr; + if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_setClosed(res->m_ctx); + } + + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnClose) { + if (auto* res = this->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + this->detach(); + return; + } + + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnClose.get(); + if (!callbackObject) { + if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + thisObject->detach(); + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + if (auto* res = thisObject->currentResponseObject.get(); res != nullptr && res->m_ctx != nullptr) { + Bun__NodeHTTPResponse_onClose(res->m_ctx, JSValue::encode(res)); + } + + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + thisObject->detach(); + }); + } +} + +void JSNodeHTTPServerSocket::onDrain() +{ + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnDrain) { + return; + } + + auto bufferedSize = this->streamBuffer.bufferedSize(); + if (bufferedSize > 0) { + auto* globalObject = defaultGlobalObject(this->globalObject()); + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + us_socket_buffered_js_write(this->socket, this->is_ssl, this->ended, &this->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); + if (scope.exception()) { + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); + return; + } + bufferedSize = this->streamBuffer.bufferedSize(); + + if (bufferedSize > 0) { + // need to drain more + return; + } + } + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnDrain.get(); + if (!callbackObject) { + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } +} + +void JSNodeHTTPServerSocket::onData(const char* data, int length, bool last) +{ + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnData) { + return; + } + + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, std::span(reinterpret_cast(data), length)); + auto chunk = JSC::JSValue(buffer); + if (scope.exception()) { + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); + return; + } + gcProtect(chunk); + scriptExecutionContext->postTask([self = this, chunk = chunk, last = last](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnData.get(); + EnsureStillAliveScope ensureChunkStillAlive(chunk); + gcUnprotect(chunk); + if (!callbackObject) { + return; + } + + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + args.append(chunk); + args.append(JSC::jsBoolean(last)); + EnsureStillAliveScope ensureStillAlive(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } +} + +JSC::Structure* JSNodeHTTPServerSocket::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); + auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +void JSNodeHTTPServerSocket::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); +} + +template +void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSNodeHTTPServerSocket* fn = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(fn, info()); + Base::visitChildren(fn, visitor); + + visitor.append(fn->currentResponseObject); + visitor.append(fn->functionToCallOnClose); + visitor.append(fn->functionToCallOnDrain); + visitor.append(fn->functionToCallOnData); + visitor.append(fn->m_remoteAddress); + visitor.append(fn->m_localAddress); + visitor.append(fn->m_duplex); +} + +DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); + +template +static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) +{ + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + return reinterpret_cast(httpResponseData->socketData); +} + +template +static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) +{ + auto* serverSocket = getNodeHTTPServerSocket(socket); + if (!serverSocket) { + return nullptr; + } + return serverSocket->currentResponseObject.get(); +} + +extern "C" JSC::EncodedJSValue Bun__getNodeHTTPResponseThisValue(bool is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPResponse(socket)); + } + return JSValue::encode(getNodeHTTPResponse(socket)); +} + +extern "C" JSC::EncodedJSValue Bun__getNodeHTTPServerSocketThisValue(bool is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPServerSocket(socket)); + } + return JSValue::encode(getNodeHTTPServerSocket(socket)); +} + +extern "C" void Bun__setNodeHTTPServerSocketUsSocketValue(JSC::EncodedJSValue thisValue, us_socket_t* socket) +{ + auto* response = jsCast(JSValue::decode(thisValue)); + response->socket = socket; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocketForClientError(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + RETURN_IF_EXCEPTION(scope, {}); + + if (isSSL) { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + if (currentSocketDataPtr) { + return JSValue::encode(currentSocketDataPtr); + } + } else { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + if (currentSocketDataPtr) { + return JSValue::encode(currentSocketDataPtr); + } + } + // socket without response because is not valid http + JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( + vm, + globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), + us_socket, + isSSL, nullptr); + if (isSSL) { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + response->getHttpResponseData()->socketData = socket; + } else { + uWS::HttpResponse* response = reinterpret_cast*>(us_socket); + response->getHttpResponseData()->socketData = socket; + } + RETURN_IF_EXCEPTION(scope, {}); + if (socket) { + socket->strongThis.set(vm, socket); + return JSValue::encode(socket); + } + + return JSValue::encode(JSC::jsNull()); +} + +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSNodeHTTPServerSocket::createStructure(vm, globalObject); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h new file mode 100644 index 0000000000..111a27ac1d --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocket.h @@ -0,0 +1,108 @@ +#pragma once + +#include "root.h" +#include +#include +#include "BunClientData.h" + +extern "C" { +struct us_socket_stream_buffer_t { + char* list_ptr = nullptr; + size_t list_cap = 0; + size_t listLen = 0; + size_t total_bytes_written = 0; + size_t cursor = 0; + + size_t bufferedSize() const + { + return listLen - cursor; + } + size_t totalBytesWritten() const + { + return total_bytes_written; + } +}; + +struct us_socket_t; +} + +namespace uWS { +template +struct HttpResponseData; +struct WebSocketData; +} + +namespace WebCore { +class JSNodeHTTPResponse; +} + +namespace Bun { + +class JSNodeHTTPServerSocketPrototype; + +class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + us_socket_stream_buffer_t streamBuffer = {}; + us_socket_t* socket = nullptr; + unsigned is_ssl : 1 = 0; + unsigned ended : 1 = 0; + unsigned upgraded : 1 = 0; + JSC::Strong strongThis = {}; + + static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + + static void destroy(JSC::JSCell* cell) + { + static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); + } + + template + static void clearSocketData(bool upgraded, us_socket_t* socket); + + void close(); + bool isClosed() const; + bool isAuthorized() const; + + ~JSNodeHTTPServerSocket(); + + JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response); + + mutable JSC::WriteBarrier functionToCallOnClose; + mutable JSC::WriteBarrier functionToCallOnDrain; + mutable JSC::WriteBarrier functionToCallOnData; + mutable JSC::WriteBarrier currentResponseObject; + mutable JSC::WriteBarrier m_remoteAddress; + mutable JSC::WriteBarrier m_localAddress; + mutable JSC::WriteBarrier m_duplex; + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); + } + + void detach(); + void onClose(); + void onDrain(); + void onData(const char* data, int length, bool last); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); + void finishCreation(JSC::VM& vm); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp new file mode 100644 index 0000000000..4695bb909c --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp @@ -0,0 +1,330 @@ +#include "JSNodeHTTPServerSocketPrototype.h" +#include "JSNodeHTTPServerSocket.h" +#include "JSSocketAddressDTO.h" +#include "ZigGlobalObject.h" +#include "ZigGeneratedClasses.h" +#include "helpers.h" +#include +#include + +extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding); +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); +extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6); + +namespace Bun { + +using namespace JSC; +using namespace WebCore; + +// Declare custom getters/setters and host functions +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished); + +JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + return false; +} + +const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; + +static const JSC::HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { + { "onclose"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, + { "ondrain"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnDrain, jsNodeHttpServerSocketSetterOnDrain } }, + { "ondata"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnData, jsNodeHttpServerSocketSetterOnData } }, + { "bytesWritten"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterBytesWritten, noOpSetter } }, + { "closed"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, + { "response"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, + { "duplex"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, + { "remoteAddress"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, + { "localAddress"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, + { "write"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketWrite, 2 } }, + { "end"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketEnd, 0 } }, + { "secureEstablished"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } }, +}; + +void JSNodeHTTPServerSocketPrototype::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); + this->structure()->setMayBePrototype(true); +} + +// Implementation of host functions +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + thisObject->close(); + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsNumber(0)); + } + if (thisObject->isClosed() || thisObject->ended) { + return JSValue::encode(JSC::jsNumber(0)); + } + + return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(callFrame->argument(0)), JSValue::encode(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + + thisObject->ended = true; + auto bufferedSize = thisObject->streamBuffer.bufferedSize(); + if (bufferedSize == 0) { + return us_socket_buffered_js_write(thisObject->socket, thisObject->is_ssl, thisObject->ended, &thisObject->streamBuffer, globalObject, JSValue::encode(JSC::jsUndefined()), JSValue::encode(JSC::jsUndefined())); + } + return JSValue::encode(JSC::jsUndefined()); +} + +// Implementation of custom getters +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isAuthorized())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_duplex) { + return JSValue::encode(thisObject->m_duplex.get()); + } + return JSValue::encode(JSC::jsNull()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + if (auto* object = value.getObject()) { + thisObject->m_duplex.set(vm, thisObject, object); + } else { + thisObject->m_duplex.clear(); + } + + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_remoteAddress) { + return JSValue::encode(thisObject->m_remoteAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_remoteAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_localAddress) { + return JSValue::encode(thisObject->m_localAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_local_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddressDTO::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_localAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnClose) { + return JSValue::encode(thisObject->functionToCallOnClose.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnDrain) { + return JSValue::encode(thisObject->functionToCallOnDrain.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnDrain, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnDrain.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnDrain.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnData) { + return JSValue::encode(thisObject->functionToCallOnData.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnData, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnData.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnData.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnClose.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsNumber(thisObject->streamBuffer.totalBytesWritten())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (!thisObject->currentResponseObject) { + return JSValue::encode(JSC::jsNull()); + } + + return JSValue::encode(thisObject->currentResponseObject.get()); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h new file mode 100644 index 0000000000..8aecf5f467 --- /dev/null +++ b/src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.h @@ -0,0 +1,45 @@ +#pragma once + +#include "root.h" +#include +#include + +namespace Bun { + +class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::HasStaticPropertyTable; + + static JSNodeHTTPServerSocketPrototype* create(JSC::VM& vm, JSC::Structure* structure) + { + JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + +private: + JSNodeHTTPServerSocketPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +} // namespace Bun From f1204ea2fd3b76379b8c990d85f00dd5945ea284 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 3 Oct 2025 17:13:22 -0700 Subject: [PATCH 013/391] bun test dots reporter (#22919) Adds a simple dots reporter for bun test image --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/bun.js/ConsoleObject.zig | 4 + src/bun.js/test/Execution.zig | 2 - src/bun.js/test/bun_test.zig | 30 +++- src/bun.js/test/jest.zig | 26 ++- src/bunfig.zig | 6 +- src/cli.zig | 5 +- src/cli/Arguments.zig | 14 +- src/cli/test_command.zig | 169 +++++++++++------- src/output.zig | 18 ++ test/js/bun/test/dots.fixture.ts | 7 + test/js/bun/test/dots.test.ts | 103 +++++++++++ .../bun/test/printing/dots/dots1.fixture.ts | 11 ++ .../bun/test/printing/dots/dots2.fixture.ts | 8 + .../bun/test/printing/dots/dots3.fixture.ts | 11 ++ 14 files changed, 330 insertions(+), 84 deletions(-) create mode 100644 test/js/bun/test/dots.fixture.ts create mode 100644 test/js/bun/test/dots.test.ts create mode 100644 test/js/bun/test/printing/dots/dots1.fixture.ts create mode 100644 test/js/bun/test/printing/dots/dots2.fixture.ts create mode 100644 test/js/bun/test/printing/dots/dots3.fixture.ts diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index e8a3e1c926..6b27a3adc7 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -153,6 +153,10 @@ fn messageWithTypeAndLevel_( var writer = buffered_writer.writer(); const Writer = @TypeOf(writer); + if (bun.jsc.Jest.Jest.runner) |runner| { + runner.bun_test_root.onBeforePrint(); + } + var print_length = len; // Get console depth from CLI options or bunfig, fallback to default const cli_context = CLI.get(); diff --git a/src/bun.js/test/Execution.zig b/src/bun.js/test/Execution.zig index 08f43ab059..2d439e26e9 100644 --- a/src/bun.js/test/Execution.zig +++ b/src/bun.js/test/Execution.zig @@ -593,8 +593,6 @@ pub fn handleUncaughtException(this: *Execution, user_data: bun_test.BunTest.Ref groupLog.begin(@src()); defer groupLog.end(); - if (bun.jsc.Jest.Jest.runner) |runner| runner.current_file.printIfNeeded(); - const sequence, const group = this.getCurrentAndValidExecutionSequence(user_data) orelse return .show_unhandled_error_between_tests; _ = group; diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index a5ba01a625..2ce02522ba 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -169,10 +169,25 @@ pub const BunTestRoot = struct { first: bool, last: bool, }; + + pub fn onBeforePrint(this: *BunTestRoot) void { + if (this.active_file.get()) |active_file| { + if (active_file.reporter) |reporter| { + if (reporter.last_printed_dot and reporter.reporters.dots) { + bun.Output.prettyError("\n", .{}); + bun.Output.flush(); + reporter.last_printed_dot = false; + } + if (bun.jsc.Jest.Jest.runner) |runner| { + runner.current_file.printIfNeeded(); + } + } + } + } }; pub const BunTest = struct { - buntest: *BunTestRoot, + bun_test_root: *BunTestRoot, in_run_loop: bool, allocation_scope: bun.AllocationScope, gpa: std.mem.Allocator, @@ -207,7 +222,7 @@ pub const BunTest = struct { this.arena = this.arena_allocator.allocator(); this.* = .{ - .buntest = bunTest, + .bun_test_root = bunTest, .in_run_loop = false, .allocation_scope = this.allocation_scope, .gpa = this.gpa, @@ -569,10 +584,10 @@ pub const BunTest = struct { }); defer order.deinit(); - const beforeall_order: Order.AllOrderResult = if (this.first_last.first) try order.generateAllOrder(this.buntest.hook_scope.beforeAll.items) else .empty; + const beforeall_order: Order.AllOrderResult = if (this.first_last.first) try order.generateAllOrder(this.bun_test_root.hook_scope.beforeAll.items) else .empty; try order.generateOrderDescribe(this.collection.root_scope); beforeall_order.setFailureSkipTo(&order); - const afterall_order: Order.AllOrderResult = if (this.first_last.last) try order.generateAllOrder(this.buntest.hook_scope.afterAll.items) else .empty; + const afterall_order: Order.AllOrderResult = if (this.first_last.last) try order.generateAllOrder(this.bun_test_root.hook_scope.afterAll.items) else .empty; afterall_order.setFailureSkipTo(&order); try this.execution.loadFromOrder(&order); @@ -703,6 +718,7 @@ pub const BunTest = struct { if (handle_status == .hide_error) return; // do not print error, it was already consumed if (exception == null) return; // the exception should not be visible (eg m_terminationException) + this.bun_test_root.onBeforePrint(); if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { this.reporter.?.jest.unhandled_errors_between_tests += 1; bun.Output.prettyErrorln( @@ -713,12 +729,14 @@ pub const BunTest = struct { , .{}); bun.Output.flush(); } + globalThis.bunVM().runErrorHandler(exception.?, null); - bun.Output.flush(); + if (handle_status == .show_unhandled_error_between_tests or handle_status == .show_unhandled_error_in_describe) { bun.Output.prettyError("-------------------------------\n\n", .{}); - bun.Output.flush(); } + + bun.Output.flush(); } }; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 521c3d064d..d34e0c22bb 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -7,8 +7,15 @@ const CurrentFile = struct { } = .{}, has_printed_filename: bool = false, - pub fn set(this: *CurrentFile, title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { - if (Output.isAIAgent()) { + pub fn set( + this: *CurrentFile, + title: string, + prefix: string, + repeat_count: u32, + repeat_index: u32, + reporter: *CommandLineReporter, + ) void { + if (Output.isAIAgent() or reporter.reporters.dots) { this.freeAndClear(); this.title = bun.handleOom(bun.default_allocator.dupe(u8, title)); this.prefix = bun.handleOom(bun.default_allocator.dupe(u8, prefix)); @@ -28,14 +35,19 @@ const CurrentFile = struct { } fn print(title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + const enable_buffering = Output.enableBufferingScope(); + defer enable_buffering.deinit(); + + Output.prettyError("\n", .{}); + if (repeat_count > 0) { if (repeat_count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); + Output.prettyErrorln("{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + Output.prettyErrorln("{s}{s}:\n", .{ prefix, title }); } } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + Output.prettyErrorln("{s}{s}:\n", .{ prefix, title }); } Output.flush(); @@ -44,6 +56,7 @@ const CurrentFile = struct { pub fn printIfNeeded(this: *CurrentFile) void { if (this.has_printed_filename) return; this.has_printed_filename = true; + print(this.title, this.prefix, this.repeat_info.count, this.repeat_info.index); } }; @@ -456,7 +469,7 @@ pub fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: [] pub fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObject) u32 { if (Jest.runner) |runner| { - if (runner.test_options.file_reporter == .junit) { + if (runner.test_options.reporters.junit) { return bun.cpp.Bun__CallFrame__getLineNumber(callframe, globalThis); } } @@ -475,6 +488,7 @@ const string = []const u8; pub const bun_test = @import("./bun_test.zig"); const std = @import("std"); +const CommandLineReporter = @import("../../cli/test_command.zig").CommandLineReporter; const Snapshots = @import("./snapshot.zig").Snapshots; const expect = @import("./expect.zig"); diff --git a/src/bunfig.zig b/src/bunfig.zig index 39cf0d3b7e..b551af317b 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -244,10 +244,14 @@ pub const Bunfig = struct { if (expr.get("junit")) |junit_expr| { try this.expectString(junit_expr); if (junit_expr.data.e_string.len() > 0) { - this.ctx.test_options.file_reporter = .junit; + this.ctx.test_options.reporters.junit = true; this.ctx.test_options.reporter_outfile = try junit_expr.data.e_string.string(allocator); } } + if (expr.get("dots") orelse expr.get("dot")) |dots_expr| { + try this.expect(dots_expr, .e_boolean); + this.ctx.test_options.reporters.dots = dots_expr.data.e_boolean.value; + } } if (test_.get("coverageReporter")) |expr| brk: { diff --git a/src/cli.zig b/src/cli.zig index 6cf201e69f..bccc5c29f1 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -352,7 +352,10 @@ pub const Command = struct { test_filter_regex: ?*RegularExpression = null, max_concurrency: u32 = 20, - file_reporter: ?TestCommand.FileReporter = null, + reporters: struct { + dots: bool = false, + junit: bool = false, + } = .{}, reporter_outfile: ?[]const u8 = null, }; diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 0a3ac55964..4b55ba7446 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -204,8 +204,9 @@ pub const test_only_params = [_]ParamType{ clap.parseParam("--coverage-dir Directory for coverage files. Defaults to 'coverage'.") catch unreachable, clap.parseParam("--bail ? Exit the test suite after failures. If you do not specify a number, it defaults to 1.") catch unreachable, clap.parseParam("-t, --test-name-pattern Run only tests with a name that matches the given regex.") catch unreachable, - clap.parseParam("--reporter Test output reporter format. Available: 'junit' (requires --reporter-outfile). Default: console output.") catch unreachable, + clap.parseParam("--reporter Test output reporter format. Available: 'junit' (requires --reporter-outfile), 'dots'. Default: console output.") catch unreachable, clap.parseParam("--reporter-outfile Output file path for the reporter format (required with --reporter).") catch unreachable, + clap.parseParam("--dots Enable dots reporter. Shorthand for --reporter=dots.") catch unreachable, clap.parseParam("--max-concurrency Maximum number of concurrent tests to execute at once. Default is 20.") catch unreachable, }; pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; @@ -455,13 +456,20 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("--reporter=junit requires --reporter-outfile [file] to specify where to save the XML report", .{}); Global.crash(); } - ctx.test_options.file_reporter = .junit; + ctx.test_options.reporters.junit = true; + } else if (strings.eqlComptime(reporter, "dots") or strings.eqlComptime(reporter, "dot")) { + ctx.test_options.reporters.dots = true; } else { - Output.errGeneric("unsupported reporter format '{s}'. Available options: 'junit' (for XML test results)", .{reporter}); + Output.errGeneric("unsupported reporter format '{s}'. Available options: 'junit' (for XML test results), 'dots'", .{reporter}); Global.crash(); } } + // Handle --dots flag as shorthand for --reporter=dots + if (args.flag("--dots")) { + ctx.test_options.reporters.dots = true; + } + if (args.option("--coverage-dir")) |dir| { ctx.test_options.coverage.reports_directory = dir; } diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 0749181606..c74e88db93 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -62,11 +62,6 @@ fn fmtStatusTextLine(status: bun_test.Execution.Result, emoji_or_color: bool) [] } pub fn writeTestStatusLine(comptime status: bun_test.Execution.Result, writer: anytype) void { - // When using AI agents, only print failures - if (Output.isAIAgent() and status != .fail) { - return; - } - switch (Output.enable_ansi_colors_stderr) { inline else => |enable_ansi_colors_stderr| writer.print(comptime fmtStatusTextLine(status, enable_ansi_colors_stderr), .{}) catch unreachable, } @@ -576,16 +571,16 @@ pub const CommandLineReporter = struct { last_dot: u32 = 0, prev_file: u64 = 0, repeat_count: u32 = 1, + last_printed_dot: bool = false, failures_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, - file_reporter: ?FileReporter = null, - - pub const FileReporter = union(enum) { - junit: *JunitReporter, - }; + reporters: struct { + dots: bool = false, + junit: ?*JunitReporter = null, + } = .{}, const DotColorMap = std.EnumMap(TestRunner.Test.Status, string); const dots: DotColorMap = brk: { @@ -602,7 +597,6 @@ pub const CommandLineReporter = struct { fn printTestLine( comptime status: bun_test.Execution.Result, - buntest: *bun_test.BunTest, sequence: *bun_test.Execution.ExecutionSequence, test_entry: *bun_test.ExecutionEntry, elapsed_ns: u64, @@ -611,10 +605,6 @@ pub const CommandLineReporter = struct { ) void { var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; - const assertions = sequence.expect_call_count; - const line_number = test_entry.base.line_no; - - const file: []const u8 = if (bun.jsc.Jest.Jest.runner) |runner| runner.files.get(buntest.file_id).source.path.text else ""; while (parent_) |scope| { scopes_stack.append(scope) catch break; @@ -711,10 +701,33 @@ pub const CommandLineReporter = struct { }, } } + } - if (buntest.reporter) |cmd_reporter| if (cmd_reporter.file_reporter) |reporter| { - switch (reporter) { - .junit => |junit| { + fn maybePrintJunitLine( + comptime status: bun_test.Execution.Result, + buntest: *bun_test.BunTest, + sequence: *bun_test.Execution.ExecutionSequence, + test_entry: *bun_test.ExecutionEntry, + elapsed_ns: u64, + ) void { + if (buntest.reporter) |cmd_reporter| { + if (cmd_reporter.reporters.junit) |junit| { + var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; + var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; + const assertions = sequence.expect_call_count; + const line_number = test_entry.base.line_no; + + const file: []const u8 = if (bun.jsc.Jest.Jest.runner) |runner| runner.files.get(buntest.file_id).source.path.text else ""; + + while (parent_) |scope| { + scopes_stack.append(scope) catch break; + parent_ = scope.base.parent; + } + + const scopes: []*bun_test.DescribeScope = scopes_stack.slice(); + const display_label = test_entry.base.name orelse "(unnamed)"; + + { const filename = brk: { if (strings.hasPrefix(file, bun.fs.FileSystem.instance.top_level_dir)) { break :brk strings.withoutLeadingPathSeparator(file[bun.fs.FileSystem.instance.top_level_dir.len..]); @@ -827,9 +840,9 @@ pub const CommandLineReporter = struct { } bun.handleOom(junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number)); - }, + } } - }; + } } pub inline fn summary(this: *CommandLineReporter) *TestRunner.Summary { @@ -846,17 +859,39 @@ pub const CommandLineReporter = struct { switch (sequence.result) { inline else => |result| { - if (result != .skipped_because_label or buntest.reporter != null and buntest.reporter.?.file_reporter != null) { - writeTestStatusLine(result, &writer); - const dim = switch (comptime result.basicResult()) { - .todo => if (bun.jsc.Jest.Jest.runner) |runner| !runner.run_todo else true, - .skip, .pending => true, - .pass, .fail => false, - }; - switch (dim) { - inline else => |dim_comptime| printTestLine(result, buntest, sequence, test_entry, elapsed_ns, &writer, dim_comptime), + if (result != .skipped_because_label) { + if (buntest.reporter != null and buntest.reporter.?.reporters.dots and (comptime switch (result.basicResult()) { + .pass, .skip, .todo, .pending => true, + .fail => false, + })) { + switch (Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors_stderr| switch (comptime result.basicResult()) { + .pass => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .skip => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .todo => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .pending => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + .fail => writer.print(comptime Output.prettyFmt(".", enable_ansi_colors_stderr), .{}) catch {}, + }, + } + buntest.reporter.?.last_printed_dot = true; + } else if (Output.isAIAgent() and (comptime result.basicResult()) != .fail) { + // when using AI agents, only print failures + } else { + buntest.bun_test_root.onBeforePrint(); + + writeTestStatusLine(result, &writer); + const dim = switch (comptime result.basicResult()) { + .todo => if (bun.jsc.Jest.Jest.runner) |runner| !runner.run_todo else true, + .skip, .pending => true, + .pass, .fail => false, + }; + switch (dim) { + inline else => |dim_comptime| printTestLine(result, sequence, test_entry, elapsed_ns, &writer, dim_comptime), + } } } + // always print junit if needed + maybePrintJunitLine(result, buntest, sequence, test_entry, elapsed_ns); }, } @@ -865,12 +900,12 @@ pub const CommandLineReporter = struct { var this: *CommandLineReporter = buntest.reporter orelse return; // command line reporter is missing! uh oh! - switch (sequence.result.basicResult()) { + if (!this.reporters.dots) switch (sequence.result.basicResult()) { .skip => bun.handleOom(this.skips_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .todo => bun.handleOom(this.todos_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .fail => bun.handleOom(this.failures_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])), .pass, .pending => {}, - } + }; switch (sequence.result) { .pending => {}, @@ -1258,10 +1293,6 @@ pub const TestCommand = struct { lcov: bool, }; - pub const FileReporter = enum { - junit, - }; - pub fn exec(ctx: Command.Context) !void { Output.is_github_action = Output.isGithubAction(); @@ -1293,12 +1324,8 @@ pub const TestCommand = struct { var reporter = try ctx.allocator.create(CommandLineReporter); defer { - if (reporter.file_reporter) |*file_reporter| { - switch (file_reporter.*) { - .junit => |junit_reporter| { - junit_reporter.deinit(); - }, - } + if (reporter.reporters.junit) |file_reporter| { + file_reporter.deinit(); } } reporter.* = CommandLineReporter{ @@ -1328,10 +1355,11 @@ pub const TestCommand = struct { jest.Jest.runner = &reporter.jest; reporter.jest.test_options = &ctx.test_options; - if (ctx.test_options.file_reporter) |file_reporter| { - reporter.file_reporter = switch (file_reporter) { - .junit => .{ .junit = JunitReporter.init() }, - }; + if (ctx.test_options.reporters.junit) { + reporter.reporters.junit = JunitReporter.init(); + } + if (ctx.test_options.reporters.dots) { + reporter.reporters.dots = true; } js_ast.Expr.Data.Store.create(); @@ -1500,7 +1528,7 @@ pub const TestCommand = struct { const write_snapshots_success = try jest.Jest.runner.?.snapshots.writeInlineSnapshots(); try jest.Jest.runner.?.snapshots.writeSnapshotFile(); var coverage_options = ctx.test_options.coverage; - if (reporter.summary().pass > 20 and !Output.isAIAgent()) { + if (reporter.summary().pass > 20 and !Output.isAIAgent() and !reporter.reporters.dots) { if (reporter.summary().skip > 0) { Output.prettyError("\n{d} tests skipped:\n", .{reporter.summary().skip}); Output.flush(); @@ -1618,25 +1646,40 @@ pub const TestCommand = struct { const did_label_filter_out_all_tests = summary.didLabelFilterOutAllTests() and reporter.jest.unhandled_errors_between_tests == 0; if (!did_label_filter_out_all_tests) { + const DotIndenter = struct { + indent: bool = false, + + pub fn format(this: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + if (this.indent) { + try writer.writeAll(" "); + } + } + }; + + const indenter = DotIndenter{ .indent = !ctx.test_options.reporters.dots }; + if (!indenter.indent) { + Output.prettyError("\n", .{}); + } + // Display the random seed if tests were randomized if (random != null) { - Output.prettyError(" --seed={d}\n", .{seed}); + Output.prettyError("{}--seed={d}\n", .{ indenter, seed }); } if (summary.pass > 0) { Output.prettyError("", .{}); } - Output.prettyError(" {d:5>} pass\n", .{summary.pass}); + Output.prettyError("{}{d:5>} pass\n", .{ indenter, summary.pass }); if (summary.skip > 0) { - Output.prettyError(" {d:5>} skip\n", .{summary.skip}); + Output.prettyError("{}{d:5>} skip\n", .{ indenter, summary.skip }); } else if (summary.skipped_because_label > 0) { - Output.prettyError(" {d:5>} filtered out\n", .{summary.skipped_because_label}); + Output.prettyError("{}{d:5>} filtered out\n", .{ indenter, summary.skipped_because_label }); } if (summary.todo > 0) { - Output.prettyError(" {d:5>} todo\n", .{summary.todo}); + Output.prettyError("{}{d:5>} todo\n", .{ indenter, summary.todo }); } if (summary.fail > 0) { @@ -1645,9 +1688,9 @@ pub const TestCommand = struct { Output.prettyError("", .{}); } - Output.prettyError(" {d:5>} fail\n", .{summary.fail}); + Output.prettyError("{}{d:5>} fail\n", .{ indenter, summary.fail }); if (reporter.jest.unhandled_errors_between_tests > 0) { - Output.prettyError(" {d:5>} error{s}\n", .{ reporter.jest.unhandled_errors_between_tests, if (reporter.jest.unhandled_errors_between_tests > 1) "s" else "" }); + Output.prettyError("{}{d:5>} error{s}\n", .{ indenter, reporter.jest.unhandled_errors_between_tests, if (reporter.jest.unhandled_errors_between_tests > 1) "s" else "" }); } var print_expect_calls = reporter.summary().expectations > 0; @@ -1659,9 +1702,9 @@ pub const TestCommand = struct { var first = true; if (print_expect_calls and added == 0 and failed == 0) { print_expect_calls = false; - Output.prettyError(" {d:5>} snapshots, {d:5>} expect() calls", .{ reporter.jest.snapshots.total, reporter.summary().expectations }); + Output.prettyError("{}{d:5>} snapshots, {d:5>} expect() calls", .{ indenter, reporter.jest.snapshots.total, reporter.summary().expectations }); } else { - Output.prettyError(" snapshots: ", .{}); + Output.prettyError("snapshots: ", .{}); if (passed > 0) { Output.prettyError("{d} passed", .{passed}); @@ -1691,7 +1734,7 @@ pub const TestCommand = struct { } if (print_expect_calls) { - Output.prettyError(" {d:5>} expect() calls\n", .{reporter.summary().expectations}); + Output.prettyError("{}{d:5>} expect() calls\n", .{ indenter, reporter.summary().expectations }); } reporter.printSummary(); @@ -1710,15 +1753,11 @@ pub const TestCommand = struct { Output.prettyError("\n", .{}); Output.flush(); - if (reporter.file_reporter) |file_reporter| { - switch (file_reporter) { - .junit => |junit| { - if (junit.current_file.len > 0) { - junit.endTestSuite() catch {}; - } - junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {}; - }, + if (reporter.reporters.junit) |junit| { + if (junit.current_file.len > 0) { + junit.endTestSuite() catch {}; } + junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {}; } if (vm.hot_reload == .watch) { @@ -1841,7 +1880,7 @@ pub const TestCommand = struct { bun_test_root.enterFile(file_id, reporter, should_run_concurrent, first_last); defer bun_test_root.exitFile(); - reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); + reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index, reporter); bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); diff --git a/src/output.zig b/src/output.zig index 7b5a2d33f7..6d1f79d90a 100644 --- a/src/output.zig +++ b/src/output.zig @@ -521,6 +521,24 @@ pub fn enableBuffering() void { if (comptime Environment.isNative) enable_buffering = true; } +const EnableBufferingScope = struct { + prev_buffering: bool, + pub fn init() EnableBufferingScope { + const prev_buffering = enable_buffering; + enable_buffering = true; + return .{ .prev_buffering = prev_buffering }; + } + + /// Does not call Output.flush(). + pub fn deinit(self: EnableBufferingScope) void { + enable_buffering = self.prev_buffering; + } +}; + +pub fn enableBufferingScope() EnableBufferingScope { + return EnableBufferingScope.init(); +} + pub fn disableBuffering() void { flush(); if (comptime Environment.isNative) enable_buffering = false; diff --git a/test/js/bun/test/dots.fixture.ts b/test/js/bun/test/dots.fixture.ts new file mode 100644 index 0000000000..5f36924ca4 --- /dev/null +++ b/test/js/bun/test/dots.fixture.ts @@ -0,0 +1,7 @@ +test.each(Array.from({ length: 10 }, () => 0))("passing filterin", () => {}); +test.skip.each(Array.from({ length: 10 }, () => 0))("skipped filterin", () => {}); +test.failing("failing filterin", () => {}); +test("passing filterout", () => {}); +test.failing("failing filterin", () => {}); +test.failing("failing filterin", () => {}); +test.todo.each(Array.from({ length: 10 }, () => 0))("todo filterin", () => {}); diff --git a/test/js/bun/test/dots.test.ts b/test/js/bun/test/dots.test.ts new file mode 100644 index 0000000000..3cb4027199 --- /dev/null +++ b/test/js/bun/test/dots.test.ts @@ -0,0 +1,103 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot } from "harness"; + +test("dots 1", async () => { + const result = await Bun.spawn({ + cmd: [bunExe(), "test", import.meta.dir + "/dots.fixture.ts", "--dots", "-t", "filterin"], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stdout: normalizeBunSnapshot(stdout), + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 1, + "stderr": + ".................... + + test/js/bun/test/dots.fixture.ts: + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + (fail) failing filterin + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + .......... + + 10 pass + 10 skip + 10 todo + 3 fail + Ran 33 tests across 1 file." + , + "stdout": "bun test ()", + } + `); +}); + +test("dots 2", async () => { + const result = await Bun.spawn({ + cmd: [ + bunExe(), + "test", + import.meta.dir + "/printing/dots/dots1.fixture.ts", + import.meta.dir + "/printing/dots/dots2.fixture.ts", + import.meta.dir + "/printing/dots/dots3.fixture.ts", + "--dots", + ], + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + const exitCode = await result.exited; + const stdout = await result.stdout.text(); + const stderr = await result.stderr.text(); + expect({ + exitCode, + stderr: normalizeBunSnapshot(stderr), + }).toMatchInlineSnapshot(` + { + "exitCode": 1, + "stderr": + ".......... + + test/js/bun/test/printing/dots/dots1.fixture.ts: + Hello, world! + ........... + Hello, world! + . + + test/js/bun/test/printing/dots/dots2.fixture.ts: + Hello, world! + ........... + (fail) failing test + ^ this test is marked as failing but it passed. Remove \`.failing\` if tested behavior now works + .................... + + test/js/bun/test/printing/dots/dots3.fixture.ts: + 3 | // unhandled failure. it should print the filename + 4 | test("failure", async () => { + 5 | const { resolve, reject, promise } = Promise.withResolvers(); + 6 | setTimeout(() => { + 7 | resolve(); + 8 | throw new Error("unhandled error"); + ^ + error: unhandled error + at (file:NN:NN) + (fail) failure + + + 43 pass + 10 skip + 2 fail + Ran 55 tests across 3 files." + , + } + `); +}); diff --git a/test/js/bun/test/printing/dots/dots1.fixture.ts b/test/js/bun/test/printing/dots/dots1.fixture.ts new file mode 100644 index 0000000000..603e253327 --- /dev/null +++ b/test/js/bun/test/printing/dots/dots1.fixture.ts @@ -0,0 +1,11 @@ +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// now, console.log. it should show the filename +test("console.log", () => { + console.warn("Hello, world!"); +}); +// more tests +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// console.log again. it should add a newline but not show the filename again. +test("console.log again", () => { + console.warn("Hello, world!"); +}); diff --git a/test/js/bun/test/printing/dots/dots2.fixture.ts b/test/js/bun/test/printing/dots/dots2.fixture.ts new file mode 100644 index 0000000000..557d6c61ba --- /dev/null +++ b/test/js/bun/test/printing/dots/dots2.fixture.ts @@ -0,0 +1,8 @@ +test("console.log first. it should not add a newline but should show the filename", () => { + console.warn("Hello, world!"); +}); +// more dots +test.skip.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); +// failing test. it should add a newline but not show the filename again. +test.failing("failing test", () => {}); +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); diff --git a/test/js/bun/test/printing/dots/dots3.fixture.ts b/test/js/bun/test/printing/dots/dots3.fixture.ts new file mode 100644 index 0000000000..e3b566696c --- /dev/null +++ b/test/js/bun/test/printing/dots/dots3.fixture.ts @@ -0,0 +1,11 @@ +test.each(Array.from({ length: 10 }, () => 0))("pass", () => {}); + +// unhandled failure. it should print the filename +test("failure", async () => { + const { resolve, reject, promise } = Promise.withResolvers(); + setTimeout(() => { + resolve(); + throw new Error("unhandled error"); + }, 0); + await promise; +}); From e3bd03628a0b709d3e53a1ed71f5737d943ebab2 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Fri, 3 Oct 2025 17:50:47 -0700 Subject: [PATCH 014/391] fix(Bun.SQL) fix command detection on sqlite (#23221) ### What does this PR do? Returning clause should work with insert now ### How did you verify your code works? Tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/js/internal/sql/sqlite.ts | 385 ++++++++++++--------------------- test/js/sql/sqlite-sql.test.ts | 31 ++- 2 files changed, 174 insertions(+), 242 deletions(-) diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index 0a150cb79a..9899948908 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -29,11 +29,11 @@ const enum SQLCommand { interface SQLParsedInfo { command: SQLCommand; - firstKeyword: string; // SELECT, INSERT, UPDATE, etc. - hasReturning: boolean; + lastToken?: string; + canReturnRows: boolean; } -function commandToString(command: SQLCommand): string { +function commandToString(command: SQLCommand, lastToken?: string): string { switch (command) { case SQLCommand.insert: return "INSERT"; @@ -42,258 +42,168 @@ function commandToString(command: SQLCommand): string { return "UPDATE"; case SQLCommand.in: case SQLCommand.where: + if (lastToken) return lastToken; return "WHERE"; default: + if (lastToken) return lastToken; return ""; } } -function matchAsciiIgnoreCase(str: string, start: number, end: number, target: string): boolean { - if (end - start !== target.length) return false; - for (let i = 0; i < target.length; i++) { - const c = str.charCodeAt(start + i); - const t = target.charCodeAt(i); - - if (c !== t) { - if (c >= 65 && c <= 90) { - if (c + 32 !== t) return false; - } else if (c >= 97 && c <= 122) { - if (c - 32 !== t) return false; - } else { - return false; - } - } - } - - return true; -} - -// Check if character is whitespace or delimiter (anything that's not a letter/digit/underscore) -function isTokenDelimiter(code: number): boolean { - // Quick check for common ASCII whitespace - if (code <= 32) return true; - // Letters A-Z, a-z - if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) return false; - // Digits 0-9 - if (code >= 48 && code <= 57) return false; - // Underscore (allowed in SQL identifiers) - if (code === 95) return false; - // Everything else is a delimiter (including Unicode whitespace, punctuation, etc.) - return true; -} - -function parseSQLQuery(query: string): SQLParsedInfo { - const text_len = query.length; - - // Skip leading whitespace/delimiters - let i = 0; - while (i < text_len && isTokenDelimiter(query.charCodeAt(i))) { - i++; - } +/** + * Parse the SQL query and return the command and the last token + * @param query - The SQL query to parse + * @param partial - Whether to stop on the first command we find + * @returns The command, the last token, and whether it can return rows + */ +function parseSQLQuery(query: string, partial: boolean = false): SQLParsedInfo { + const text = query.toUpperCase().trim(); + const text_len = text.length; + let token = ""; let command = SQLCommand.none; - let firstKeyword = ""; - let hasReturning = false; - let quotedDouble = false; - let tokenStart = i; - - while (i < text_len) { - const char = query[i]; - const charCode = query.charCodeAt(i); - - // Handle quotes BEFORE checking delimiters, since quotes are also delimiters - // Handle single quotes - skip entire string literal - if (!quotedDouble && char === "'") { - // Process any pending token before the quote - if (i > tokenStart) { - // We have a token to process before the quote - // Check what token it is - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; - } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { - command = SQLCommand.updateSet; + let lastToken = ""; + let canReturnRows = false; + let quoted: false | "'" | '"' = false; + // we need to reverse search so we find the closest command to the parameter + for (let i = text_len - 1; i >= 0; i--) { + const char = text[i]; + switch (char) { + case " ": + case "\n": + case "\t": + case "\r": + case "\f": + case "\v": { + switch (token) { + case "INSERT": { + if (command === SQLCommand.none) { + command = SQLCommand.insert; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.insert, lastToken, canReturnRows }; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; - } - } - } - - // Now skip the entire string literal - i++; - while (i < text_len) { - if (query[i] === "'") { - // Check for escaped quote - if (i + 1 < text_len && query[i + 1] === "'") { - i += 2; // Skip escaped quote continue; } - i++; - break; - } - i++; - } - // After string, skip any whitespace and reset token start - while (i < text_len && isTokenDelimiter(query.charCodeAt(i))) { - i++; - } - tokenStart = i; - continue; - } - - if (char === '"') { - quotedDouble = !quotedDouble; - i++; - continue; - } - - if (quotedDouble) { - i++; - continue; - } - - if (isTokenDelimiter(charCode)) { - if (i > tokenStart) { - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; + case "UPDATE": { + if (command === SQLCommand.none) { + command = SQLCommand.update; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.update, lastToken, canReturnRows }; + } + continue; } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { + case "WHERE": { + if (command === SQLCommand.none) { + command = SQLCommand.where; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.where, lastToken, canReturnRows }; + } + continue; + } + case "SET": { + if (command === SQLCommand.none) { command = SQLCommand.updateSet; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.updateSet, lastToken, canReturnRows }; + } + continue; + } + case "IN": { + if (command === SQLCommand.none) { + command = SQLCommand.in; + } + lastToken = token; + token = ""; + if (partial) { + return { command: SQLCommand.in, lastToken, canReturnRows }; + } + continue; + } + case "SELECT": + case "PRAGMA": + case "WITH": + case "EXPLAIN": + case "RETURNING": { + lastToken = token; + canReturnRows = true; + token = ""; + continue; + } + default: { + lastToken = token; + token = ""; + continue; } } } - - // Skip delimiters but stop at quotes (they need special handling) - while (++i < text_len) { - const nextChar = query[i]; - if (nextChar === "'" || nextChar === '"') { - break; // Stop at quotes, they'll be handled in next iteration + default: { + // skip quoted commands + if (char === '"' || char === "'") { + if (quoted === char) { + quoted = false; + } else { + quoted = char; + } + continue; } - if (!isTokenDelimiter(query.charCodeAt(i))) { - break; // Stop at non-delimiter + if (!quoted) { + token = char + token; } } - tokenStart = i; - continue; } - i++; } - - // Handle last token if we reached end of string - if (i >= text_len && i > tokenStart && !quotedDouble) { - // Track the first keyword for the command string - if (!firstKeyword) { - if (matchAsciiIgnoreCase(query, tokenStart, i, "select")) { - firstKeyword = "SELECT"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "insert")) { - firstKeyword = "INSERT"; - command = SQLCommand.insert; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "update")) { - firstKeyword = "UPDATE"; - command = SQLCommand.update; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "delete")) { - firstKeyword = "DELETE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "create")) { - firstKeyword = "CREATE"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "drop")) { - firstKeyword = "DROP"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "alter")) { - firstKeyword = "ALTER"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "pragma")) { - firstKeyword = "PRAGMA"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "explain")) { - firstKeyword = "EXPLAIN"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { - firstKeyword = "WITH"; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - firstKeyword = "IN"; - command = SQLCommand.in; - } - } else { - // After we have the first keyword, look for other keywords - if (matchAsciiIgnoreCase(query, tokenStart, i, "where")) { - command = SQLCommand.where; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "set")) { - if (command === SQLCommand.update) { + if (token) { + lastToken = token; + switch (token) { + case "INSERT": + if (command === SQLCommand.none) { + command = SQLCommand.insert; + } + break; + case "UPDATE": + if (command === SQLCommand.none) command = SQLCommand.update; + break; + case "WHERE": + if (command === SQLCommand.none) { + command = SQLCommand.where; + } + break; + case "SET": + if (command === SQLCommand.none) { command = SQLCommand.updateSet; } - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - command = SQLCommand.in; - } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { - hasReturning = true; + break; + case "IN": + if (command === SQLCommand.none) { + command = SQLCommand.in; + } + break; + case "SELECT": + case "PRAGMA": + case "WITH": + case "EXPLAIN": + case "RETURNING": { + canReturnRows = true; + break; } + default: + command = SQLCommand.none; + break; } } - - return { command, firstKeyword, hasReturning }; + return { command, lastToken, canReturnRows }; } class SQLiteQueryHandle implements BaseQueryHandle { @@ -323,19 +233,11 @@ class SQLiteQueryHandle implements BaseQueryHandle { } const { sql, values, mode, parsedInfo } = this; - try { - const command = parsedInfo.firstKeyword; - + const command = parsedInfo.command; // For SELECT queries, we need to use a prepared statement // For other queries, we can check if there are multiple statements and use db.run() if so - if ( - command === "SELECT" || - command === "PRAGMA" || - command === "WITH" || - command === "EXPLAIN" || - parsedInfo.hasReturning - ) { + if (parsedInfo.canReturnRows) { // SELECT queries must use prepared statements for results const stmt = db.prepare(sql); let result: unknown[] | undefined; @@ -350,7 +252,7 @@ class SQLiteQueryHandle implements BaseQueryHandle { const sqlResult = $isArray(result) ? new SQLResultArray(result) : new SQLResultArray([result]); - sqlResult.command = command; + sqlResult.command = commandToString(command, parsedInfo.lastToken); sqlResult.count = $isArray(result) ? result.length : 1; stmt.finalize(); @@ -360,7 +262,7 @@ class SQLiteQueryHandle implements BaseQueryHandle { const changes = db.run.$apply(db, [sql].concat(values)); const sqlResult = new SQLResultArray(); - sqlResult.command = command; + sqlResult.command = commandToString(command, parsedInfo.lastToken); sqlResult.count = changes.changes; sqlResult.lastInsertRowid = changes.lastInsertRowid; @@ -512,7 +414,8 @@ class SQLiteAdapter implements DatabaseAdapter { await sql.close(); }); }); - describe("Transactions", () => { let sql: SQL; @@ -1185,6 +1184,36 @@ describe("SQLite-specific features", () => { expect(results[0].id).toBe(1); expect(results[1].id).toBe(3); }); + test("returning clause on insert statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql` + create table users ( + id integer primary key, + name text not null, + verified integer not null default 0, + created_at integer not null default (strftime('%s', 'now')) + )`; + + const result = + await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')), (null, ${"Bruce"}, ${0}, strftime('%s', 'now')), (null, ${"Jane"}, ${0}, strftime('%s', 'now')), (null, ${"Austin"}, ${0}, strftime('%s', 'now')) returning "id", "name", "verified"`; + + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].verified).toBe(0); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Bruce"); + expect(result[1].verified).toBe(0); + expect(result[2].id).toBe(3); + expect(result[2].name).toBe("Jane"); + expect(result[2].verified).toBe(0); + expect(result[3].id).toBe(4); + expect(result[3].name).toBe("Austin"); + expect(result[3].verified).toBe(0); + + const [{ 'upper("name")': upperName }] = + await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')) returning upper("name")`; + expect(upperName).toBe("JOHN"); + }); test("last_insert_rowid()", async () => { await sql`CREATE TABLE rowid_test (id INTEGER PRIMARY KEY, value TEXT)`; From d8350c2c59de1f6f2d6c3154222cbb4345662447 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 3 Oct 2025 22:05:29 -0700 Subject: [PATCH 015/391] Add `jsc.DecodedJSValue`; make `jsc.Strong` more efficient (#23218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `jsc.DecodedJSValue`, an extern struct which is ABI-compatible with `JSC::JSValue`. (By contrast, `jsc.JSValue` is ABI-compatible with `JSC::EncodedJSValue`.) This enables `jsc.Strong.get` to be more efficient: it no longer has to call into C⁠+⁠+. (For internal tracking: fixes ENG-20748) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/ModuleLoader.zig | 6 ++--- src/bun.js/Strong.zig | 6 ++--- src/bun.js/bindings/DecodedJSValue.zig | 33 ++++++++++++++++++++++++++ src/bun.js/bindings/EncodedJSValue.zig | 9 ------- src/bun.js/bindings/JSValue.zig | 7 ++++++ src/bun.js/bindings/StrongRef.cpp | 5 ---- src/bun.js/bindings/StrongRef.h | 1 - src/bun.js/jsc.zig | 2 +- 8 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 src/bun.js/bindings/DecodedJSValue.zig delete mode 100644 src/bun.js/bindings/EncodedJSValue.zig diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index d0bcafe343..8398f08614 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -1350,10 +1350,10 @@ pub fn transpileSourceCode( if (virtual_source) |source| { if (globalObject) |globalThis| { // attempt to avoid reading the WASM file twice. - const encoded = jsc.EncodedJSValue{ - .asPtr = globalThis, + const decoded: jsc.DecodedJSValue = .{ + .u = .{ .ptr = @ptrCast(globalThis) }, }; - const globalValue = @as(JSValue, @enumFromInt(encoded.asInt64)); + const globalValue = decoded.encode(); globalValue.put( globalThis, ZigString.static("wasmSourceBytes"), diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index 5c098b88eb..8c22aa9da4 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -121,8 +121,9 @@ pub const Impl = opaque { } pub fn get(this: *Impl) jsc.JSValue { - jsc.markBinding(@src()); - return Bun__StrongRef__get(this); + // `this` is actually a pointer to a `JSC::JSValue`; see Strong.cpp. + const js_value: *jsc.DecodedJSValue = @ptrCast(@alignCast(this)); + return js_value.encode(); } pub fn set(this: *Impl, global: *jsc.JSGlobalObject, value: jsc.JSValue) void { @@ -142,7 +143,6 @@ pub const Impl = opaque { extern fn Bun__StrongRef__delete(this: *Impl) void; extern fn Bun__StrongRef__new(*jsc.JSGlobalObject, jsc.JSValue) *Impl; - extern fn Bun__StrongRef__get(this: *Impl) jsc.JSValue; extern fn Bun__StrongRef__set(this: *Impl, *jsc.JSGlobalObject, jsc.JSValue) void; extern fn Bun__StrongRef__clear(this: *Impl) void; }; diff --git a/src/bun.js/bindings/DecodedJSValue.zig b/src/bun.js/bindings/DecodedJSValue.zig new file mode 100644 index 0000000000..4f6bb32511 --- /dev/null +++ b/src/bun.js/bindings/DecodedJSValue.zig @@ -0,0 +1,33 @@ +/// ABI-compatible with `JSC::JSValue`. +pub const DecodedJSValue = extern struct { + const Self = @This(); + + u: EncodedValueDescriptor, + + /// ABI-compatible with `JSC::EncodedValueDescriptor`. + pub const EncodedValueDescriptor = extern union { + asInt64: i64, + ptr: ?*jsc.JSCell, + asBits: extern struct { + payload: i32, + tag: i32, + }, + }; + + /// Equivalent to `JSC::JSValue::encode`. + pub fn encode(self: Self) jsc.JSValue { + return @enumFromInt(self.u.asInt64); + } +}; + +comptime { + bun.assertf(@sizeOf(usize) == 8, "EncodedValueDescriptor assumes a 64-bit system", .{}); + bun.assertf( + @import("builtin").target.cpu.arch.endian() == .little, + "EncodedValueDescriptor.asBits assumes a little-endian system", + .{}, + ); +} + +const bun = @import("bun"); +const jsc = bun.bun_js.jsc; diff --git a/src/bun.js/bindings/EncodedJSValue.zig b/src/bun.js/bindings/EncodedJSValue.zig deleted file mode 100644 index 793bc6e8c9..0000000000 --- a/src/bun.js/bindings/EncodedJSValue.zig +++ /dev/null @@ -1,9 +0,0 @@ -pub const EncodedJSValue = extern union { - asInt64: i64, - ptr: ?*JSCell, - asBits: [8]u8, - asPtr: ?*anyopaque, - asDouble: f64, -}; - -const JSCell = @import("./JSCell.zig").JSCell; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 76b66be73b..1b36c3c4bd 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -2391,6 +2391,13 @@ pub const JSValue = enum(i64) { }; pub const backing_int = @typeInfo(JSValue).@"enum".tag_type; + + /// Equivalent to `JSC::JSValue::decode`. + pub fn decode(self: JSValue) jsc.DecodedJSValue { + var decoded: jsc.DecodedJSValue = undefined; + decoded.u.asInt64 = @intFromEnum(self); + return decoded; + } }; extern "c" fn AsyncContextFrame__withAsyncContextIfNeeded(global: *JSGlobalObject, callback: JSValue) JSValue; diff --git a/src/bun.js/bindings/StrongRef.cpp b/src/bun.js/bindings/StrongRef.cpp index 8466df5cfa..232b3ac1ee 100644 --- a/src/bun.js/bindings/StrongRef.cpp +++ b/src/bun.js/bindings/StrongRef.cpp @@ -27,11 +27,6 @@ extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, return handleSlot; } -extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot) -{ - return JSC::JSValue::encode(*handleSlot); -} - extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue) { auto& vm = globalObject->vm(); diff --git a/src/bun.js/bindings/StrongRef.h b/src/bun.js/bindings/StrongRef.h index 9726d6895a..4701cf9c34 100644 --- a/src/bun.js/bindings/StrongRef.h +++ b/src/bun.js/bindings/StrongRef.h @@ -4,7 +4,6 @@ extern "C" void Bun__StrongRef__delete(JSC::JSValue* _Nonnull handleSlot); extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); -extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot); extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); extern "C" void Bun__StrongRef__clear(JSC::JSValue* _Nonnull handleSlot); diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 0e42512c45..ee13a61d0f 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -52,8 +52,8 @@ pub const CommonStrings = @import("./bindings/CommonStrings.zig").CommonStrings; pub const CustomGetterSetter = @import("./bindings/CustomGetterSetter.zig").CustomGetterSetter; pub const DOMFormData = @import("./bindings/DOMFormData.zig").DOMFormData; pub const DOMURL = @import("./bindings/DOMURL.zig").DOMURL; +pub const DecodedJSValue = @import("./bindings/DecodedJSValue.zig").DecodedJSValue; pub const DeferredError = @import("./bindings/DeferredError.zig").DeferredError; -pub const EncodedJSValue = @import("./bindings/EncodedJSValue.zig").EncodedJSValue; pub const GetterSetter = @import("./bindings/GetterSetter.zig").GetterSetter; pub const JSArray = @import("./bindings/JSArray.zig").JSArray; pub const JSArrayIterator = @import("./bindings/JSArrayIterator.zig").JSArrayIterator; From 8d28289407eef8c30529c3d3c49cb350a3c64929 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 00:31:47 -0700 Subject: [PATCH 016/391] fix(install): make negative workspace patterns work (#23229) ### What does this PR do? It's common for monorepos to exclude portions of a large glob ```json "workspaces": [ "packages/**", "!packages/**/test/**", "!packages/**/template/**" ], ``` closes #4621 (note: patterns like `"packages/!(*-standalone)"` will need to be written `"!packages/*-standalone"`) ### How did you verify your code works? Manually tested https://github.com/opentiny/tiny-engine, and added a new workspace test. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/api/glob.zig | 6 +-- src/bun.js/test/jest.zig | 3 +- src/cli/filter_arg.zig | 8 ++-- src/cli/outdated_command.zig | 6 +-- src/cli/pack_command.zig | 10 ++--- src/cli/test_command.zig | 4 +- src/cli/update_interactive_command.zig | 4 +- src/glob.zig | 40 +++++++++++++++++-- src/glob/GlobWalker.zig | 6 +-- src/glob/match.zig | 38 +----------------- .../PackageManager/install_with_manager.zig | 2 +- src/install/lockfile/Package/WorkspaceMap.zig | 32 ++++++++++++--- src/install/lockfile/Tree.zig | 2 +- src/resolver/package_json.zig | 4 +- src/shell/interpreter.zig | 3 +- src/shell/shell.zig | 3 +- test/cli/install/bun-workspaces.test.ts | 38 ++++++++++++++++++ 17 files changed, 129 insertions(+), 80 deletions(-) diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index f25381c077..0e3f37375c 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -371,7 +371,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame var str = try str_arg.toSlice(globalThis, arena.allocator()); defer str.deinit(); - return jsc.JSValue.jsBoolean(globImpl.match(arena.allocator(), this.pattern, str.slice()).matches()); + return jsc.JSValue.jsBoolean(bun.glob.match(this.pattern, str.slice()).matches()); } pub fn convertUtf8(codepoints: *std.ArrayList(u32), pattern: []const u8) !void { @@ -390,12 +390,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Arena = std.heap.ArenaAllocator; -const globImpl = @import("../../glob.zig"); -const GlobWalker = globImpl.BunGlobWalker; - const bun = @import("bun"); const BunString = bun.String; const CodepointIterator = bun.strings.UnsignedCodepointIterator; +const GlobWalker = bun.glob.BunGlobWalker; const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d34e0c22bb..62bfe42229 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -127,9 +127,8 @@ pub const TestRunner = struct { const file_path = this.files.items(.source)[file_id].path.text; // Check if the file path matches any of the glob patterns - const glob = @import("../../glob.zig"); for (glob_patterns) |pattern| { - const result = glob.match(this.allocator, pattern, file_path); + const result = bun.glob.match(pattern, file_path); if (result == .match) return true; } return false; diff --git a/src/cli/filter_arg.zig b/src/cli/filter_arg.zig index 0d072805d6..3d0e00c6e6 100644 --- a/src/cli/filter_arg.zig +++ b/src/cli/filter_arg.zig @@ -22,7 +22,7 @@ fn globIgnoreFn(val: []const u8) bool { return false; } -const GlobWalker = Glob.GlobWalker(globIgnoreFn, Glob.walk.DirEntryAccessor, false); +const GlobWalker = glob.GlobWalker(globIgnoreFn, glob.walk.DirEntryAccessor, false); pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logger.Log, out_patterns: *std.ArrayList([]u8), workdir_: []const u8, root_buf: *bun.PathBuffer) ![]const u8 { bun.ast.Expr.Data.Store.create(); @@ -177,7 +177,7 @@ pub const FilterSet = struct { pub fn matchesPath(self: *const FilterSet, path: []const u8) bool { for (self.filters) |filter| { - if (Glob.walk.matchImpl(self.allocator, filter.pattern, path).matches()) { + if (glob.match(filter.pattern, path).matches()) { return true; } } @@ -190,7 +190,7 @@ pub const FilterSet = struct { .name => name, .path => path, }; - if (Glob.walk.matchImpl(self.allocator, filter.pattern, target).matches()) { + if (glob.match(filter.pattern, target).matches()) { return true; } } @@ -275,11 +275,11 @@ pub const PackageFilterIterator = struct { const string = []const u8; -const Glob = @import("../glob.zig"); const std = @import("std"); const bun = @import("bun"); const Global = bun.Global; const JSON = bun.json; const Output = bun.Output; +const glob = bun.glob; const strings = bun.strings; diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 98b2cf0fc4..91f5cd64e7 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -190,14 +190,14 @@ pub const OutdatedCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, @@ -403,7 +403,7 @@ pub const OutdatedCommand = struct { .path => unreachable, .name => |name_pattern| { if (name_pattern.len == 0) continue; - if (!glob.walk.matchImpl(bun.default_allocator, name_pattern, dep.name.slice(string_buf)).matches()) { + if (!glob.match(name_pattern, dep.name.slice(string_buf)).matches()) { break :match false; } }, diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index d8b2f139d4..5c2b7e3d0e 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -293,7 +293,7 @@ pub const PackCommand = struct { // normally the behavior of `index.js` and `**/index.js` are the same, // but includes require `**/` const match_path = if (include.flags.@"leading **/") entry_name else entry_subpath; - switch (glob.walk.matchImpl(allocator, include.glob.slice(), match_path)) { + switch (glob.match(include.glob.slice(), match_path)) { .match => included = true, .negate_no_match, .negate_match => unreachable, else => {}, @@ -310,7 +310,7 @@ pub const PackCommand = struct { const match_path = if (exclude.flags.@"leading **/") entry_name else entry_subpath; // NOTE: These patterns have `!` so `.match` logic is // inverted here - switch (glob.walk.matchImpl(allocator, exclude.glob.slice(), match_path)) { + switch (glob.match(exclude.glob.slice(), match_path)) { .negate_no_match => included = false, else => {}, } @@ -1034,7 +1034,7 @@ pub const PackCommand = struct { // check default ignores that only apply to the root project directory for (root_default_ignore_patterns) |pattern| { - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { // cannot be reversed return .{ @@ -1061,7 +1061,7 @@ pub const PackCommand = struct { for (default_ignore_patterns) |pattern_info| { const pattern, const can_override = pattern_info; - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { if (can_override) { ignored = true; @@ -1103,7 +1103,7 @@ pub const PackCommand = struct { if (pattern.flags.dirs_only and entry.kind != .directory) continue; const match_path = if (pattern.flags.rel_path) rel else entry_name; - switch (glob.walk.matchImpl(bun.default_allocator, pattern.glob.slice(), match_path)) { + switch (glob.match(pattern.glob.slice(), match_path)) { .match => { ignored = true; ignore_pattern = pattern.glob.slice(); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index c74e88db93..28c11d2bba 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1014,7 +1014,7 @@ pub const CommandLineReporter = struct { if (opts.ignore_patterns.len > 0) { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } @@ -1134,7 +1134,7 @@ pub const CommandLineReporter = struct { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } diff --git a/src/cli/update_interactive_command.zig b/src/cli/update_interactive_command.zig index 4aaa36d1a7..873b25bc5b 100644 --- a/src/cli/update_interactive_command.zig +++ b/src/cli/update_interactive_command.zig @@ -602,14 +602,14 @@ pub const UpdateInteractiveCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, diff --git a/src/glob.zig b/src/glob.zig index 5519351638..07b48c5c18 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -1,8 +1,40 @@ +pub const match = @import("./glob/match.zig").match; pub const walk = @import("./glob/GlobWalker.zig"); -pub const match_impl = @import("./glob/match.zig"); -pub const match = match_impl.match; -pub const detectGlobSyntax = match_impl.detectGlobSyntax; - pub const GlobWalker = walk.GlobWalker_; pub const BunGlobWalker = GlobWalker(null, walk.SyscallAccessor, false); pub const BunGlobWalkerZ = GlobWalker(null, walk.SyscallAccessor, true); + +/// Returns true if the given string contains glob syntax, +/// excluding those escaped with backslashes +/// TODO: this doesn't play nicely with Windows directory separator and +/// backslashing, should we just require the user to supply posix filepaths? +pub fn detectGlobSyntax(potential_pattern: []const u8) bool { + // Negation only allowed in the beginning of the pattern + if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; + + // In descending order of how popular the token is + const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; + + inline for (SPECIAL_SYNTAX) |token| { + var slice = potential_pattern[0..]; + while (slice.len > 0) { + if (std.mem.indexOfScalar(u8, slice, token)) |idx| { + // Check for even number of backslashes preceding the + // token to know that it's not escaped + var i = idx; + var backslash_count: u16 = 0; + + while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { + backslash_count += 1; + } + + if (backslash_count % 2 == 0) return true; + slice = slice[idx + 1 ..]; + } else break; + } + } + + return false; +} + +const std = @import("std"); diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index 96a484c663..38f4e30aa1 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -1324,8 +1324,7 @@ pub fn GlobWalker_( } fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { - return match( - this.arena.allocator(), + return bun.glob.match( pattern_component.patternSlice(this.pattern), filepath, ).matches(); @@ -1686,11 +1685,8 @@ pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { return std.mem.eql(u8, literal, path); } -pub const matchImpl = match; - const DirIterator = @import("../bun.js/node/dir_iterator.zig"); const ResolvePath = @import("../resolver/resolve_path.zig"); -const match = @import("./match.zig").match; const bun = @import("bun"); const BunString = bun.String; diff --git a/src/glob/match.zig b/src/glob/match.zig index 391a86f455..36d50919cb 100644 --- a/src/glob/match.zig +++ b/src/glob/match.zig @@ -33,7 +33,7 @@ const Brace = struct { }; const BraceStack = bun.BoundedArray(Brace, 10); -pub const MatchResult = enum { +const MatchResult = enum { no_match, match, @@ -116,7 +116,7 @@ const Wildcard = struct { /// Used to escape any of the special characters above. // TODO: consider just taking arena and resetting to initial state, // all usages of this function pass in Arena.allocator() -pub fn match(_: Allocator, glob: []const u8, path: []const u8) MatchResult { +pub fn match(glob: []const u8, path: []const u8) MatchResult { var state = State{}; var negated = false; @@ -486,39 +486,6 @@ inline fn skipGlobstars(glob: []const u8, glob_index: *u32) void { glob_index.* -= 2; } -/// Returns true if the given string contains glob syntax, -/// excluding those escaped with backslashes -/// TODO: this doesn't play nicely with Windows directory separator and -/// backslashing, should we just require the user to supply posix filepaths? -pub fn detectGlobSyntax(potential_pattern: []const u8) bool { - // Negation only allowed in the beginning of the pattern - if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; - - // In descending order of how popular the token is - const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; - - inline for (SPECIAL_SYNTAX) |token| { - var slice = potential_pattern[0..]; - while (slice.len > 0) { - if (std.mem.indexOfScalar(u8, slice, token)) |idx| { - // Check for even number of backslashes preceding the - // token to know that it's not escaped - var i = idx; - var backslash_count: u16 = 0; - - while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { - backslash_count += 1; - } - - if (backslash_count % 2 == 0) return true; - slice = slice[idx + 1 ..]; - } else break; - } - } - - return false; -} - const BraceIndex = struct { start: u32 = 0, end: u32 = 0, @@ -526,4 +493,3 @@ const BraceIndex = struct { const bun = @import("bun"); const std = @import("std"); -const Allocator = std.mem.Allocator; diff --git a/src/install/PackageManager/install_with_manager.zig b/src/install/PackageManager/install_with_manager.zig index 90d016cd8c..cb4663e145 100644 --- a/src/install/PackageManager/install_with_manager.zig +++ b/src/install/PackageManager/install_with_manager.zig @@ -1064,7 +1064,7 @@ pub fn getWorkspaceFilters(manager: *PackageManager, original_cwd: []const u8) ! }, }; - switch (bun.glob.walk.matchImpl(manager.allocator, pattern, path_or_name)) { + switch (bun.glob.match(pattern, path_or_name)) { .match, .negate_match => install_root_dependencies = true, .negate_no_match => { diff --git a/src/install/lockfile/Package/WorkspaceMap.zig b/src/install/lockfile/Package/WorkspaceMap.zig index f0cbc1279e..2373d9db1d 100644 --- a/src/install/lockfile/Package/WorkspaceMap.zig +++ b/src/install/lockfile/Package/WorkspaceMap.zig @@ -130,7 +130,7 @@ pub fn processNamesArray( if (input_path.len == 0 or input_path.len == 1 and input_path[0] == '.') continue; - if (Glob.detectGlobSyntax(input_path)) { + if (glob.detectGlobSyntax(input_path)) { bun.handleOom(workspace_globs.append(input_path)); continue; } @@ -215,7 +215,7 @@ pub fn processNamesArray( if (workspace_globs.items.len > 0) { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - for (workspace_globs.items) |user_pattern| { + for (workspace_globs.items, 0..) |user_pattern, i| { defer _ = arena.reset(.retain_capacity); const glob_pattern = if (user_pattern.len == 0) "package.json" else brk: { @@ -253,7 +253,7 @@ pub fn processNamesArray( return error.GlobError; } - while (switch (try iter.next()) { + next_match: while (switch (try iter.next()) { .result => |r| r, .err => |e| { log.addErrorFmt( @@ -271,6 +271,28 @@ pub fn processNamesArray( // skip root package.json if (strings.eqlComptime(matched_path, "package.json")) continue; + { + const matched_path_without_package_json = strings.withoutTrailingSlash(strings.withoutSuffixComptime(matched_path, "package.json")); + + // check if it's negated by any remaining patterns + for (workspace_globs.items[i + 1 ..]) |next_pattern| { + switch (bun.glob.match(next_pattern, matched_path_without_package_json)) { + .no_match, + .match, + .negate_match, + => {}, + + .negate_no_match => { + debug("skipping negated path: {s}, {s}\n", .{ + matched_path_without_package_json, + next_pattern, + }); + continue :next_match; + }, + } + } + } + debug("matched path: {s}, dirname: {s}\n", .{ matched_path, entry_dir }); const abs_package_json_path = Path.joinAbsStringBufZ( @@ -375,7 +397,7 @@ fn ignoredWorkspacePaths(path: []const u8) bool { } return false; } -const GlobWalker = Glob.GlobWalker(ignoredWorkspacePaths, Glob.walk.SyscallAccessor, false); +const GlobWalker = glob.GlobWalker(ignoredWorkspacePaths, glob.walk.SyscallAccessor, false); const string = []const u8; const debug = Output.scoped(.Lockfile, .hidden); @@ -386,10 +408,10 @@ const Allocator = std.mem.Allocator; const bun = @import("bun"); const Environment = bun.Environment; -const Glob = bun.glob; const JSAst = bun.ast; const Output = bun.Output; const Path = bun.path; +const glob = bun.glob; const logger = bun.logger; const strings = bun.strings; diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index 2b5f958659..a75feae070 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -408,7 +408,7 @@ pub fn isFilteredDependencyOrWorkspace( }, }; - switch (bun.glob.match(undefined, pattern, name_or_path)) { + switch (bun.glob.match(pattern, name_or_path)) { .match, .negate_match => workspace_matched = true, .negate_no_match => { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index e118f86f89..1ccf7529ca 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -150,7 +150,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (glob_list.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } @@ -166,7 +166,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (mixed.globs.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index b14bcb5b99..04fe05f620 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -67,7 +67,7 @@ pub const WorkPool = jsc.WorkPool; pub const Pipe = [2]bun.FileDescriptor; pub const SmolList = shell.SmolList; -pub const GlobWalker = Glob.BunGlobWalkerZ; +pub const GlobWalker = bun.glob.BunGlobWalkerZ; pub const stdin_no = 0; pub const stdout_no = 1; @@ -1957,7 +1957,6 @@ pub fn unreachableState(context: []const u8, state: []const u8) noreturn { return bun.Output.panic("Bun shell has reached an unreachable state \"{s}\" in the {s} context. This indicates a bug, please open a GitHub issue.", .{ state, context }); } -const Glob = @import("../glob.zig"); const builtin = @import("builtin"); const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct; diff --git a/src/shell/shell.zig b/src/shell/shell.zig index e8ded9f1ae..7759256915 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -17,7 +17,7 @@ pub const IOReader = Interpreter.IOReader; pub const Yield = @import("./Yield.zig").Yield; pub const unreachableState = interpret.unreachableState; -const GlobWalker = Glob.GlobWalker_(null, true); +const GlobWalker = bun.glob.GlobWalker(null, true); // const GlobWalker = Glob.BunGlobWalker; pub const SUBSHELL_TODO_ERROR = "Subshells are not implemented, please open GitHub issue!"; @@ -4429,7 +4429,6 @@ pub const TestingAPIs = struct { pub const ShellSubprocess = @import("./subproc.zig").ShellSubprocess; -const Glob = @import("../glob.zig"); const Syscall = @import("../sys.zig"); const builtin = @import("builtin"); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 219c2b50ac..a5a2ba94a5 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -136,6 +136,44 @@ test("dependency on workspace without version in package.json", async () => { } }, 20_000); +test("allowing negative workspace patterns", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "root", + workspaces: ["packages/*", "!packages/pkg2"], + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "doesnt-exist-oops": "1.2.3", + }, + }), + ), + ]); + + const { exited } = await runBunInstall(env, packageDir); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + }); +}); + test("dependency on same name as workspace and dist-tag", async () => { await Promise.all([ write( From 46d6e0885b805cfb8d7148aaa13a984546178093 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 00:53:15 -0700 Subject: [PATCH 017/391] fix(pnpm migration): fix `"lockfileVersion"` number parsing (#23232) ### What does this PR do? Parsing would fail because the lockfile version might be parsing as a non-whole float instead of a string (`5.4` vs `'5.4'`) and the migration would have the wrong error. ### How did you verify your code works? Added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Bot Co-authored-by: Claude --- src/install/migration.zig | 25 +------------------ src/install/pnpm.zig | 8 +++--- .../install/migration/pnpm-migration.test.ts | 19 ++++++++++++++ .../pnpm/version-number-dot/package.json | 0 .../pnpm/version-number-dot/pnpm-lock.yaml | 1 + 5 files changed, 25 insertions(+), 28 deletions(-) create mode 100644 test/cli/install/migration/pnpm/version-number-dot/package.json create mode 100644 test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml diff --git a/src/install/migration.zig b/src/install/migration.zig index 2390d4e850..fcf417b140 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -26,14 +26,6 @@ pub fn detectAndLoadOtherLockfile( , .{}); Global.exit(1); } - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid NPM package-lock.json\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } return LoadResult{ .err = .{ .step = .migrating, .value = err, @@ -58,14 +50,6 @@ pub fn detectAndLoadOtherLockfile( defer lockfile.close(); const data = lockfile.readToEnd(allocator).unwrap() catch break :yarn; const migrate_result = @import("./yarn.zig").migrateYarnLockfile(this, manager, allocator, log, data, dir) catch |err| { - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid yarn.lock\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } return LoadResult{ .err = .{ .step = .migrating, .value = err, @@ -137,14 +121,7 @@ pub fn detectAndLoadOtherLockfile( }, else => {}, } - if (Environment.isDebug) { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - Output.prettyErrorln("Error: {s}", .{@errorName(err)}); - log.print(Output.errorWriter()) catch {}; - Output.prettyErrorln("Invalid pnpm-lock.yaml\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); - Global.exit(1); - } + log.reset(); return LoadResult{ .err = .{ .step = .migrating, .value = err, diff --git a/src/install/pnpm.zig b/src/install/pnpm.zig index eb7e759639..33e733a6ad 100644 --- a/src/install/pnpm.zig +++ b/src/install/pnpm.zig @@ -105,21 +105,21 @@ pub fn migratePnpmLockfile( return error.PnpmLockfileMissingVersion; }; - const lockfile_version_num: u32 = lockfile_version: { + const lockfile_version_num: f64 = lockfile_version: { err: { switch (lockfile_version_expr.data) { .e_number => |num| { - if (num.value < 0 or num.value > std.math.maxInt(u32)) { + if (num.value < 0) { break :err; } - break :lockfile_version @intFromFloat(std.math.divExact(f64, num.value, 1) catch break :err); + break :lockfile_version num.value; }, .e_string => |version_str| { const str = version_str.slice(allocator); const end = strings.indexOfChar(str, '.') orelse str.len; - break :lockfile_version std.fmt.parseUnsigned(u32, str[0..end], 10) catch break :err; + break :lockfile_version std.fmt.parseFloat(f64, str[0..end]) catch break :err; }, else => {}, } diff --git a/test/cli/install/migration/pnpm-migration.test.ts b/test/cli/install/migration/pnpm-migration.test.ts index 5ce3d4219e..d92a68ad23 100644 --- a/test/cli/install/migration/pnpm-migration.test.ts +++ b/test/cli/install/migration/pnpm-migration.test.ts @@ -54,6 +54,25 @@ test("basic", async () => { expect(err).not.toContain("Saved lockfile"); }); +test("version is number with dot", async () => { + const { packageDir } = await verdaccio.createTestDir({ + files: join(import.meta.dir, "pnpm/version-number-dot"), + }); + + let proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + + let [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + expect(err).toContain("pnpm-lock.yaml version is too old (< v7)"); +}); + describe.todo("bin", () => { test("manifests are fetched for bins", async () => { const { packageDir, packageJson } = await verdaccio.createTestDir({ diff --git a/test/cli/install/migration/pnpm/version-number-dot/package.json b/test/cli/install/migration/pnpm/version-number-dot/package.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml b/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml new file mode 100644 index 0000000000..9764deb1cc --- /dev/null +++ b/test/cli/install/migration/pnpm/version-number-dot/pnpm-lock.yaml @@ -0,0 +1 @@ +lockfileVersion: 5.4 From 02d0586da59550796f89425f50b3bd47171b604b Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 00:54:24 -0700 Subject: [PATCH 018/391] Increase crash report stack trace buffer from 10 to 20 frames (#23225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Increase the stack trace buffer size in the crash handler from 10 to 20 frames to ensure more useful frames are included in crash reports sent to bun.report. ## Motivation Currently, we capture up to 10 stack frames when generating crash reports. However, many of these frames get filtered out when `StackLine.fromAddress()` returns `null` for invalid/empty frames. This results in only a small number of frames (sometimes as few as 5) actually being sent to the server. ## Changes - Increased `addr_buf` array size from `[10]usize` to `[20]usize` in `src/crash_handler.zig:307` ## Impact By capturing more frames initially, we ensure that after filtering we still have a meaningful number of frames in the crash report. This will help with debugging crashes by providing more context about the call stack. The encoding function `encodeTraceString()` has no hardcoded limits and will encode all available frames, so this change directly translates to more frames being sent to bun.report. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/crash_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 2cea2ce25e..56f4545395 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -304,7 +304,7 @@ pub fn crashHandler( writer.print("Crashed while {}\n", .{action}) catch std.posix.abort(); } - var addr_buf: [10]usize = undefined; + var addr_buf: [20]usize = undefined; var trace_buf: std.builtin.StackTrace = undefined; // If a trace was not provided, compute one now From 9993e120503693145ee1520e21c9099324ef13ec Mon Sep 17 00:00:00 2001 From: pfg Date: Sat, 4 Oct 2025 01:50:09 -0700 Subject: [PATCH 019/391] Unify timer enum (#23228) ### What does this PR do? Unify EventLoopTimer.Tag to one enum instead of two ### How did you verify your code works? Build & CI --- src/bun.js/api/Timer/EventLoopTimer.zig | 55 ++++--------------------- 1 file changed, 8 insertions(+), 47 deletions(-) diff --git a/src/bun.js/api/Timer/EventLoopTimer.zig b/src/bun.js/api/Timer/EventLoopTimer.zig index 0a0ea9dbf2..c8ed6d911b 100644 --- a/src/bun.js/api/Timer/EventLoopTimer.zig +++ b/src/bun.js/api/Timer/EventLoopTimer.zig @@ -47,7 +47,7 @@ pub fn less(_: void, a: *const Self, b: *const Self) bool { return order == .lt; } -pub const Tag = if (Environment.isWindows) enum { +pub const Tag = enum { TimerCallback, TimeoutObject, ImmediateObject, @@ -78,7 +78,7 @@ pub const Tag = if (Environment.isWindows) enum { .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, .DNSResolver => DNSResolver, - .WindowsNamedPipe => uws.WindowsNamedPipe, + .WindowsNamedPipe => if (Environment.isWindows) uws.WindowsNamedPipe else UnreachableTimer, .WTFTimer => WTFTimer, .PostgresSQLConnectionTimeout => jsc.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => jsc.Postgres.PostgresSQLConnection, @@ -96,52 +96,13 @@ pub const Tag = if (Environment.isWindows) enum { .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, }; } -} else enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - StatWatcherScheduler, - UpgradedDuplex, - WTFTimer, - DNSResolver, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - MySQLConnectionTimeout, - MySQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - AbortSignalTimeout, - DateHeaderTimer, - BunTest, - EventLoopDelayMonitor, +}; - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .WTFTimer => WTFTimer, - .DNSResolver => DNSResolver, - .PostgresSQLConnectionTimeout => jsc.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => jsc.Postgres.PostgresSQLConnection, - .MySQLConnectionTimeout => jsc.MySQL.MySQLConnection, - .MySQLConnectionMaxLifetime => jsc.MySQL.MySQLConnection, - .ValkeyConnectionTimeout => jsc.API.Valkey, - .ValkeyConnectionReconnect => jsc.API.Valkey, - .SubprocessTimeout => jsc.Subprocess, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - .AbortSignalTimeout => jsc.WebCore.AbortSignal.Timeout, - .DateHeaderTimer => jsc.API.Timer.DateHeaderTimer, - .BunTest => jsc.Jest.bun_test.BunTest, - .EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor, - }; +const UnreachableTimer = struct { + event_loop_timer: Self, + fn callback(_: *UnreachableTimer, _: *UnreachableTimer) Arm { + if (Environment.ci_assert) bun.assert(false); + return .disarm; } }; From 578a47ce4a3930bff98346a3bc90734167c2d5ed Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 4 Oct 2025 17:56:42 +0900 Subject: [PATCH 020/391] Fix segmentation fault during building stack traces string (#22902) ### What does this PR do? Bun sometimes crashes with a segmentation fault while generating stack traces. the following might be happening in `remapZigException`: 1. The first populateStackTrace (OnlyPosition) sets `frames_len` (e.g., frames_len = 5) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/bindings/bindings.cpp#L4793 ``` [frame1, frame2, frame3, frame4, frame5] ``` 2. Frame filtering in remapZigException reduces `frames_len` (e.g., frames_len = 3) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/VirtualMachine.zig#L2686-L2704 ``` [frame1, frame4, frame5, (frame4, frame5)] // frame2 and frame3 are removed by filtering; frames_len is set to 3 here, but frame4 and frame5 remain in their original positions ``` 3. The second populateStackTrace (OnlySourceLine) increases `frames_len` (e.g., frames_len = 5) https://github.com/oven-sh/bun/blob/613aea1787f1c8ef78d54a4dbc322b9fe59d1743/src/bun.js/bindings/bindings.cpp#L4793 ``` [frame1, frame4, frame5, frame4, frame5] ``` When deinit is executed on these frames, the ref count is excessively decremented (for frame4 and frame5), resulting in a UAF. ### How did you verify your code works? WIP. I'm working on creating minimal reproduction code. However, I've confirmed that `twenty-server` tests passes with this PR. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- src/bun.js/bindings/ZigStackFrame.zig | 4 +++ src/bun.js/bindings/bindings.cpp | 42 +++++++++++++++-------- src/bun.js/bindings/headers-handwritten.h | 12 +++++++ src/logger.zig | 10 ------ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index 3d9bc442ae..4082b86f25 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -11,6 +11,9 @@ pub const ZigStackFrame = extern struct { /// This informs formatters whether to display as a blob URL or not remapped: bool = false, + /// -1 means not set. + jsc_stack_frame_index: i32 = -1, + pub fn deinit(this: *ZigStackFrame) void { this.function_name.deref(); this.source_url.deref(); @@ -213,6 +216,7 @@ pub const ZigStackFrame = extern struct { .source_url = .empty, .position = .invalid, .is_async = false, + .jsc_stack_frame_index = -1, }; pub fn nameFormatter(this: *const ZigStackFrame, comptime enable_color: bool) NameFormatter { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index e41c9d6979..e8147330ea 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4772,25 +4772,37 @@ public: static void populateStackTrace(JSC::VM& vm, const WTF::Vector& frames, ZigStackTrace& trace, JSC::JSGlobalObject* globalObject, PopulateStackTraceFlags flags) { - uint8_t frame_i = 0; - size_t stack_frame_i = 0; - const size_t total_frame_count = frames.size(); - const uint8_t frame_count = total_frame_count < trace.frames_cap ? total_frame_count : trace.frames_cap; + if (flags == PopulateStackTraceFlags::OnlyPosition) { + uint8_t frame_i = 0; + size_t stack_frame_i = 0; + const size_t total_frame_count = frames.size(); + const uint8_t frame_count = total_frame_count < trace.frames_cap ? total_frame_count : trace.frames_cap; - while (frame_i < frame_count && stack_frame_i < total_frame_count) { - // Skip native frames - while (stack_frame_i < total_frame_count && !(frames.at(stack_frame_i).hasLineAndColumnInfo()) && !(frames.at(stack_frame_i).isWasmFrame())) { + while (frame_i < frame_count && stack_frame_i < total_frame_count) { + // Skip native frames + while (stack_frame_i < total_frame_count && !(frames.at(stack_frame_i).hasLineAndColumnInfo()) && !(frames.at(stack_frame_i).isWasmFrame())) { + stack_frame_i++; + } + if (stack_frame_i >= total_frame_count) + break; + + ZigStackFrame& frame = trace.frames_ptr[frame_i]; + frame.jsc_stack_frame_index = static_cast(stack_frame_i); + populateStackFrame(vm, trace, frames[stack_frame_i], frame, frame_i == 0, &trace.referenced_source_provider, globalObject, flags); stack_frame_i++; + frame_i++; + } + trace.frames_len = frame_i; + } else if (flags == PopulateStackTraceFlags::OnlySourceLines) { + for (uint8_t i = 0; i < trace.frames_len; i++) { + ZigStackFrame& frame = trace.frames_ptr[i]; + // A call with flags set to OnlySourceLines always follows a call with flags set to OnlyPosition, + // so jsc_stack_frame_index is always a valid value here. + ASSERT(frame.jsc_stack_frame_index >= 0); + ASSERT(static_cast(frame.jsc_stack_frame_index) < frames.size()); + populateStackFrame(vm, trace, frames[frame.jsc_stack_frame_index], frame, i == 0, &trace.referenced_source_provider, globalObject, flags); } - if (stack_frame_i >= total_frame_count) - break; - - ZigStackFrame& frame = trace.frames_ptr[frame_i]; - populateStackFrame(vm, trace, frames[stack_frame_i], frame, frame_i == 0, &trace.referenced_source_provider, globalObject, flags); - stack_frame_i++; - frame_i++; } - trace.frames_len = frame_i; } static JSC::JSValue getNonObservable(JSC::VM& vm, JSC::JSGlobalObject* global, JSC::JSObject* obj, const JSC::PropertyName& propertyName) diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index b5f56ffa0a..26d03cbb10 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -183,6 +183,18 @@ typedef struct ZigStackFrame { ZigStackFrameCode code_type; bool is_async; bool remapped; + int32_t jsc_stack_frame_index; + + ZigStackFrame() + : function_name {} + , source_url {} + , position {} + , code_type {} + , is_async(false) + , remapped(false) + , jsc_stack_frame_index(-1) + { + } } ZigStackFrame; typedef struct ZigStackTrace { diff --git a/src/logger.zig b/src/logger.zig index bd858b7458..c778cc9cad 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -1335,15 +1335,6 @@ pub const Log = struct { if (needs_newline) _ = try to.write("\n"); } - - pub fn toZigException(this: *const Log, allocator: std.mem.Allocator) *js.ZigException.Holder { - var holder = try allocator.create(js.ZigException.Holder); - holder.* = js.ZigException.Holder.init(); - var zig_exception: *js.ZigException = holder.zigException(); - zig_exception.exception = this; - zig_exception.code = js.JSErrorCode.BundlerError; - return holder; - } }; pub inline fn usize2Loc(loc: usize) Loc { @@ -1617,7 +1608,6 @@ const Output = bun.Output; const StringBuilder = bun.StringBuilder; const assert = bun.assert; const default_allocator = bun.default_allocator; -const js = bun.jsc; const jsc = bun.jsc; const strings = bun.strings; const Index = bun.ast.Index; From 9cab1fbfe0b25b5fd49db0f2ba208829f1cc51ab Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:17:55 -0700 Subject: [PATCH 021/391] update CLAUDE.md --- CLAUDE.md | 56 +----------------------------------------------- src/AGENTS.md | 1 + src/CLAUDE.md | 5 +++++ src/js/CLAUDE.md | 2 ++ 4 files changed, 9 insertions(+), 55 deletions(-) create mode 120000 src/AGENTS.md create mode 100644 src/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 09a8499345..54f107ec54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,47 +177,6 @@ Built-in JavaScript modules use special syntax and are organized as: - `internal/` - Internal modules not exposed to users - `builtins/` - Core JavaScript builtins (streams, console, etc.) -### Special Syntax in Built-in Modules - -1. **`$` prefix** - Access to private properties and JSC intrinsics: - - ```js - const arr = $Array.from(...); // Private global - map.$set(...); // Private method - const arr2 = $newArrayWithSize(5); // JSC intrinsic - ``` - -2. **`require()`** - Must use string literals, resolved at compile time: - - ```js - const fs = require("fs"); // Directly loads by numeric ID - ``` - -3. **Debug helpers**: - - `$debug()` - Like console.log but stripped in release builds - - `$assert()` - Assertions stripped in release builds - - `if($debug) {}` - Check if debug env var is set - -4. **Platform detection**: `process.platform` and `process.arch` are inlined and dead-code eliminated - -5. **Export syntax**: Use `export default` which gets converted to a return statement: - ```js - export default { - readFile, - writeFile, - }; - ``` - -Note: These are NOT ES modules. The preprocessor converts `$` to `@` (JSC's actual syntax) and handles the special functions. - -## CI - -Bun uses BuildKite for CI. To get the status of a PR, you can use the following command: - -```bash -bun ci -``` - ## Important Development Notes 1. **Never use `bun test` or `bun ` directly** - always use `bun bd test` or `bun bd `. `bun bd` compiles & runs the debug build. @@ -229,19 +188,6 @@ bun ci 7. **Avoid shell commands** - Don't use `find` or `grep` in tests; use Bun's Glob and built-in tools 8. **Memory management** - In Zig code, be careful with allocators and use defer for cleanup 9. **Cross-platform** - Run `bun run zig:check-all` to compile the Zig code on all platforms when making platform-specific changes -10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific scopes +10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s 11. **Be humble & honest** - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user. 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. - -## Key APIs and Features - -### Bun-Specific APIs - -- **Bun.serve()** - High-performance HTTP server -- **Bun.spawn()** - Process spawning with better performance than Node.js -- **Bun.file()** - Fast file I/O operations -- **Bun.write()** - Unified API for writing to files, stdout, etc. -- **Bun.$ (Shell)** - Cross-platform shell scripting -- **Bun.SQLite** - Native SQLite integration -- **Bun.FFI** - Call native libraries from JavaScript -- **Bun.Glob** - Fast file pattern matching diff --git a/src/AGENTS.md b/src/AGENTS.md new file mode 120000 index 0000000000..681311eb9c --- /dev/null +++ b/src/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 100644 index 0000000000..d53c2b889b --- /dev/null +++ b/src/CLAUDE.md @@ -0,0 +1,5 @@ +## Zig + +- Prefer `@import` at the **bottom** of the file. +- It's `@import("bun")` not `@import("root").bun` +- You must be patient with the build. diff --git a/src/js/CLAUDE.md b/src/js/CLAUDE.md index ed175a119a..e34bbfc526 100644 --- a/src/js/CLAUDE.md +++ b/src/js/CLAUDE.md @@ -72,6 +72,8 @@ $debug("Module loaded:", name); // Debug (stripped in release) $assert(condition, "message"); // Assertions (stripped in release) ``` +**Platform detection**: `process.platform` and `process.arch` are inlined and dead-code eliminated + ## Validation and Errors ```typescript From 4424c5ed08bc690553218abbafeeca78fe1378c7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:20:59 -0700 Subject: [PATCH 022/391] Update CLAUDE.md --- CLAUDE.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 54f107ec54..d6c6ff3675 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,19 +143,6 @@ When implementing JavaScript classes in C++: 3. Add iso subspaces for classes with C++ fields 4. Cache structures in ZigGlobalObject -## Development Workflow - -### Code Formatting - -- `bun run prettier` - Format JS/TS files -- `bun run zig-format` - Format Zig files -- `bun run clang-format` - Format C++ files - -### Watching for Changes - -- `bun run watch` - Incremental Zig compilation with error checking -- `bun run watch-windows` - Windows-specific watch mode - ### Code Generation Code generation happens automatically as part of the build process. The main scripts are: @@ -188,6 +175,6 @@ Built-in JavaScript modules use special syntax and are organized as: 7. **Avoid shell commands** - Don't use `find` or `grep` in tests; use Bun's Glob and built-in tools 8. **Memory management** - In Zig code, be careful with allocators and use defer for cleanup 9. **Cross-platform** - Run `bun run zig:check-all` to compile the Zig code on all platforms when making platform-specific changes -10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s +10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s 11. **Be humble & honest** - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user. 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. From 3c9433f9af7864335439f45e3164bd997015a0d3 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 4 Oct 2025 02:48:50 -0700 Subject: [PATCH 023/391] fix(sqlite) enable order by and limit in delete/update statements on windows (#23227) ### What does this PR do? Enable compiler flags Update SQLite amalgamation using https://www.sqlite.org/download.html source code [sqlite-src-3500400.zip](https://www.sqlite.org/2025/sqlite-src-3500400.zip) with: ```bash ./configure CFLAGS="-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT" make sqlite3.c ``` This is the same version that before just with this adicional flag that must be enabled when generating the amalgamation so we are actually able to use this option. You can also see that without this the build will happen but the feature will not be enable https://buildkite.com/bun/bun/builds/27940, as informed in https://www.sqlite.org/howtocompile.html topic 5. ### How did you verify your code works? Add in CI two tests that check if the feature is enabled on windows --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- .github/workflows/update-sqlite3.yml | 19 +- scripts/update-sqlite-amalgamation.sh | 60 ++ src/bun.js/bindings/sqlite/CMakeLists.txt | 2 + src/bun.js/bindings/sqlite/sqlite3.c | 753 +++++++++++----------- test/js/sql/sqlite-sql.test.ts | 18 +- 5 files changed, 465 insertions(+), 387 deletions(-) create mode 100755 scripts/update-sqlite-amalgamation.sh diff --git a/.github/workflows/update-sqlite3.yml b/.github/workflows/update-sqlite3.yml index 6ee8115f7c..65321f466a 100644 --- a/.github/workflows/update-sqlite3.yml +++ b/.github/workflows/update-sqlite3.yml @@ -70,24 +70,7 @@ jobs: - name: Update SQLite if needed if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num run: | - set -euo pipefail - - TEMP_DIR=$(mktemp -d) - cd $TEMP_DIR - - echo "Downloading from: https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - - # Download and extract latest version - wget "https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - unzip "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - cd "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}" - - # Add header comment and copy files - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - cat sqlite3.c >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h - cat sqlite3.h >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h + ./scripts/update-sqlite-amalgamation.sh ${{ steps.check-version.outputs.latest_num }} ${{ steps.check-version.outputs.latest_year }} - name: Create Pull Request if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num diff --git a/scripts/update-sqlite-amalgamation.sh b/scripts/update-sqlite-amalgamation.sh new file mode 100755 index 0000000000..f580f0dc5d --- /dev/null +++ b/scripts/update-sqlite-amalgamation.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script updates SQLite amalgamation files with the required compiler flags. +# It downloads the SQLite source, configures it with necessary flags, builds the +# amalgamation, and copies the generated files to the Bun source tree. +# +# Usage: +# ./scripts/update-sqlite-amalgamation.sh +# +# Example: +# ./scripts/update-sqlite-amalgamation.sh 3500400 2025 +# +# The version number is a 7-digit SQLite version (e.g., 3500400 for 3.50.4) +# The year is the release year found in the download URL + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 3500400 2025" + exit 1 +fi + +VERSION_NUM="$1" +YEAR="$2" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +cd "$TEMP_DIR" + +echo "Downloading SQLite source version $VERSION_NUM from year $YEAR..." +DOWNLOAD_URL="https://sqlite.org/$YEAR/sqlite-src-$VERSION_NUM.zip" +echo "URL: $DOWNLOAD_URL" + +wget -q "$DOWNLOAD_URL" +unzip -q "sqlite-src-$VERSION_NUM.zip" +cd "sqlite-src-$VERSION_NUM" + +echo "Configuring SQLite with required flags..." +# These flags must be set during amalgamation generation for them to take effect +# in the parser and other compile-time generated code +CFLAGS="-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1 -DSQLITE_ENABLE_COLUMN_METADATA=1" +./configure CFLAGS="$CFLAGS" > /dev/null 2>&1 + +echo "Building amalgamation..." +make sqlite3.c > /dev/null 2>&1 + +echo "Copying files to Bun source tree..." +# Add clang-format off directive and copy the amalgamation +echo "// clang-format off" > "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3.c" +cat sqlite3.c >> "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3.c" + +echo "// clang-format off" > "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3_local.h" +cat sqlite3.h >> "$REPO_ROOT/src/bun.js/bindings/sqlite/sqlite3_local.h" + +echo "✓ Successfully updated SQLite amalgamation files" diff --git a/src/bun.js/bindings/sqlite/CMakeLists.txt b/src/bun.js/bindings/sqlite/CMakeLists.txt index 9609feb3d3..065a862978 100644 --- a/src/bun.js/bindings/sqlite/CMakeLists.txt +++ b/src/bun.js/bindings/sqlite/CMakeLists.txt @@ -12,6 +12,8 @@ target_compile_definitions(sqlite3 PRIVATE "SQLITE_ENABLE_FTS5=1" "SQLITE_ENABLE_JSON1=1" "SQLITE_ENABLE_MATH_FUNCTIONS=1" + "SQLITE_ENABLE_UPDATE_DELETE_LIMIT=1" + "SQLITE_UDL_CAPABLE_PARSER=1" ) if(WIN32) diff --git a/src/bun.js/bindings/sqlite/sqlite3.c b/src/bun.js/bindings/sqlite/sqlite3.c index e218709dfb..fd226702de 100644 --- a/src/bun.js/bindings/sqlite/sqlite3.c +++ b/src/bun.js/bindings/sqlite/sqlite3.c @@ -29,6 +29,7 @@ #ifndef SQLITE_PRIVATE # define SQLITE_PRIVATE static #endif +#define SQLITE_UDL_CAPABLE_PARSER 1 /************** Begin file sqliteInt.h ***************************************/ /* ** 2001 September 15 @@ -175265,7 +175266,9 @@ SQLITE_PRIVATE void sqlite3WindowCodeStep( /************** End of window.c **********************************************/ /************** Begin file parse.c *******************************************/ /* This file is automatically generated by Lemon from input grammar -** source file "parse.y". +** source file "parse.y" with these options: +** +** -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT */ /* ** 2001-09-15 @@ -175811,18 +175814,18 @@ typedef union { #define sqlite3ParserCTX_FETCH Parse *pParse=yypParser->pParse; #define sqlite3ParserCTX_STORE yypParser->pParse=pParse; #define YYFALLBACK 1 -#define YYNSTATE 583 +#define YYNSTATE 587 #define YYNRULE 409 #define YYNRULE_WITH_ACTION 344 #define YYNTOKEN 187 -#define YY_MAX_SHIFT 582 -#define YY_MIN_SHIFTREDUCE 845 -#define YY_MAX_SHIFTREDUCE 1253 -#define YY_ERROR_ACTION 1254 -#define YY_ACCEPT_ACTION 1255 -#define YY_NO_ACTION 1256 -#define YY_MIN_REDUCE 1257 -#define YY_MAX_REDUCE 1665 +#define YY_MAX_SHIFT 586 +#define YY_MIN_SHIFTREDUCE 849 +#define YY_MAX_SHIFTREDUCE 1257 +#define YY_ERROR_ACTION 1258 +#define YY_ACCEPT_ACTION 1259 +#define YY_NO_ACTION 1260 +#define YY_MIN_REDUCE 1261 +#define YY_MAX_REDUCE 1669 #define YY_MIN_DSTRCTR 206 #define YY_MAX_DSTRCTR 320 /************* End control #defines *******************************************/ @@ -175909,227 +175912,227 @@ typedef union { *********** Begin parsing tables **********************************************/ #define YY_ACTTAB_COUNT (2207) static const YYACTIONTYPE yy_action[] = { - /* 0 */ 130, 127, 234, 282, 282, 1328, 576, 1307, 460, 289, - /* 10 */ 289, 576, 1622, 381, 576, 1328, 573, 576, 562, 413, - /* 20 */ 1300, 1542, 573, 481, 562, 524, 460, 459, 558, 82, - /* 30 */ 82, 983, 294, 375, 51, 51, 498, 61, 61, 984, - /* 40 */ 82, 82, 1577, 137, 138, 91, 7, 1228, 1228, 1063, - /* 50 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 413, - /* 60 */ 288, 288, 182, 288, 288, 481, 536, 288, 288, 130, - /* 70 */ 127, 234, 432, 573, 525, 562, 573, 557, 562, 1290, - /* 80 */ 573, 421, 562, 137, 138, 91, 559, 1228, 1228, 1063, - /* 90 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 296, - /* 100 */ 460, 398, 1249, 134, 134, 134, 134, 133, 133, 132, - /* 110 */ 132, 132, 131, 128, 451, 451, 1050, 1050, 1064, 1067, - /* 120 */ 1255, 1, 1, 582, 2, 1259, 581, 1174, 1259, 1174, - /* 130 */ 321, 413, 155, 321, 1584, 155, 379, 112, 481, 1341, - /* 140 */ 456, 299, 1341, 134, 134, 134, 134, 133, 133, 132, - /* 150 */ 132, 132, 131, 128, 451, 137, 138, 91, 498, 1228, - /* 160 */ 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, - /* 170 */ 136, 1204, 862, 1281, 288, 288, 283, 288, 288, 523, - /* 180 */ 523, 1250, 139, 578, 7, 578, 1345, 573, 1169, 562, - /* 190 */ 573, 1054, 562, 136, 136, 136, 136, 129, 573, 547, - /* 200 */ 562, 1169, 245, 1541, 1169, 245, 133, 133, 132, 132, - /* 210 */ 132, 131, 128, 451, 302, 134, 134, 134, 134, 133, - /* 220 */ 133, 132, 132, 132, 131, 128, 451, 1575, 1204, 1205, - /* 230 */ 1204, 7, 470, 550, 455, 413, 550, 455, 130, 127, + /* 0 */ 130, 127, 234, 282, 282, 1332, 580, 1311, 464, 289, + /* 10 */ 289, 580, 1626, 385, 580, 1332, 577, 580, 566, 417, + /* 20 */ 1304, 1546, 577, 485, 566, 528, 464, 463, 562, 82, + /* 30 */ 82, 987, 294, 379, 51, 51, 502, 61, 61, 988, + /* 40 */ 82, 82, 1581, 137, 138, 91, 7, 1232, 1232, 1067, + /* 50 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 417, + /* 60 */ 288, 288, 182, 288, 288, 485, 540, 288, 288, 130, + /* 70 */ 127, 234, 436, 577, 529, 566, 577, 561, 566, 1294, + /* 80 */ 577, 425, 566, 137, 138, 91, 563, 1232, 1232, 1067, + /* 90 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 296, + /* 100 */ 464, 402, 1253, 134, 134, 134, 134, 133, 133, 132, + /* 110 */ 132, 132, 131, 128, 455, 455, 1054, 1054, 1068, 1071, + /* 120 */ 1259, 1, 1, 586, 2, 1263, 585, 1178, 1263, 1178, + /* 130 */ 321, 417, 155, 321, 1588, 155, 383, 112, 485, 1345, + /* 140 */ 460, 299, 1345, 134, 134, 134, 134, 133, 133, 132, + /* 150 */ 132, 132, 131, 128, 455, 137, 138, 91, 502, 1232, + /* 160 */ 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, + /* 170 */ 136, 1208, 866, 1285, 288, 288, 283, 288, 288, 527, + /* 180 */ 527, 1254, 139, 582, 7, 582, 1349, 577, 1173, 566, + /* 190 */ 577, 1058, 566, 136, 136, 136, 136, 129, 577, 551, + /* 200 */ 566, 1173, 245, 1545, 1173, 245, 133, 133, 132, 132, + /* 210 */ 132, 131, 128, 455, 302, 134, 134, 134, 134, 133, + /* 220 */ 133, 132, 132, 132, 131, 128, 455, 1579, 1208, 1209, + /* 230 */ 1208, 7, 474, 554, 459, 417, 554, 459, 130, 127, /* 240 */ 234, 134, 134, 134, 134, 133, 133, 132, 132, 132, - /* 250 */ 131, 128, 451, 136, 136, 136, 136, 538, 483, 137, - /* 260 */ 138, 91, 1019, 1228, 1228, 1063, 1066, 1053, 1053, 135, - /* 270 */ 135, 136, 136, 136, 136, 1085, 576, 1204, 132, 132, - /* 280 */ 132, 131, 128, 451, 93, 214, 134, 134, 134, 134, - /* 290 */ 133, 133, 132, 132, 132, 131, 128, 451, 401, 19, + /* 250 */ 131, 128, 455, 136, 136, 136, 136, 542, 487, 137, + /* 260 */ 138, 91, 1023, 1232, 1232, 1067, 1070, 1057, 1057, 135, + /* 270 */ 135, 136, 136, 136, 136, 1089, 580, 1208, 132, 132, + /* 280 */ 132, 131, 128, 455, 93, 214, 134, 134, 134, 134, + /* 290 */ 133, 133, 132, 132, 132, 131, 128, 455, 405, 19, /* 300 */ 19, 134, 134, 134, 134, 133, 133, 132, 132, 132, - /* 310 */ 131, 128, 451, 1498, 426, 267, 344, 467, 332, 134, + /* 310 */ 131, 128, 455, 1502, 430, 267, 348, 471, 334, 134, /* 320 */ 134, 134, 134, 133, 133, 132, 132, 132, 131, 128, - /* 330 */ 451, 1281, 576, 6, 1204, 1205, 1204, 257, 576, 413, - /* 340 */ 511, 508, 507, 1279, 94, 1019, 464, 1204, 551, 551, - /* 350 */ 506, 1224, 1571, 44, 38, 51, 51, 411, 576, 413, - /* 360 */ 45, 51, 51, 137, 138, 91, 530, 1228, 1228, 1063, - /* 370 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 398, - /* 380 */ 1148, 82, 82, 137, 138, 91, 39, 1228, 1228, 1063, - /* 390 */ 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, 344, - /* 400 */ 44, 288, 288, 375, 1204, 1205, 1204, 209, 1204, 1224, - /* 410 */ 320, 567, 471, 576, 573, 576, 562, 576, 316, 264, + /* 330 */ 455, 1285, 580, 6, 1208, 1209, 1208, 257, 580, 417, + /* 340 */ 515, 512, 511, 1283, 94, 1023, 468, 1208, 555, 555, + /* 350 */ 510, 1228, 1575, 44, 38, 51, 51, 415, 580, 417, + /* 360 */ 45, 51, 51, 137, 138, 91, 534, 1232, 1232, 1067, + /* 370 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 402, + /* 380 */ 1152, 82, 82, 137, 138, 91, 39, 1232, 1232, 1067, + /* 390 */ 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, 348, + /* 400 */ 44, 288, 288, 379, 1208, 1209, 1208, 209, 1208, 1228, + /* 410 */ 320, 571, 475, 580, 577, 580, 566, 580, 316, 264, /* 420 */ 231, 46, 160, 134, 134, 134, 134, 133, 133, 132, - /* 430 */ 132, 132, 131, 128, 451, 303, 82, 82, 82, 82, - /* 440 */ 82, 82, 442, 134, 134, 134, 134, 133, 133, 132, - /* 450 */ 132, 132, 131, 128, 451, 1582, 544, 320, 567, 1250, - /* 460 */ 874, 1582, 380, 382, 413, 1204, 1205, 1204, 360, 182, - /* 470 */ 288, 288, 1576, 557, 1339, 557, 7, 557, 1277, 472, - /* 480 */ 346, 526, 531, 573, 556, 562, 439, 1511, 137, 138, - /* 490 */ 91, 219, 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, - /* 500 */ 136, 136, 136, 136, 465, 1511, 1513, 532, 413, 288, - /* 510 */ 288, 423, 512, 288, 288, 411, 288, 288, 874, 130, - /* 520 */ 127, 234, 573, 1107, 562, 1204, 573, 1107, 562, 573, - /* 530 */ 560, 562, 137, 138, 91, 1293, 1228, 1228, 1063, 1066, - /* 540 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 134, 134, - /* 550 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 451, - /* 560 */ 493, 503, 1292, 1204, 257, 288, 288, 511, 508, 507, - /* 570 */ 1204, 1628, 1169, 123, 568, 275, 4, 506, 573, 1511, - /* 580 */ 562, 331, 1204, 1205, 1204, 1169, 548, 548, 1169, 261, - /* 590 */ 571, 7, 134, 134, 134, 134, 133, 133, 132, 132, - /* 600 */ 132, 131, 128, 451, 108, 533, 130, 127, 234, 1204, - /* 610 */ 448, 447, 413, 1451, 452, 983, 886, 96, 1598, 1233, - /* 620 */ 1204, 1205, 1204, 984, 1235, 1450, 565, 1204, 1205, 1204, - /* 630 */ 229, 522, 1234, 534, 1333, 1333, 137, 138, 91, 1449, - /* 640 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 650 */ 136, 136, 373, 1595, 971, 1040, 413, 1236, 418, 1236, - /* 660 */ 879, 121, 121, 948, 373, 1595, 1204, 1205, 1204, 122, - /* 670 */ 1204, 452, 577, 452, 363, 417, 1028, 882, 373, 1595, - /* 680 */ 137, 138, 91, 462, 1228, 1228, 1063, 1066, 1053, 1053, + /* 430 */ 132, 132, 131, 128, 455, 303, 82, 82, 82, 82, + /* 440 */ 82, 82, 446, 134, 134, 134, 134, 133, 133, 132, + /* 450 */ 132, 132, 131, 128, 455, 1586, 548, 320, 571, 1254, + /* 460 */ 878, 1586, 384, 386, 417, 1208, 1209, 1208, 364, 182, + /* 470 */ 288, 288, 1580, 561, 1343, 561, 7, 561, 1281, 476, + /* 480 */ 350, 530, 535, 577, 560, 566, 443, 1515, 137, 138, + /* 490 */ 91, 219, 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, + /* 500 */ 136, 136, 136, 136, 469, 1515, 1517, 536, 417, 288, + /* 510 */ 288, 427, 516, 288, 288, 415, 288, 288, 878, 130, + /* 520 */ 127, 234, 577, 1111, 566, 1208, 577, 1111, 566, 577, + /* 530 */ 564, 566, 137, 138, 91, 1297, 1232, 1232, 1067, 1070, + /* 540 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 134, 134, + /* 550 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 455, + /* 560 */ 497, 507, 1296, 1208, 257, 288, 288, 515, 512, 511, + /* 570 */ 1208, 1632, 1173, 123, 572, 275, 4, 510, 577, 1515, + /* 580 */ 566, 331, 1208, 1209, 1208, 1173, 552, 552, 1173, 261, + /* 590 */ 575, 7, 134, 134, 134, 134, 133, 133, 132, 132, + /* 600 */ 132, 131, 128, 455, 108, 537, 130, 127, 234, 1208, + /* 610 */ 452, 451, 417, 1455, 456, 987, 890, 96, 1602, 1237, + /* 620 */ 1208, 1209, 1208, 988, 1239, 1454, 569, 1208, 1209, 1208, + /* 630 */ 229, 526, 1238, 538, 1337, 1337, 137, 138, 91, 1453, + /* 640 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 650 */ 136, 136, 377, 1599, 975, 1044, 417, 1240, 422, 1240, + /* 660 */ 883, 121, 121, 952, 377, 1599, 1208, 1209, 1208, 122, + /* 670 */ 1208, 456, 581, 456, 367, 421, 1032, 886, 377, 1599, + /* 680 */ 137, 138, 91, 466, 1232, 1232, 1067, 1070, 1057, 1057, /* 690 */ 135, 135, 136, 136, 136, 136, 134, 134, 134, 134, - /* 700 */ 133, 133, 132, 132, 132, 131, 128, 451, 1028, 1028, - /* 710 */ 1030, 1031, 35, 570, 570, 570, 197, 423, 1040, 198, - /* 720 */ 1204, 123, 568, 1204, 4, 320, 567, 1204, 1205, 1204, - /* 730 */ 40, 388, 576, 384, 882, 1029, 423, 1188, 571, 1028, + /* 700 */ 133, 133, 132, 132, 132, 131, 128, 455, 1032, 1032, + /* 710 */ 1034, 1035, 35, 574, 574, 574, 197, 427, 1044, 198, + /* 720 */ 1208, 123, 572, 1208, 4, 320, 571, 1208, 1209, 1208, + /* 730 */ 40, 392, 580, 388, 886, 1033, 427, 1192, 575, 1032, /* 740 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 750 */ 128, 451, 529, 1568, 1204, 19, 19, 1204, 575, 492, - /* 760 */ 413, 157, 452, 489, 1187, 1331, 1331, 5, 1204, 949, - /* 770 */ 431, 1028, 1028, 1030, 565, 22, 22, 1204, 1205, 1204, - /* 780 */ 1204, 1205, 1204, 477, 137, 138, 91, 212, 1228, 1228, - /* 790 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 800 */ 1188, 48, 111, 1040, 413, 1204, 213, 970, 1041, 121, - /* 810 */ 121, 1204, 1205, 1204, 1204, 1205, 1204, 122, 221, 452, - /* 820 */ 577, 452, 44, 487, 1028, 1204, 1205, 1204, 137, 138, - /* 830 */ 91, 378, 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, + /* 750 */ 128, 455, 533, 1572, 1208, 19, 19, 1208, 579, 496, + /* 760 */ 417, 157, 456, 493, 1191, 1335, 1335, 5, 1208, 953, + /* 770 */ 435, 1032, 1032, 1034, 569, 22, 22, 1208, 1209, 1208, + /* 780 */ 1208, 1209, 1208, 481, 137, 138, 91, 212, 1232, 1232, + /* 790 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 800 */ 1192, 48, 111, 1044, 417, 1208, 213, 974, 1045, 121, + /* 810 */ 121, 1208, 1209, 1208, 1208, 1209, 1208, 122, 221, 456, + /* 820 */ 581, 456, 44, 491, 1032, 1208, 1209, 1208, 137, 138, + /* 830 */ 91, 382, 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, /* 840 */ 136, 136, 136, 136, 134, 134, 134, 134, 133, 133, - /* 850 */ 132, 132, 132, 131, 128, 451, 1028, 1028, 1030, 1031, - /* 860 */ 35, 461, 1204, 1205, 1204, 1569, 1040, 377, 214, 1149, - /* 870 */ 1657, 535, 1657, 437, 902, 320, 567, 1568, 364, 320, - /* 880 */ 567, 412, 329, 1029, 519, 1188, 3, 1028, 134, 134, - /* 890 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 451, - /* 900 */ 1659, 399, 1169, 307, 893, 307, 515, 576, 413, 214, - /* 910 */ 498, 944, 1024, 540, 903, 1169, 943, 392, 1169, 1028, - /* 920 */ 1028, 1030, 406, 298, 1204, 50, 1149, 1658, 413, 1658, - /* 930 */ 145, 145, 137, 138, 91, 293, 1228, 1228, 1063, 1066, - /* 940 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 1188, 1147, - /* 950 */ 514, 1568, 137, 138, 91, 1505, 1228, 1228, 1063, 1066, - /* 960 */ 1053, 1053, 135, 135, 136, 136, 136, 136, 434, 323, - /* 970 */ 435, 539, 111, 1506, 274, 291, 372, 517, 367, 516, - /* 980 */ 262, 1204, 1205, 1204, 1574, 481, 363, 576, 7, 1569, - /* 990 */ 1568, 377, 134, 134, 134, 134, 133, 133, 132, 132, - /* 1000 */ 132, 131, 128, 451, 1568, 576, 1147, 576, 232, 576, + /* 850 */ 132, 132, 132, 131, 128, 455, 1032, 1032, 1034, 1035, + /* 860 */ 35, 465, 1208, 1209, 1208, 1573, 1044, 381, 214, 1153, + /* 870 */ 1661, 539, 1661, 441, 906, 320, 571, 1572, 368, 320, + /* 880 */ 571, 416, 329, 1033, 523, 1192, 3, 1032, 134, 134, + /* 890 */ 134, 134, 133, 133, 132, 132, 132, 131, 128, 455, + /* 900 */ 1663, 403, 1173, 307, 897, 307, 519, 580, 417, 214, + /* 910 */ 502, 948, 1028, 544, 907, 1173, 947, 396, 1173, 1032, + /* 920 */ 1032, 1034, 410, 298, 1208, 50, 1153, 1662, 417, 1662, + /* 930 */ 145, 145, 137, 138, 91, 293, 1232, 1232, 1067, 1070, + /* 940 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 1192, 1151, + /* 950 */ 518, 1572, 137, 138, 91, 1509, 1232, 1232, 1067, 1070, + /* 960 */ 1057, 1057, 135, 135, 136, 136, 136, 136, 438, 323, + /* 970 */ 439, 543, 111, 1510, 274, 291, 376, 521, 371, 520, + /* 980 */ 262, 1208, 1209, 1208, 1578, 485, 367, 580, 7, 1573, + /* 990 */ 1572, 381, 134, 134, 134, 134, 133, 133, 132, 132, + /* 1000 */ 132, 131, 128, 455, 1572, 580, 1151, 580, 232, 580, /* 1010 */ 19, 19, 134, 134, 134, 134, 133, 133, 132, 132, - /* 1020 */ 132, 131, 128, 451, 1169, 433, 576, 1207, 19, 19, - /* 1030 */ 19, 19, 19, 19, 1627, 576, 911, 1169, 47, 120, - /* 1040 */ 1169, 117, 413, 306, 498, 438, 1125, 206, 336, 19, - /* 1050 */ 19, 1435, 49, 449, 449, 449, 1368, 315, 81, 81, - /* 1060 */ 576, 304, 413, 1570, 207, 377, 137, 138, 91, 115, - /* 1070 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 1080 */ 136, 136, 576, 82, 82, 1207, 137, 138, 91, 1340, - /* 1090 */ 1228, 1228, 1063, 1066, 1053, 1053, 135, 135, 136, 136, - /* 1100 */ 136, 136, 1569, 386, 377, 82, 82, 463, 1126, 1552, - /* 1110 */ 333, 463, 335, 131, 128, 451, 1569, 161, 377, 16, - /* 1120 */ 317, 387, 428, 1127, 448, 447, 134, 134, 134, 134, - /* 1130 */ 133, 133, 132, 132, 132, 131, 128, 451, 1128, 576, - /* 1140 */ 1105, 10, 445, 267, 576, 1554, 134, 134, 134, 134, - /* 1150 */ 133, 133, 132, 132, 132, 131, 128, 451, 532, 576, - /* 1160 */ 922, 576, 19, 19, 576, 1573, 576, 147, 147, 7, - /* 1170 */ 923, 1236, 498, 1236, 576, 487, 413, 552, 285, 1224, - /* 1180 */ 969, 215, 82, 82, 66, 66, 1435, 67, 67, 21, - /* 1190 */ 21, 1110, 1110, 495, 334, 297, 413, 53, 53, 297, - /* 1200 */ 137, 138, 91, 119, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1210 */ 135, 135, 136, 136, 136, 136, 413, 1336, 1311, 446, - /* 1220 */ 137, 138, 91, 227, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1230 */ 135, 135, 136, 136, 136, 136, 574, 1224, 936, 936, - /* 1240 */ 137, 126, 91, 141, 1228, 1228, 1063, 1066, 1053, 1053, - /* 1250 */ 135, 135, 136, 136, 136, 136, 533, 429, 472, 346, + /* 1020 */ 132, 131, 128, 455, 1173, 437, 580, 1211, 19, 19, + /* 1030 */ 19, 19, 19, 19, 1631, 580, 915, 1173, 47, 120, + /* 1040 */ 1173, 117, 417, 306, 502, 442, 1129, 206, 340, 19, + /* 1050 */ 19, 1439, 49, 453, 453, 453, 1372, 315, 81, 81, + /* 1060 */ 580, 304, 417, 1574, 207, 381, 137, 138, 91, 115, + /* 1070 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 1080 */ 136, 136, 580, 82, 82, 1211, 137, 138, 91, 1344, + /* 1090 */ 1232, 1232, 1067, 1070, 1057, 1057, 135, 135, 136, 136, + /* 1100 */ 136, 136, 1573, 390, 381, 82, 82, 467, 1130, 1556, + /* 1110 */ 337, 467, 339, 131, 128, 455, 1573, 161, 381, 16, + /* 1120 */ 317, 391, 432, 1131, 452, 451, 134, 134, 134, 134, + /* 1130 */ 133, 133, 132, 132, 132, 131, 128, 455, 1132, 580, + /* 1140 */ 1109, 10, 449, 267, 580, 1558, 134, 134, 134, 134, + /* 1150 */ 133, 133, 132, 132, 132, 131, 128, 455, 536, 580, + /* 1160 */ 926, 580, 19, 19, 580, 1577, 580, 147, 147, 7, + /* 1170 */ 927, 1240, 502, 1240, 580, 491, 417, 556, 285, 1228, + /* 1180 */ 973, 215, 82, 82, 66, 66, 1439, 67, 67, 21, + /* 1190 */ 21, 1114, 1114, 499, 338, 297, 417, 53, 53, 297, + /* 1200 */ 137, 138, 91, 119, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1210 */ 135, 135, 136, 136, 136, 136, 417, 1340, 1315, 450, + /* 1220 */ 137, 138, 91, 227, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1230 */ 135, 135, 136, 136, 136, 136, 578, 1228, 940, 940, + /* 1240 */ 137, 126, 91, 141, 1232, 1232, 1067, 1070, 1057, 1057, + /* 1250 */ 135, 135, 136, 136, 136, 136, 537, 433, 476, 350, /* 1260 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1270 */ 128, 451, 576, 457, 233, 343, 1435, 403, 498, 1550, + /* 1270 */ 128, 455, 580, 461, 233, 347, 1439, 407, 502, 1554, /* 1280 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1290 */ 128, 451, 576, 324, 576, 82, 82, 487, 576, 969, + /* 1290 */ 128, 455, 580, 324, 580, 82, 82, 491, 580, 973, /* 1300 */ 134, 134, 134, 134, 133, 133, 132, 132, 132, 131, - /* 1310 */ 128, 451, 288, 288, 546, 68, 68, 54, 54, 553, - /* 1320 */ 413, 69, 69, 351, 6, 573, 944, 562, 410, 409, - /* 1330 */ 1435, 943, 450, 545, 260, 259, 258, 576, 158, 576, - /* 1340 */ 413, 222, 1180, 479, 969, 138, 91, 430, 1228, 1228, - /* 1350 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 1360 */ 70, 70, 71, 71, 576, 1126, 91, 576, 1228, 1228, - /* 1370 */ 1063, 1066, 1053, 1053, 135, 135, 136, 136, 136, 136, - /* 1380 */ 1127, 166, 850, 851, 852, 1282, 419, 72, 72, 108, - /* 1390 */ 73, 73, 1310, 358, 1180, 1128, 576, 305, 576, 123, - /* 1400 */ 568, 494, 4, 488, 134, 134, 134, 134, 133, 133, - /* 1410 */ 132, 132, 132, 131, 128, 451, 571, 564, 534, 55, - /* 1420 */ 55, 56, 56, 576, 134, 134, 134, 134, 133, 133, - /* 1430 */ 132, 132, 132, 131, 128, 451, 576, 1104, 233, 1104, - /* 1440 */ 452, 1602, 582, 2, 1259, 576, 57, 57, 576, 321, - /* 1450 */ 576, 155, 565, 1435, 485, 353, 576, 356, 1341, 59, - /* 1460 */ 59, 576, 44, 969, 569, 419, 576, 238, 60, 60, - /* 1470 */ 261, 74, 74, 75, 75, 287, 231, 576, 1366, 76, - /* 1480 */ 76, 1040, 420, 184, 20, 20, 576, 121, 121, 77, - /* 1490 */ 77, 97, 218, 288, 288, 122, 125, 452, 577, 452, - /* 1500 */ 143, 143, 1028, 576, 520, 576, 573, 576, 562, 144, - /* 1510 */ 144, 474, 227, 1244, 478, 123, 568, 576, 4, 320, - /* 1520 */ 567, 245, 411, 576, 443, 411, 78, 78, 62, 62, - /* 1530 */ 79, 79, 571, 319, 1028, 1028, 1030, 1031, 35, 418, - /* 1540 */ 63, 63, 576, 290, 411, 9, 80, 80, 1144, 576, - /* 1550 */ 400, 576, 486, 455, 576, 1223, 452, 576, 325, 342, - /* 1560 */ 576, 111, 576, 1188, 242, 64, 64, 473, 565, 576, - /* 1570 */ 23, 576, 170, 170, 171, 171, 576, 87, 87, 328, - /* 1580 */ 65, 65, 542, 83, 83, 146, 146, 541, 123, 568, - /* 1590 */ 341, 4, 84, 84, 168, 168, 576, 1040, 576, 148, - /* 1600 */ 148, 576, 1380, 121, 121, 571, 1021, 576, 266, 576, - /* 1610 */ 424, 122, 576, 452, 577, 452, 576, 553, 1028, 142, - /* 1620 */ 142, 169, 169, 576, 162, 162, 528, 889, 371, 452, - /* 1630 */ 152, 152, 151, 151, 1379, 149, 149, 109, 370, 150, - /* 1640 */ 150, 565, 576, 480, 576, 266, 86, 86, 576, 1092, - /* 1650 */ 1028, 1028, 1030, 1031, 35, 542, 482, 576, 266, 466, - /* 1660 */ 543, 123, 568, 1616, 4, 88, 88, 85, 85, 475, - /* 1670 */ 1040, 52, 52, 222, 901, 900, 121, 121, 571, 1188, - /* 1680 */ 58, 58, 244, 1032, 122, 889, 452, 577, 452, 908, - /* 1690 */ 909, 1028, 300, 347, 504, 111, 263, 361, 165, 111, - /* 1700 */ 111, 1088, 452, 263, 974, 1153, 266, 1092, 986, 987, - /* 1710 */ 942, 939, 125, 125, 565, 1103, 872, 1103, 159, 941, - /* 1720 */ 1309, 125, 1557, 1028, 1028, 1030, 1031, 35, 542, 337, - /* 1730 */ 1530, 205, 1529, 541, 499, 1589, 490, 348, 1376, 352, - /* 1740 */ 355, 1032, 357, 1040, 359, 1324, 1308, 366, 563, 121, - /* 1750 */ 121, 376, 1188, 1389, 1434, 1362, 280, 122, 1374, 452, - /* 1760 */ 577, 452, 167, 1439, 1028, 1289, 1280, 1268, 1267, 1269, - /* 1770 */ 1609, 1359, 312, 313, 314, 397, 12, 237, 224, 1421, - /* 1780 */ 295, 1416, 1409, 1426, 339, 484, 340, 509, 1371, 1612, - /* 1790 */ 1372, 1425, 1244, 404, 301, 228, 1028, 1028, 1030, 1031, - /* 1800 */ 35, 1601, 1192, 454, 345, 1307, 292, 369, 1502, 1501, - /* 1810 */ 270, 396, 396, 395, 277, 393, 1370, 1369, 859, 1549, - /* 1820 */ 186, 123, 568, 235, 4, 1188, 391, 210, 211, 223, - /* 1830 */ 1547, 239, 1241, 327, 422, 96, 220, 195, 571, 180, - /* 1840 */ 188, 326, 468, 469, 190, 191, 502, 192, 193, 566, - /* 1850 */ 247, 109, 1430, 491, 199, 251, 102, 281, 402, 476, - /* 1860 */ 405, 1496, 452, 497, 253, 1422, 13, 1428, 14, 1427, - /* 1870 */ 203, 1507, 241, 500, 565, 354, 407, 92, 95, 1270, - /* 1880 */ 175, 254, 518, 43, 1327, 255, 1326, 1325, 436, 1518, - /* 1890 */ 350, 1318, 104, 229, 893, 1626, 440, 441, 1625, 408, - /* 1900 */ 240, 1296, 268, 1040, 310, 269, 1297, 527, 444, 121, - /* 1910 */ 121, 368, 1295, 1594, 1624, 311, 1394, 122, 1317, 452, - /* 1920 */ 577, 452, 374, 1580, 1028, 1393, 140, 553, 11, 90, - /* 1930 */ 568, 385, 4, 116, 318, 414, 1579, 110, 1483, 537, - /* 1940 */ 320, 567, 1350, 555, 42, 579, 571, 1349, 1198, 383, - /* 1950 */ 276, 390, 216, 389, 278, 279, 1028, 1028, 1030, 1031, - /* 1960 */ 35, 172, 580, 1265, 458, 1260, 415, 416, 185, 156, - /* 1970 */ 452, 1534, 1535, 173, 1533, 1532, 89, 308, 225, 226, - /* 1980 */ 846, 174, 565, 453, 217, 1188, 322, 236, 1102, 154, - /* 1990 */ 1100, 330, 187, 176, 1223, 243, 189, 925, 338, 246, - /* 2000 */ 1116, 194, 177, 425, 178, 427, 98, 196, 99, 100, - /* 2010 */ 101, 1040, 179, 1119, 1115, 248, 249, 121, 121, 163, - /* 2020 */ 24, 250, 349, 1238, 496, 122, 1108, 452, 577, 452, - /* 2030 */ 1192, 454, 1028, 266, 292, 200, 252, 201, 861, 396, - /* 2040 */ 396, 395, 277, 393, 15, 501, 859, 370, 292, 256, - /* 2050 */ 202, 554, 505, 396, 396, 395, 277, 393, 103, 239, - /* 2060 */ 859, 327, 25, 26, 1028, 1028, 1030, 1031, 35, 326, - /* 2070 */ 362, 510, 891, 239, 365, 327, 513, 904, 105, 309, - /* 2080 */ 164, 181, 27, 326, 106, 521, 107, 1185, 1069, 1155, - /* 2090 */ 17, 1154, 230, 1188, 284, 286, 265, 204, 125, 1171, - /* 2100 */ 241, 28, 978, 972, 29, 41, 1175, 1179, 175, 1173, - /* 2110 */ 30, 43, 31, 8, 241, 1178, 32, 1160, 208, 549, - /* 2120 */ 33, 111, 175, 1083, 1070, 43, 1068, 1072, 240, 113, - /* 2130 */ 114, 34, 561, 118, 1124, 271, 1073, 36, 18, 572, - /* 2140 */ 1033, 873, 240, 124, 37, 935, 272, 273, 1617, 183, - /* 2150 */ 153, 394, 1194, 1193, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2160 */ 1256, 1256, 1256, 414, 1256, 1256, 1256, 1256, 320, 567, - /* 2170 */ 1256, 1256, 1256, 1256, 1256, 1256, 1256, 414, 1256, 1256, - /* 2180 */ 1256, 1256, 320, 567, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2190 */ 1256, 1256, 458, 1256, 1256, 1256, 1256, 1256, 1256, 1256, - /* 2200 */ 1256, 1256, 1256, 1256, 1256, 1256, 458, + /* 1310 */ 128, 455, 288, 288, 550, 68, 68, 54, 54, 557, + /* 1320 */ 417, 69, 69, 355, 6, 577, 948, 566, 414, 413, + /* 1330 */ 1439, 947, 454, 549, 260, 259, 258, 580, 158, 580, + /* 1340 */ 417, 222, 1184, 483, 973, 138, 91, 434, 1232, 1232, + /* 1350 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 1360 */ 70, 70, 71, 71, 580, 1130, 91, 580, 1232, 1232, + /* 1370 */ 1067, 1070, 1057, 1057, 135, 135, 136, 136, 136, 136, + /* 1380 */ 1131, 166, 854, 855, 856, 1286, 423, 72, 72, 108, + /* 1390 */ 73, 73, 1314, 362, 1184, 1132, 580, 305, 580, 123, + /* 1400 */ 572, 498, 4, 492, 134, 134, 134, 134, 133, 133, + /* 1410 */ 132, 132, 132, 131, 128, 455, 575, 568, 538, 55, + /* 1420 */ 55, 56, 56, 580, 134, 134, 134, 134, 133, 133, + /* 1430 */ 132, 132, 132, 131, 128, 455, 580, 1108, 233, 1108, + /* 1440 */ 456, 1606, 586, 2, 1263, 580, 57, 57, 580, 321, + /* 1450 */ 580, 155, 569, 1439, 489, 357, 580, 360, 1345, 59, + /* 1460 */ 59, 580, 44, 973, 573, 423, 580, 238, 60, 60, + /* 1470 */ 261, 74, 74, 75, 75, 287, 231, 580, 1370, 76, + /* 1480 */ 76, 1044, 424, 184, 20, 20, 580, 121, 121, 77, + /* 1490 */ 77, 97, 218, 288, 288, 122, 125, 456, 581, 456, + /* 1500 */ 143, 143, 1032, 580, 524, 580, 577, 580, 566, 144, + /* 1510 */ 144, 478, 227, 1248, 482, 123, 572, 580, 4, 320, + /* 1520 */ 571, 245, 415, 580, 447, 415, 78, 78, 62, 62, + /* 1530 */ 79, 79, 575, 319, 1032, 1032, 1034, 1035, 35, 422, + /* 1540 */ 63, 63, 580, 290, 415, 9, 80, 80, 1148, 580, + /* 1550 */ 404, 580, 490, 459, 580, 1227, 456, 580, 325, 346, + /* 1560 */ 580, 111, 580, 1192, 242, 64, 64, 477, 569, 580, + /* 1570 */ 23, 580, 170, 170, 171, 171, 580, 87, 87, 328, + /* 1580 */ 65, 65, 546, 83, 83, 146, 146, 545, 123, 572, + /* 1590 */ 345, 4, 84, 84, 168, 168, 580, 1044, 580, 148, + /* 1600 */ 148, 580, 1384, 121, 121, 575, 1025, 580, 266, 580, + /* 1610 */ 428, 122, 580, 456, 581, 456, 580, 557, 1032, 142, + /* 1620 */ 142, 169, 169, 580, 162, 162, 532, 893, 375, 456, + /* 1630 */ 152, 152, 151, 151, 1383, 149, 149, 109, 374, 150, + /* 1640 */ 150, 569, 580, 484, 580, 266, 86, 86, 580, 1096, + /* 1650 */ 1032, 1032, 1034, 1035, 35, 546, 486, 580, 266, 470, + /* 1660 */ 547, 123, 572, 1620, 4, 88, 88, 85, 85, 479, + /* 1670 */ 1044, 52, 52, 222, 905, 904, 121, 121, 575, 1192, + /* 1680 */ 58, 58, 244, 1036, 122, 893, 456, 581, 456, 912, + /* 1690 */ 913, 1032, 300, 351, 508, 111, 263, 365, 165, 111, + /* 1700 */ 111, 1092, 456, 263, 978, 1157, 266, 1096, 990, 991, + /* 1710 */ 946, 943, 125, 125, 569, 1107, 876, 1107, 159, 945, + /* 1720 */ 1313, 125, 1561, 1032, 1032, 1034, 1035, 35, 546, 341, + /* 1730 */ 1534, 205, 1533, 545, 503, 1593, 494, 352, 1380, 356, + /* 1740 */ 359, 1036, 361, 1044, 363, 1328, 1312, 370, 567, 121, + /* 1750 */ 121, 380, 1192, 1393, 1438, 1366, 280, 122, 1378, 456, + /* 1760 */ 581, 456, 167, 1443, 1032, 1293, 1284, 1272, 1271, 1273, + /* 1770 */ 1613, 1363, 312, 313, 314, 401, 12, 237, 224, 1425, + /* 1780 */ 295, 333, 336, 1430, 343, 488, 344, 513, 1375, 1616, + /* 1790 */ 1376, 1429, 1248, 408, 301, 228, 1032, 1032, 1034, 1035, + /* 1800 */ 35, 1605, 1196, 458, 349, 1311, 292, 373, 1506, 1505, + /* 1810 */ 270, 400, 400, 399, 277, 397, 1374, 1373, 863, 1553, + /* 1820 */ 186, 123, 572, 235, 4, 1192, 395, 210, 211, 223, + /* 1830 */ 1551, 239, 1245, 327, 426, 96, 220, 195, 575, 140, + /* 1840 */ 557, 326, 180, 472, 1420, 332, 188, 1413, 335, 570, + /* 1850 */ 190, 191, 192, 193, 473, 506, 247, 109, 1434, 495, + /* 1860 */ 251, 199, 456, 406, 480, 1426, 13, 409, 102, 14, + /* 1870 */ 501, 1511, 241, 1432, 569, 1431, 203, 92, 95, 1500, + /* 1880 */ 175, 281, 358, 43, 504, 253, 411, 254, 522, 1331, + /* 1890 */ 104, 1274, 1522, 354, 440, 1322, 255, 1330, 1630, 1629, + /* 1900 */ 240, 897, 229, 1044, 444, 1321, 445, 448, 269, 121, + /* 1910 */ 121, 1329, 531, 268, 310, 412, 1398, 122, 1301, 456, + /* 1920 */ 581, 456, 1300, 372, 1032, 1299, 1628, 378, 311, 90, + /* 1930 */ 572, 11, 4, 1397, 1598, 418, 1487, 389, 116, 110, + /* 1940 */ 320, 571, 1584, 559, 318, 541, 575, 1583, 42, 1354, + /* 1950 */ 387, 583, 216, 1353, 1202, 276, 1032, 1032, 1034, 1035, + /* 1960 */ 35, 393, 394, 278, 462, 279, 419, 584, 1538, 1269, + /* 1970 */ 456, 1264, 172, 420, 173, 1539, 1537, 1536, 156, 308, + /* 1980 */ 225, 89, 569, 850, 226, 1192, 457, 174, 217, 236, + /* 1990 */ 322, 154, 1106, 1104, 187, 330, 176, 1227, 929, 189, + /* 2000 */ 243, 342, 246, 1120, 194, 177, 178, 429, 431, 98, + /* 2010 */ 196, 1044, 99, 185, 100, 101, 179, 121, 121, 1123, + /* 2020 */ 248, 249, 1119, 163, 250, 122, 24, 456, 581, 456, + /* 2030 */ 1196, 458, 1032, 353, 292, 266, 1112, 200, 1242, 400, + /* 2040 */ 400, 399, 277, 397, 500, 252, 863, 201, 292, 15, + /* 2050 */ 865, 558, 505, 400, 400, 399, 277, 397, 374, 239, + /* 2060 */ 863, 327, 256, 202, 1032, 1032, 1034, 1035, 35, 326, + /* 2070 */ 103, 25, 26, 239, 509, 327, 366, 514, 369, 895, + /* 2080 */ 908, 517, 105, 326, 309, 164, 525, 106, 181, 27, + /* 2090 */ 1189, 1073, 17, 1192, 107, 1159, 1158, 284, 230, 286, + /* 2100 */ 241, 204, 125, 1175, 982, 265, 1182, 976, 175, 28, + /* 2110 */ 1179, 43, 29, 1177, 241, 30, 31, 8, 1183, 32, + /* 2120 */ 1164, 41, 175, 208, 553, 43, 111, 33, 240, 1087, + /* 2130 */ 1074, 113, 114, 1072, 1076, 34, 1077, 565, 1128, 118, + /* 2140 */ 271, 36, 240, 18, 939, 1037, 877, 272, 124, 37, + /* 2150 */ 398, 1198, 1197, 576, 183, 273, 153, 1621, 1260, 1260, + /* 2160 */ 1260, 1260, 1260, 418, 1260, 1260, 1260, 1260, 320, 571, + /* 2170 */ 1260, 1260, 1260, 1260, 1260, 1260, 1260, 418, 1260, 1260, + /* 2180 */ 1260, 1260, 320, 571, 1260, 1260, 1260, 1260, 1260, 1260, + /* 2190 */ 1260, 1260, 462, 1260, 1260, 1260, 1260, 1260, 1260, 1260, + /* 2200 */ 1260, 1260, 1260, 1260, 1260, 1260, 462, }; static const YYCODETYPE yy_lookahead[] = { /* 0 */ 277, 278, 279, 241, 242, 225, 195, 227, 195, 241, @@ -176315,39 +176318,39 @@ static const YYCODETYPE yy_lookahead[] = { /* 1800 */ 158, 0, 1, 2, 247, 227, 5, 221, 221, 221, /* 1810 */ 142, 10, 11, 12, 13, 14, 262, 262, 17, 202, /* 1820 */ 300, 19, 20, 300, 22, 183, 247, 251, 251, 245, - /* 1830 */ 202, 30, 38, 32, 202, 152, 151, 22, 36, 43, - /* 1840 */ 236, 40, 18, 202, 239, 239, 18, 239, 239, 283, - /* 1850 */ 201, 150, 236, 202, 236, 201, 159, 202, 248, 248, - /* 1860 */ 248, 248, 60, 63, 201, 275, 273, 275, 273, 275, - /* 1870 */ 22, 286, 71, 223, 72, 202, 223, 297, 297, 202, - /* 1880 */ 79, 201, 116, 82, 220, 201, 220, 220, 65, 293, - /* 1890 */ 292, 229, 22, 166, 127, 226, 24, 114, 226, 223, - /* 1900 */ 99, 222, 202, 101, 285, 92, 220, 308, 83, 107, - /* 1910 */ 108, 220, 220, 316, 220, 285, 268, 115, 229, 117, - /* 1920 */ 118, 119, 223, 321, 122, 268, 149, 146, 22, 19, - /* 1930 */ 20, 202, 22, 159, 282, 134, 321, 148, 280, 147, - /* 1940 */ 139, 140, 252, 141, 25, 204, 36, 252, 13, 251, - /* 1950 */ 196, 248, 250, 249, 196, 6, 154, 155, 156, 157, - /* 1960 */ 158, 209, 194, 194, 163, 194, 306, 306, 303, 224, - /* 1970 */ 60, 215, 215, 209, 215, 215, 215, 224, 216, 216, - /* 1980 */ 4, 209, 72, 3, 22, 183, 164, 15, 23, 16, - /* 1990 */ 23, 140, 152, 131, 25, 24, 143, 20, 16, 145, - /* 2000 */ 1, 143, 131, 62, 131, 37, 54, 152, 54, 54, - /* 2010 */ 54, 101, 131, 117, 1, 34, 142, 107, 108, 5, - /* 2020 */ 22, 116, 162, 76, 41, 115, 69, 117, 118, 119, - /* 2030 */ 1, 2, 122, 25, 5, 69, 142, 116, 20, 10, - /* 2040 */ 11, 12, 13, 14, 24, 19, 17, 132, 5, 126, - /* 2050 */ 22, 141, 68, 10, 11, 12, 13, 14, 22, 30, - /* 2060 */ 17, 32, 22, 22, 154, 155, 156, 157, 158, 40, - /* 2070 */ 23, 68, 60, 30, 24, 32, 97, 28, 22, 68, - /* 2080 */ 23, 37, 34, 40, 150, 22, 25, 23, 23, 23, - /* 2090 */ 22, 98, 142, 183, 23, 23, 34, 22, 25, 89, - /* 2100 */ 71, 34, 117, 144, 34, 22, 76, 76, 79, 87, - /* 2110 */ 34, 82, 34, 44, 71, 94, 34, 23, 25, 24, - /* 2120 */ 34, 25, 79, 23, 23, 82, 23, 23, 99, 143, - /* 2130 */ 143, 22, 25, 25, 23, 22, 11, 22, 22, 25, - /* 2140 */ 23, 23, 99, 22, 22, 136, 142, 142, 142, 25, - /* 2150 */ 23, 15, 1, 1, 323, 323, 323, 323, 323, 323, + /* 1830 */ 202, 30, 38, 32, 202, 152, 151, 22, 36, 149, + /* 1840 */ 146, 40, 43, 18, 252, 251, 236, 252, 251, 283, + /* 1850 */ 239, 239, 239, 239, 202, 18, 201, 150, 236, 202, + /* 1860 */ 201, 236, 60, 248, 248, 275, 273, 248, 159, 273, + /* 1870 */ 63, 286, 71, 275, 72, 275, 22, 297, 297, 248, + /* 1880 */ 79, 202, 202, 82, 223, 201, 223, 201, 116, 220, + /* 1890 */ 22, 202, 293, 292, 65, 229, 201, 220, 226, 226, + /* 1900 */ 99, 127, 166, 101, 24, 229, 114, 83, 92, 107, + /* 1910 */ 108, 220, 308, 202, 285, 223, 268, 115, 220, 117, + /* 1920 */ 118, 119, 222, 220, 122, 220, 220, 223, 285, 19, + /* 1930 */ 20, 22, 22, 268, 316, 134, 280, 202, 159, 148, + /* 1940 */ 139, 140, 321, 141, 282, 147, 36, 321, 25, 252, + /* 1950 */ 251, 204, 250, 252, 13, 196, 154, 155, 156, 157, + /* 1960 */ 158, 249, 248, 196, 163, 6, 306, 194, 215, 194, + /* 1970 */ 60, 194, 209, 306, 209, 215, 215, 215, 224, 224, + /* 1980 */ 216, 215, 72, 4, 216, 183, 3, 209, 22, 15, + /* 1990 */ 164, 16, 23, 23, 152, 140, 131, 25, 20, 143, + /* 2000 */ 24, 16, 145, 1, 143, 131, 131, 62, 37, 54, + /* 2010 */ 152, 101, 54, 303, 54, 54, 131, 107, 108, 117, + /* 2020 */ 34, 142, 1, 5, 116, 115, 22, 117, 118, 119, + /* 2030 */ 1, 2, 122, 162, 5, 25, 69, 69, 76, 10, + /* 2040 */ 11, 12, 13, 14, 41, 142, 17, 116, 5, 24, + /* 2050 */ 20, 141, 19, 10, 11, 12, 13, 14, 132, 30, + /* 2060 */ 17, 32, 126, 22, 154, 155, 156, 157, 158, 40, + /* 2070 */ 22, 22, 22, 30, 68, 32, 23, 68, 24, 60, + /* 2080 */ 28, 97, 22, 40, 68, 23, 22, 150, 37, 34, + /* 2090 */ 23, 23, 22, 183, 25, 23, 98, 23, 142, 23, + /* 2100 */ 71, 22, 25, 89, 117, 34, 94, 144, 79, 34, + /* 2110 */ 76, 82, 34, 87, 71, 34, 34, 44, 76, 34, + /* 2120 */ 23, 22, 79, 25, 24, 82, 25, 34, 99, 23, + /* 2130 */ 23, 143, 143, 23, 23, 22, 11, 25, 23, 25, + /* 2140 */ 22, 22, 99, 22, 136, 23, 23, 142, 22, 22, + /* 2150 */ 15, 1, 1, 25, 25, 142, 23, 142, 323, 323, /* 2160 */ 323, 323, 323, 134, 323, 323, 323, 323, 139, 140, /* 2170 */ 323, 323, 323, 323, 323, 323, 323, 134, 323, 323, /* 2180 */ 323, 323, 139, 140, 323, 323, 323, 323, 323, 323, @@ -176366,16 +176369,16 @@ static const YYCODETYPE yy_lookahead[] = { /* 2310 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, /* 2320 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, /* 2330 */ 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, - /* 2340 */ 323, 187, 187, 187, 187, 187, 187, 187, 187, 187, + /* 2340 */ 323, 323, 323, 323, 323, 187, 187, 187, 187, 187, /* 2350 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2360 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2370 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2380 */ 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, /* 2390 */ 187, 187, 187, 187, }; -#define YY_SHIFT_COUNT (582) +#define YY_SHIFT_COUNT (586) #define YY_SHIFT_MIN (0) -#define YY_SHIFT_MAX (2152) +#define YY_SHIFT_MAX (2151) static const unsigned short int yy_shift_ofst[] = { /* 0 */ 2029, 1801, 2043, 1380, 1380, 318, 271, 1496, 1569, 1642, /* 10 */ 702, 702, 702, 740, 318, 318, 318, 318, 318, 0, @@ -176410,36 +176413,36 @@ static const unsigned short int yy_shift_ofst[] = { /* 300 */ 667, 667, 1487, 667, 1198, 1435, 777, 1011, 1423, 584, /* 310 */ 584, 584, 1273, 1273, 1273, 1273, 1471, 1471, 880, 1530, /* 320 */ 1190, 1095, 1731, 1731, 1668, 1668, 1794, 1794, 1668, 1683, - /* 330 */ 1685, 1815, 1796, 1824, 1824, 1824, 1824, 1668, 1828, 1701, - /* 340 */ 1685, 1685, 1701, 1815, 1796, 1701, 1796, 1701, 1668, 1828, - /* 350 */ 1697, 1800, 1668, 1828, 1848, 1668, 1828, 1668, 1828, 1848, - /* 360 */ 1766, 1766, 1766, 1823, 1870, 1870, 1848, 1766, 1767, 1766, - /* 370 */ 1823, 1766, 1766, 1727, 1872, 1783, 1783, 1848, 1668, 1813, - /* 380 */ 1813, 1825, 1825, 1777, 1781, 1906, 1668, 1774, 1777, 1789, - /* 390 */ 1792, 1701, 1919, 1935, 1935, 1949, 1949, 1949, 2207, 2207, - /* 400 */ 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, - /* 410 */ 2207, 2207, 2207, 69, 1032, 79, 357, 1377, 1206, 400, - /* 420 */ 1525, 835, 332, 1540, 1437, 1539, 1536, 1548, 1583, 1620, - /* 430 */ 1633, 1670, 1671, 1674, 1567, 1553, 1682, 1506, 1675, 1358, - /* 440 */ 1607, 1589, 1678, 1681, 1624, 1687, 1688, 1283, 1561, 1693, - /* 450 */ 1696, 1623, 1521, 1976, 1980, 1962, 1822, 1972, 1973, 1965, - /* 460 */ 1967, 1851, 1840, 1862, 1969, 1969, 1971, 1853, 1977, 1854, - /* 470 */ 1982, 1999, 1858, 1871, 1969, 1873, 1941, 1968, 1969, 1855, - /* 480 */ 1952, 1954, 1955, 1956, 1881, 1896, 1981, 1874, 2013, 2014, - /* 490 */ 1998, 1905, 1860, 1957, 2008, 1966, 1947, 1983, 1894, 1921, - /* 500 */ 2020, 2018, 2026, 1915, 1923, 2028, 1984, 2036, 2040, 2047, - /* 510 */ 2041, 2003, 2012, 2050, 1979, 2049, 2056, 2011, 2044, 2057, - /* 520 */ 2048, 1934, 2063, 2064, 2065, 2061, 2066, 2068, 1993, 1950, - /* 530 */ 2071, 2072, 1985, 2062, 2075, 1959, 2073, 2067, 2070, 2076, - /* 540 */ 2078, 2010, 2030, 2022, 2069, 2031, 2021, 2082, 2094, 2083, - /* 550 */ 2095, 2093, 2096, 2086, 1986, 1987, 2100, 2073, 2101, 2103, - /* 560 */ 2104, 2109, 2107, 2108, 2111, 2113, 2125, 2115, 2116, 2117, - /* 570 */ 2118, 2121, 2122, 2114, 2009, 2004, 2005, 2006, 2124, 2127, - /* 580 */ 2136, 2151, 2152, + /* 330 */ 1685, 1815, 1690, 1694, 1799, 1690, 1694, 1825, 1825, 1825, + /* 340 */ 1825, 1668, 1837, 1707, 1685, 1685, 1707, 1815, 1799, 1707, + /* 350 */ 1799, 1707, 1668, 1837, 1709, 1807, 1668, 1837, 1854, 1668, + /* 360 */ 1837, 1668, 1837, 1854, 1772, 1772, 1772, 1829, 1868, 1868, + /* 370 */ 1854, 1772, 1774, 1772, 1829, 1772, 1772, 1736, 1880, 1792, + /* 380 */ 1792, 1854, 1668, 1816, 1816, 1824, 1824, 1690, 1694, 1909, + /* 390 */ 1668, 1779, 1690, 1791, 1798, 1707, 1923, 1941, 1941, 1959, + /* 400 */ 1959, 1959, 2207, 2207, 2207, 2207, 2207, 2207, 2207, 2207, + /* 410 */ 2207, 2207, 2207, 2207, 2207, 2207, 2207, 69, 1032, 79, + /* 420 */ 357, 1377, 1206, 400, 1525, 835, 332, 1540, 1437, 1539, + /* 430 */ 1536, 1548, 1583, 1620, 1633, 1670, 1671, 1674, 1567, 1553, + /* 440 */ 1682, 1506, 1675, 1358, 1607, 1589, 1678, 1681, 1624, 1687, + /* 450 */ 1688, 1283, 1561, 1693, 1696, 1623, 1521, 1979, 1983, 1966, + /* 460 */ 1826, 1974, 1975, 1969, 1970, 1855, 1842, 1865, 1972, 1972, + /* 470 */ 1976, 1856, 1978, 1857, 1985, 2002, 1861, 1874, 1972, 1875, + /* 480 */ 1945, 1971, 1972, 1858, 1955, 1958, 1960, 1961, 1885, 1902, + /* 490 */ 1986, 1879, 2021, 2018, 2004, 1908, 1871, 1967, 2010, 1968, + /* 500 */ 1962, 2003, 1903, 1931, 2025, 2030, 2033, 1926, 1936, 2041, + /* 510 */ 2006, 2048, 2049, 2053, 2050, 2009, 2019, 2054, 1984, 2052, + /* 520 */ 2060, 2016, 2051, 2062, 2055, 1937, 2064, 2067, 2068, 2069, + /* 530 */ 2072, 2070, 1998, 1956, 2074, 2076, 1987, 2071, 2079, 1963, + /* 540 */ 2077, 2075, 2078, 2081, 2082, 2014, 2034, 2026, 2073, 2042, + /* 550 */ 2012, 2085, 2097, 2099, 2100, 2098, 2101, 2093, 1988, 1989, + /* 560 */ 2106, 2077, 2107, 2110, 2111, 2113, 2112, 2114, 2115, 2118, + /* 570 */ 2125, 2119, 2121, 2122, 2123, 2126, 2127, 2128, 2008, 2005, + /* 580 */ 2013, 2015, 2129, 2133, 2135, 2150, 2151, }; -#define YY_REDUCE_COUNT (412) +#define YY_REDUCE_COUNT (416) #define YY_REDUCE_MIN (-277) -#define YY_REDUCE_MAX (1772) +#define YY_REDUCE_MAX (1778) static const short yy_reduce_ofst[] = { /* 0 */ -67, 1252, -64, -178, -181, 160, 1071, 143, -184, 137, /* 10 */ 218, 220, 222, -174, 229, 268, 272, 275, 324, -208, @@ -176474,76 +176477,76 @@ static const short yy_reduce_ofst[] = { /* 300 */ 1509, 1517, 1546, 1519, 1557, 1489, 1565, 1564, 1578, 1586, /* 310 */ 1587, 1588, 1526, 1528, 1554, 1555, 1576, 1577, 1566, 1579, /* 320 */ 1584, 1591, 1520, 1523, 1617, 1628, 1580, 1581, 1632, 1585, - /* 330 */ 1590, 1593, 1604, 1605, 1606, 1608, 1609, 1641, 1649, 1610, - /* 340 */ 1592, 1594, 1611, 1595, 1616, 1612, 1618, 1613, 1651, 1654, - /* 350 */ 1596, 1598, 1655, 1663, 1650, 1673, 1680, 1677, 1684, 1653, - /* 360 */ 1664, 1666, 1667, 1662, 1669, 1672, 1676, 1686, 1679, 1691, - /* 370 */ 1689, 1692, 1694, 1597, 1599, 1619, 1630, 1699, 1700, 1602, - /* 380 */ 1615, 1648, 1657, 1690, 1698, 1658, 1729, 1652, 1695, 1702, - /* 390 */ 1704, 1703, 1741, 1754, 1758, 1768, 1769, 1771, 1660, 1661, - /* 400 */ 1665, 1752, 1756, 1757, 1759, 1760, 1764, 1745, 1753, 1762, - /* 410 */ 1763, 1761, 1772, + /* 330 */ 1590, 1593, 1592, 1594, 1610, 1595, 1597, 1611, 1612, 1613, + /* 340 */ 1614, 1652, 1655, 1615, 1598, 1600, 1616, 1596, 1622, 1619, + /* 350 */ 1625, 1631, 1657, 1659, 1599, 1601, 1679, 1684, 1661, 1680, + /* 360 */ 1686, 1689, 1695, 1663, 1669, 1677, 1691, 1666, 1672, 1673, + /* 370 */ 1692, 1698, 1700, 1703, 1676, 1705, 1706, 1618, 1604, 1629, + /* 380 */ 1643, 1704, 1711, 1621, 1626, 1648, 1665, 1697, 1699, 1656, + /* 390 */ 1735, 1662, 1701, 1702, 1712, 1714, 1747, 1759, 1767, 1773, + /* 400 */ 1775, 1777, 1660, 1667, 1710, 1763, 1753, 1760, 1761, 1762, + /* 410 */ 1765, 1754, 1755, 1764, 1768, 1766, 1778, }; static const YYACTIONTYPE yy_default[] = { - /* 0 */ 1663, 1663, 1663, 1491, 1254, 1367, 1254, 1254, 1254, 1254, - /* 10 */ 1491, 1491, 1491, 1254, 1254, 1254, 1254, 1254, 1254, 1397, - /* 20 */ 1397, 1544, 1287, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 30 */ 1254, 1254, 1254, 1254, 1254, 1490, 1254, 1254, 1254, 1254, - /* 40 */ 1578, 1578, 1254, 1254, 1254, 1254, 1254, 1563, 1562, 1254, - /* 50 */ 1254, 1254, 1406, 1254, 1413, 1254, 1254, 1254, 1254, 1254, - /* 60 */ 1492, 1493, 1254, 1254, 1254, 1254, 1543, 1545, 1508, 1420, - /* 70 */ 1419, 1418, 1417, 1526, 1385, 1411, 1404, 1408, 1487, 1488, - /* 80 */ 1486, 1641, 1493, 1492, 1254, 1407, 1455, 1471, 1454, 1254, - /* 90 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 100 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 110 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 120 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 130 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 140 */ 1254, 1254, 1463, 1470, 1469, 1468, 1477, 1467, 1464, 1457, - /* 150 */ 1456, 1458, 1459, 1278, 1254, 1275, 1329, 1254, 1254, 1254, - /* 160 */ 1254, 1254, 1460, 1287, 1448, 1447, 1446, 1254, 1474, 1461, - /* 170 */ 1473, 1472, 1551, 1615, 1614, 1509, 1254, 1254, 1254, 1254, - /* 180 */ 1254, 1254, 1578, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 190 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 200 */ 1254, 1254, 1254, 1254, 1254, 1387, 1578, 1578, 1254, 1287, - /* 210 */ 1578, 1578, 1388, 1388, 1283, 1283, 1391, 1558, 1358, 1358, - /* 220 */ 1358, 1358, 1367, 1358, 1254, 1254, 1254, 1254, 1254, 1254, - /* 230 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1548, - /* 240 */ 1546, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 250 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 260 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1363, 1254, - /* 270 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1608, - /* 280 */ 1254, 1521, 1343, 1363, 1363, 1363, 1363, 1365, 1344, 1342, - /* 290 */ 1357, 1288, 1261, 1655, 1423, 1412, 1364, 1412, 1652, 1410, - /* 300 */ 1423, 1423, 1410, 1423, 1364, 1652, 1304, 1630, 1299, 1397, - /* 310 */ 1397, 1397, 1387, 1387, 1387, 1387, 1391, 1391, 1489, 1364, - /* 320 */ 1357, 1254, 1655, 1655, 1373, 1373, 1654, 1654, 1373, 1509, - /* 330 */ 1638, 1432, 1332, 1338, 1338, 1338, 1338, 1373, 1272, 1410, - /* 340 */ 1638, 1638, 1410, 1432, 1332, 1410, 1332, 1410, 1373, 1272, - /* 350 */ 1525, 1649, 1373, 1272, 1499, 1373, 1272, 1373, 1272, 1499, - /* 360 */ 1330, 1330, 1330, 1319, 1254, 1254, 1499, 1330, 1304, 1330, - /* 370 */ 1319, 1330, 1330, 1596, 1254, 1503, 1503, 1499, 1373, 1588, - /* 380 */ 1588, 1400, 1400, 1405, 1391, 1494, 1373, 1254, 1405, 1403, - /* 390 */ 1401, 1410, 1322, 1611, 1611, 1607, 1607, 1607, 1660, 1660, - /* 400 */ 1558, 1623, 1287, 1287, 1287, 1287, 1623, 1306, 1306, 1288, - /* 410 */ 1288, 1287, 1623, 1254, 1254, 1254, 1254, 1254, 1254, 1618, - /* 420 */ 1254, 1553, 1510, 1377, 1254, 1254, 1254, 1254, 1254, 1254, - /* 430 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 440 */ 1564, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 450 */ 1254, 1254, 1437, 1254, 1257, 1555, 1254, 1254, 1254, 1254, - /* 460 */ 1254, 1254, 1254, 1254, 1414, 1415, 1378, 1254, 1254, 1254, - /* 470 */ 1254, 1254, 1254, 1254, 1429, 1254, 1254, 1254, 1424, 1254, - /* 480 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1651, 1254, 1254, - /* 490 */ 1254, 1254, 1254, 1254, 1524, 1523, 1254, 1254, 1375, 1254, - /* 500 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 510 */ 1254, 1254, 1302, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 520 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 530 */ 1254, 1254, 1254, 1254, 1254, 1254, 1402, 1254, 1254, 1254, - /* 540 */ 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 550 */ 1254, 1593, 1392, 1254, 1254, 1254, 1254, 1642, 1254, 1254, - /* 560 */ 1254, 1254, 1352, 1254, 1254, 1254, 1254, 1254, 1254, 1254, - /* 570 */ 1254, 1254, 1254, 1634, 1346, 1438, 1254, 1441, 1276, 1254, - /* 580 */ 1266, 1254, 1254, + /* 0 */ 1667, 1667, 1667, 1495, 1258, 1371, 1258, 1258, 1258, 1258, + /* 10 */ 1495, 1495, 1495, 1258, 1258, 1258, 1258, 1258, 1258, 1401, + /* 20 */ 1401, 1548, 1291, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 30 */ 1258, 1258, 1258, 1258, 1258, 1494, 1258, 1258, 1258, 1258, + /* 40 */ 1582, 1582, 1258, 1258, 1258, 1258, 1258, 1567, 1566, 1258, + /* 50 */ 1258, 1258, 1410, 1258, 1417, 1258, 1258, 1258, 1258, 1258, + /* 60 */ 1496, 1497, 1258, 1258, 1258, 1258, 1547, 1549, 1512, 1424, + /* 70 */ 1423, 1422, 1421, 1530, 1389, 1415, 1408, 1412, 1491, 1492, + /* 80 */ 1490, 1645, 1497, 1496, 1258, 1411, 1459, 1475, 1458, 1258, + /* 90 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 100 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 110 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 120 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 130 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 140 */ 1258, 1258, 1467, 1474, 1473, 1472, 1481, 1471, 1468, 1461, + /* 150 */ 1460, 1462, 1463, 1282, 1258, 1279, 1333, 1258, 1258, 1258, + /* 160 */ 1258, 1258, 1464, 1291, 1452, 1451, 1450, 1258, 1478, 1465, + /* 170 */ 1477, 1476, 1555, 1619, 1618, 1513, 1258, 1258, 1258, 1258, + /* 180 */ 1258, 1258, 1582, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 190 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 200 */ 1258, 1258, 1258, 1258, 1258, 1391, 1582, 1582, 1258, 1291, + /* 210 */ 1582, 1582, 1392, 1392, 1287, 1287, 1395, 1562, 1362, 1362, + /* 220 */ 1362, 1362, 1371, 1362, 1258, 1258, 1258, 1258, 1258, 1258, + /* 230 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1552, + /* 240 */ 1550, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 250 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 260 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1367, 1258, + /* 270 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1612, + /* 280 */ 1258, 1525, 1347, 1367, 1367, 1367, 1367, 1369, 1348, 1346, + /* 290 */ 1361, 1292, 1265, 1659, 1427, 1416, 1368, 1416, 1656, 1414, + /* 300 */ 1427, 1427, 1414, 1427, 1368, 1656, 1308, 1634, 1303, 1401, + /* 310 */ 1401, 1401, 1391, 1391, 1391, 1391, 1395, 1395, 1493, 1368, + /* 320 */ 1361, 1258, 1659, 1659, 1377, 1377, 1658, 1658, 1377, 1513, + /* 330 */ 1642, 1436, 1409, 1395, 1336, 1409, 1395, 1342, 1342, 1342, + /* 340 */ 1342, 1377, 1276, 1414, 1642, 1642, 1414, 1436, 1336, 1414, + /* 350 */ 1336, 1414, 1377, 1276, 1529, 1653, 1377, 1276, 1503, 1377, + /* 360 */ 1276, 1377, 1276, 1503, 1334, 1334, 1334, 1323, 1258, 1258, + /* 370 */ 1503, 1334, 1308, 1334, 1323, 1334, 1334, 1600, 1258, 1507, + /* 380 */ 1507, 1503, 1377, 1592, 1592, 1404, 1404, 1409, 1395, 1498, + /* 390 */ 1377, 1258, 1409, 1407, 1405, 1414, 1326, 1615, 1615, 1611, + /* 400 */ 1611, 1611, 1664, 1664, 1562, 1627, 1291, 1291, 1291, 1291, + /* 410 */ 1627, 1310, 1310, 1292, 1292, 1291, 1627, 1258, 1258, 1258, + /* 420 */ 1258, 1258, 1258, 1622, 1258, 1557, 1514, 1381, 1258, 1258, + /* 430 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 440 */ 1258, 1258, 1258, 1258, 1568, 1258, 1258, 1258, 1258, 1258, + /* 450 */ 1258, 1258, 1258, 1258, 1258, 1258, 1441, 1258, 1261, 1559, + /* 460 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1418, 1419, + /* 470 */ 1382, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1433, 1258, + /* 480 */ 1258, 1258, 1428, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 490 */ 1258, 1655, 1258, 1258, 1258, 1258, 1258, 1258, 1528, 1527, + /* 500 */ 1258, 1258, 1379, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 510 */ 1258, 1258, 1258, 1258, 1258, 1258, 1306, 1258, 1258, 1258, + /* 520 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 530 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 540 */ 1406, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1258, + /* 550 */ 1258, 1258, 1258, 1258, 1258, 1597, 1396, 1258, 1258, 1258, + /* 560 */ 1258, 1646, 1258, 1258, 1258, 1258, 1356, 1258, 1258, 1258, + /* 570 */ 1258, 1258, 1258, 1258, 1258, 1258, 1258, 1638, 1350, 1442, + /* 580 */ 1258, 1445, 1280, 1258, 1270, 1258, 1258, }; /********** End of lemon-generated parsing tables *****************************/ @@ -177315,14 +177318,14 @@ static const char *const yyRuleName[] = { /* 149 */ "limit_opt ::= LIMIT expr", /* 150 */ "limit_opt ::= LIMIT expr OFFSET expr", /* 151 */ "limit_opt ::= LIMIT expr COMMA expr", - /* 152 */ "cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret", + /* 152 */ "cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt", /* 153 */ "where_opt ::=", /* 154 */ "where_opt ::= WHERE expr", /* 155 */ "where_opt_ret ::=", /* 156 */ "where_opt_ret ::= WHERE expr", /* 157 */ "where_opt_ret ::= RETURNING selcollist", /* 158 */ "where_opt_ret ::= WHERE expr RETURNING selcollist", - /* 159 */ "cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret", + /* 159 */ "cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt", /* 160 */ "setlist ::= setlist COMMA nm EQ expr", /* 161 */ "setlist ::= setlist COMMA LP idlist RP EQ expr", /* 162 */ "setlist ::= nm EQ expr", @@ -178240,14 +178243,14 @@ static const YYCODETYPE yyRuleInfoLhs[] = { 252, /* (149) limit_opt ::= LIMIT expr */ 252, /* (150) limit_opt ::= LIMIT expr OFFSET expr */ 252, /* (151) limit_opt ::= LIMIT expr COMMA expr */ - 192, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + 192, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ 248, /* (153) where_opt ::= */ 248, /* (154) where_opt ::= WHERE expr */ 270, /* (155) where_opt_ret ::= */ 270, /* (156) where_opt_ret ::= WHERE expr */ 270, /* (157) where_opt_ret ::= RETURNING selcollist */ 270, /* (158) where_opt_ret ::= WHERE expr RETURNING selcollist */ - 192, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + 192, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ 271, /* (160) setlist ::= setlist COMMA nm EQ expr */ 271, /* (161) setlist ::= setlist COMMA LP idlist RP EQ expr */ 271, /* (162) setlist ::= nm EQ expr */ @@ -178654,14 +178657,14 @@ static const signed char yyRuleInfoNRhs[] = { -2, /* (149) limit_opt ::= LIMIT expr */ -4, /* (150) limit_opt ::= LIMIT expr OFFSET expr */ -4, /* (151) limit_opt ::= LIMIT expr COMMA expr */ - -6, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + -8, /* (152) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ 0, /* (153) where_opt ::= */ -2, /* (154) where_opt ::= WHERE expr */ 0, /* (155) where_opt_ret ::= */ -2, /* (156) where_opt_ret ::= WHERE expr */ -2, /* (157) where_opt_ret ::= RETURNING selcollist */ -4, /* (158) where_opt_ret ::= WHERE expr RETURNING selcollist */ - -9, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + -11, /* (159) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ -5, /* (160) setlist ::= setlist COMMA nm EQ expr */ -7, /* (161) setlist ::= setlist COMMA LP idlist RP EQ expr */ -3, /* (162) setlist ::= nm EQ expr */ @@ -179579,10 +179582,17 @@ static YYACTIONTYPE yy_reduce( case 151: /* limit_opt ::= LIMIT expr COMMA expr */ {yymsp[-3].minor.yy590 = sqlite3PExpr(pParse,TK_LIMIT,yymsp[0].minor.yy590,yymsp[-2].minor.yy590);} break; - case 152: /* cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ + case 152: /* cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt limit_opt */ { - sqlite3SrcListIndexedBy(pParse, yymsp[-2].minor.yy563, &yymsp[-1].minor.yy0); - sqlite3DeleteFrom(pParse,yymsp[-2].minor.yy563,yymsp[0].minor.yy590,0,0); + sqlite3SrcListIndexedBy(pParse, yymsp[-4].minor.yy563, &yymsp[-3].minor.yy0); +#ifndef SQLITE_ENABLE_UPDATE_DELETE_LIMIT + if( yymsp[-1].minor.yy402 || yymsp[0].minor.yy590 ){ + updateDeleteLimitError(pParse,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); + yymsp[-1].minor.yy402 = 0; + yymsp[0].minor.yy590 = 0; + } +#endif + sqlite3DeleteFrom(pParse,yymsp[-4].minor.yy563,yymsp[-2].minor.yy590,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); } break; case 157: /* where_opt_ret ::= RETURNING selcollist */ @@ -179591,12 +179601,11 @@ static YYACTIONTYPE yy_reduce( case 158: /* where_opt_ret ::= WHERE expr RETURNING selcollist */ {sqlite3AddReturning(pParse,yymsp[0].minor.yy402); yymsp[-3].minor.yy590 = yymsp[-2].minor.yy590;} break; - case 159: /* cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ + case 159: /* cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret orderby_opt limit_opt */ { - sqlite3SrcListIndexedBy(pParse, yymsp[-5].minor.yy563, &yymsp[-4].minor.yy0); - sqlite3ExprListCheckLength(pParse,yymsp[-2].minor.yy402,"set list"); - if( yymsp[-1].minor.yy563 ){ - SrcList *pFromClause = yymsp[-1].minor.yy563; + sqlite3SrcListIndexedBy(pParse, yymsp[-7].minor.yy563, &yymsp[-6].minor.yy0); + if( yymsp[-3].minor.yy563 ){ + SrcList *pFromClause = yymsp[-3].minor.yy563; if( pFromClause->nSrc>1 ){ Select *pSubquery; Token as; @@ -179605,9 +179614,17 @@ static YYACTIONTYPE yy_reduce( as.z = 0; pFromClause = sqlite3SrcListAppendFromTerm(pParse,0,0,0,&as,pSubquery,0); } - yymsp[-5].minor.yy563 = sqlite3SrcListAppendList(pParse, yymsp[-5].minor.yy563, pFromClause); + yymsp[-7].minor.yy563 = sqlite3SrcListAppendList(pParse, yymsp[-7].minor.yy563, pFromClause); } - sqlite3Update(pParse,yymsp[-5].minor.yy563,yymsp[-2].minor.yy402,yymsp[0].minor.yy590,yymsp[-6].minor.yy502,0,0,0); + sqlite3ExprListCheckLength(pParse,yymsp[-4].minor.yy402,"set list"); +#ifndef SQLITE_ENABLE_UPDATE_DELETE_LIMIT + if( yymsp[-1].minor.yy402 || yymsp[0].minor.yy590 ){ + updateDeleteLimitError(pParse,yymsp[-1].minor.yy402,yymsp[0].minor.yy590); + yymsp[-1].minor.yy402 = 0; + yymsp[0].minor.yy590 = 0; + } +#endif + sqlite3Update(pParse,yymsp[-7].minor.yy563,yymsp[-4].minor.yy402,yymsp[-2].minor.yy590,yymsp[-8].minor.yy502,yymsp[-1].minor.yy402,yymsp[0].minor.yy590,0); } break; case 160: /* setlist ::= setlist COMMA nm EQ expr */ diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index 539f4854c6..ff6653a959 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -1214,7 +1214,23 @@ describe("SQLite-specific features", () => { await sql`insert into "users" ("id", "name", "verified", "created_at") values (null, ${"John"}, ${0}, strftime('%s', 'now')) returning upper("name")`; expect(upperName).toBe("JOHN"); }); - + test("order by and limit in delete statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql`CREATE TABLE users (id INTEGER, name TEXT)`; + await sql`INSERT INTO users VALUES (1, 'John'), (2, 'Jane'), (3, 'Austin')`; + const result = await sql`delete from "users" where "users"."id" = ${1} order by "users"."name" asc limit ${1}`; + expect(result.count).toBe(1); + expect(result.command).toBe("DELETE"); + }); + test("order by and limit in update statements", async () => { + await using sql = new SQL("sqlite://:memory:"); + await sql`CREATE TABLE users (id INTEGER, name TEXT)`; + await sql`INSERT INTO users VALUES (1, 'John'), (2, 'Jane'), (3, 'Austin')`; + const result = + await sql`update "users" set "name" = 'John' where "users"."id" = ${1} order by "users"."name" asc limit ${1}`; + expect(result.count).toBe(1); + expect(result.command).toBe("UPDATE"); + }); test("last_insert_rowid()", async () => { await sql`CREATE TABLE rowid_test (id INTEGER PRIMARY KEY, value TEXT)`; await sql`INSERT INTO rowid_test (value) VALUES ('test')`; From 2e86f74764e2f24958417593fc36516c071d3838 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 02:51:12 -0700 Subject: [PATCH 024/391] Update no-validate-leaksan.txt --- test/no-validate-leaksan.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/no-validate-leaksan.txt b/test/no-validate-leaksan.txt index 217b98d0dc..876d7b356c 100644 --- a/test/no-validate-leaksan.txt +++ b/test/no-validate-leaksan.txt @@ -367,6 +367,9 @@ test/js/bun/util/inspect.test.js test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js test/js/node/vm/vm.test.ts +# VM has terminated +test/js/node/test/parallel/test-net-during-close.js + # JSC::BuiltinNames::~BuiltinNames test/js/bun/shell/shell-hang.test.ts test/js/bun/util/reportError.test.ts From 6c8635da63e0519478499beca89cd15cf44bd94c Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 4 Oct 2025 02:59:47 -0700 Subject: [PATCH 025/391] fix(install): isolated installs with transitive self dependencies (#23222) ### What does this PR do? Packages with self dependencies at a different version were colliding with the current version in the store node_modules. This pr nests them in another node_modules Example: self-dep@1.0.2 has a dependency on self-dep@1.0.1. self-dep@1.0.2 is placed here in: `./node_modules/.bun/self-dep@1.0.2/node_modules/self-dep` and it's self-dep dependency symlink is now placed in: `./node_modules/.bun/self-dep@1.0.2/node_modules/self-dep/node_modules/self-dep` fixes #22681 ### How did you verify your code works? Manually tested the linked issue is working, and added a test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/isolated_install/Installer.zig | 50 +++++++++++++- test/cli/install/isolated-install.test.ts | 35 ++++++++++ .../registry/packages/self-dep/package.json | 65 ++++++++++++++++++ .../packages/self-dep/self-dep-1.0.1.tgz | Bin 0 -> 143 bytes .../packages/self-dep/self-dep-1.0.2.tgz | Bin 0 -> 166 bytes 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 test/cli/install/registry/packages/self-dep/package.json create mode 100644 test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz create mode 100644 test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz diff --git a/src/install/isolated_install/Installer.zig b/src/install/isolated_install/Installer.zig index 42c1ccabef..e84e67eb40 100644 --- a/src/install/isolated_install/Installer.zig +++ b/src/install/isolated_install/Installer.zig @@ -741,13 +741,23 @@ pub const Installer = struct { const dependencies = lockfile.buffers.dependencies.items; for (entry_dependencies[this.entry_id.get()].slice()) |dep| { - const dep_name = dependencies[dep.dep_id].name; + const dep_name = dependencies[dep.dep_id].name.slice(string_buf); var dest: bun.Path(.{ .sep = .auto }) = .initTopLevelDir(); defer dest.deinit(); installer.appendStoreNodeModulesPath(&dest, this.entry_id); - dest.append(dep_name.slice(string_buf)); + + dest.append(dep_name); + + if (installer.entryStoreNodeModulesPackageName(dep_id, pkg_id, &pkg_res, pkg_names)) |entry_node_modules_name| { + if (strings.eqlLong(dep_name, entry_node_modules_name, true)) { + // nest the dependency in another node_modules if the name is the same as the entry name + // in the store node_modules to avoid collision + dest.append("node_modules"); + dest.append(dep_name); + } + } var dep_store_path: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir(); defer dep_store_path.deinit(); @@ -1283,6 +1293,8 @@ pub const Installer = struct { } else { buf.append(pkg_name.slice(string_buf)); } + } else { + // append nothing. buf is already top_level_dir } }, .workspace => { @@ -1306,6 +1318,38 @@ pub const Installer = struct { }, } } + + /// The directory name for the entry store node_modules install + /// folder. + /// ./node_modules/.bun/jquery@3.7.1/node_modules/jquery + /// ^ this one + /// Need to know this to avoid collisions with dependencies + /// with the same name as the package. + pub fn entryStoreNodeModulesPackageName( + this: *const Installer, + dep_id: DependencyID, + pkg_id: PackageID, + pkg_res: *const Resolution, + pkg_names: []const String, + ) ?[]const u8 { + const string_buf = this.lockfile.buffers.string_bytes.items; + + return switch (pkg_res.tag) { + .root => { + if (dep_id != invalid_dependency_id) { + const pkg_name = pkg_names[pkg_id]; + if (pkg_name.isEmpty()) { + return std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir); + } + return pkg_name.slice(string_buf); + } + return null; + }, + .workspace => null, + .symlink => null, + else => pkg_names[pkg_id].slice(string_buf), + }; + } }; const string = []const u8; @@ -1332,6 +1376,8 @@ const String = bun.Semver.String; const install = bun.install; const Bin = install.Bin; +const DependencyID = install.DependencyID; +const PackageID = install.PackageID; const PackageInstall = install.PackageInstall; const PackageManager = install.PackageManager; const PackageNameHash = install.PackageNameHash; diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 3766f0c60c..1217a7bf71 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -226,6 +226,41 @@ test("handles cyclic dependencies", async () => { }); }); +test("package with dependency on previous self works", async () => { + const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); + + await write( + packageJson, + JSON.stringify({ + name: "test-transitive-self-dep", + dependencies: { + "self-dep": "1.0.2", + }, + }), + ); + + await runBunInstall(bunEnv, packageDir); + + expect( + await Promise.all([ + file(join(packageDir, "node_modules", "self-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "self-dep", "node_modules", "self-dep", "package.json")).json(), + ]), + ).toEqual([ + { + name: "self-dep", + version: "1.0.2", + dependencies: { + "self-dep": "1.0.1", + }, + }, + { + name: "self-dep", + version: "1.0.1", + }, + ]); +}); + test("can install folder dependencies", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { isolated: true } }); diff --git a/test/cli/install/registry/packages/self-dep/package.json b/test/cli/install/registry/packages/self-dep/package.json new file mode 100644 index 0000000000..41c7bf85c8 --- /dev/null +++ b/test/cli/install/registry/packages/self-dep/package.json @@ -0,0 +1,65 @@ +{ + "name": "self-dep", + "versions": { + "1.0.1": { + "name": "self-dep", + "version": "1.0.1", + "_id": "self-dep@1.0.1", + "_integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "dist": { + "integrity": "sha512-X9HQMiuvWXqhxJExWwQz5X+z901PvtvYVVAsEli2k5FDVOg2j6opnkQjha5iDTWbqBrG1m95N3rKwmq3OftU/Q==", + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "tarball": "http://http://localhost:4873/self-dep/-/self-dep-1.0.1.tgz" + }, + "contributors": [] + }, + "1.0.2": { + "name": "self-dep", + "version": "1.0.2", + "dependencies": { + "self-dep": "1.0.1" + }, + "_id": "self-dep@1.0.2", + "_integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "dist": { + "integrity": "sha512-idMxfr8aIs5CwIVOMTykKRK7MBURv62AjBZ+zRH2zPOZMsWbe+sBXha0zPhQNfP7cUWccF3yiSvs0AQwQXGKfA==", + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "tarball": "http://http://localhost:4873/self-dep/-/self-dep-1.0.2.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-10-03T21:30:17.221Z", + "created": "2025-10-03T21:30:02.446Z", + "1.0.1": "2025-10-03T21:30:02.446Z", + "1.0.2": "2025-10-03T21:30:17.221Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.2" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "self-dep-1.0.1.tgz": { + "shasum": "b95f3e460b2f2ede25a18cc3c25bc226a3edfc71", + "version": "1.0.1" + }, + "self-dep-1.0.2.tgz": { + "shasum": "d1bc984e927fd960511dbef211551408e3bb2f72", + "version": "1.0.2" + } + }, + "_rev": "", + "_id": "self-dep", + "readme": "" +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz b/test/cli/install/registry/packages/self-dep/self-dep-1.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1e8490d8548dac8e88d76ccc1df3584aab36d5c6 GIT binary patch literal 143 zcmV;A0C4{wiwFP!00002|LxDq3c@fDh2dHEDTb^yJwww7zD>|5-qO&ho8r4maHZ=a zDD!Q8m|2}1Hm9(UZGP1r%aCYh0K9Wt3*fT=de7*34-xO-7}5z=#Go&@m1`IYm|^7G xxF0b!%qE3PG;1~`o_EV_%w|!q_c)frkm^G$teECON-3rO#1kd1FfafJ004JlK{x;a literal 0 HcmV?d00001 diff --git a/test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz b/test/cli/install/registry/packages/self-dep/self-dep-1.0.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a42ab9bcc645dfe13a905217c01450e61df04ef0 GIT binary patch literal 166 zcmV;X09pSZiwFP!00002|LxDq3c@fD1<PGtS66!)- z7g5OBT$sn=dfS}rK{kHQ{1|$t76!nSCB+2rnE)3Rq1YKP8-tR-1*1{~^##{3+Cc#e zlzc1qC+-=McJ?B=CLQFwU$^4*Do$@QgsyjS!8!8nJZ;5`YsEF41YJ}7r>N0A&_V00000 literal 0 HcmV?d00001 From 46e7a3b3c5b2e22de110e8e8c38980a82b71b71b Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 04:57:29 -0700 Subject: [PATCH 026/391] Implement birthtime support on Linux using statx syscall (#23209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds birthtime (file creation time) support on Linux using the `statx` syscall - Stores birthtime in architecture-specific unused fields of the kernel Stat struct (x86_64 and aarch64) - Falls back to traditional `stat` on kernels < 4.11 that don't support `statx` - Includes comprehensive tests validating birthtime behavior Fixes #6585 ## Implementation Details **src/sys.zig:** - Added `StatxField` enum for field selection - Implemented `statxImpl()`, `fstatx()`, `statx()`, and `lstatx()` functions - Stores birthtime in unused padding fields (architecture-specific for x86_64 and aarch64) - Graceful fallback to traditional stat if statx is not supported **src/bun.js/node/node_fs.zig:** - Updated `stat()`, `fstat()`, and `lstat()` to use statx functions on Linux **src/bun.js/node/Stat.zig:** - Added `getBirthtime()` helper to extract birthtime from architecture-specific storage **test/js/node/fs/fs-birthtime-linux.test.ts:** - Tests non-zero birthtime values - Verifies birthtime immutability across file modifications - Validates consistency across stat/lstat/fstat - Tests BigInt stats with nanosecond precision - Verifies birthtime ordering relative to other timestamps ## Test Plan - [x] Run `bun bd test test/js/node/fs/fs-birthtime-linux.test.ts` - all 5 tests pass - [x] Compare behavior with Node.js - identical behavior - [x] Compare with system Bun - system Bun returns epoch, new implementation returns real birthtime 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/node/Stat.zig | 32 +++--- src/bun.js/node/node_fs.zig | 83 ++++++++++---- src/bun.js/node/node_fs_stat_watcher.zig | 60 +++++++--- src/sys.zig | 127 +++++++++++++++++++++ src/sys/PosixStat.zig | 96 ++++++++++++++++ src/sys_uv.zig | 1 + test/js/bun/util/bun-file.test.ts | 6 +- test/js/node/fs/fs-birthtime-linux.test.ts | 108 ++++++++++++++++++ 8 files changed, 455 insertions(+), 58 deletions(-) create mode 100644 src/sys/PosixStat.zig create mode 100644 test/js/node/fs/fs-birthtime-linux.test.ts diff --git a/src/bun.js/node/Stat.zig b/src/bun.js/node/Stat.zig index 45e3492ec0..1ffb7271b6 100644 --- a/src/bun.js/node/Stat.zig +++ b/src/bun.js/node/Stat.zig @@ -4,11 +4,15 @@ pub fn StatType(comptime big: bool) type { pub const new = bun.TrivialNew(@This()); pub const deinit = bun.TrivialDeinit(@This()); - value: bun.Stat, + value: Syscall.PosixStat, - const StatTimespec = if (Environment.isWindows) bun.windows.libuv.uv_timespec_t else std.posix.timespec; + const StatTimespec = bun.timespec; const Float = if (big) i64 else f64; + pub inline fn init(stat_: *const Syscall.PosixStat) @This() { + return .{ .value = stat_.* }; + } + inline fn toNanoseconds(ts: StatTimespec) u64 { if (ts.sec < 0) { return @intCast(@max(bun.timespec.nsSigned(&bun.timespec{ @@ -29,8 +33,8 @@ pub fn StatType(comptime big: bool) type { // > libuv calculates tv_sec and tv_nsec from it and converts to signed long, // > which causes Y2038 overflow. On the other platforms it is safe to treat // > negative values as pre-epoch time. - const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(ts.sec)) else ts.sec; - const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(ts.nsec)) else ts.nsec; + const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(@as(i32, @truncate(ts.sec)))) else ts.sec; + const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(@as(i32, @truncate(ts.nsec)))) else ts.nsec; if (big) { const sec: i64 = tv_sec; const nsec: i64 = tv_nsec; @@ -44,6 +48,10 @@ pub fn StatType(comptime big: bool) type { } } + fn getBirthtime(stat_: *const Syscall.PosixStat) StatTimespec { + return stat_.birthtim; + } + pub fn toJS(this: *const @This(), globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { return statToJS(&this.value, globalObject); } @@ -56,7 +64,7 @@ pub fn StatType(comptime big: bool) type { return @intCast(@min(@max(value, 0), std.math.maxInt(i64))); } - fn statToJS(stat_: *const bun.Stat, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { + fn statToJS(stat_: *const Syscall.PosixStat, globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { const aTime = stat_.atime(); const mTime = stat_.mtime(); const cTime = stat_.ctime(); @@ -70,14 +78,15 @@ pub fn StatType(comptime big: bool) type { const size: i64 = clampedInt64(stat_.size); const blksize: i64 = clampedInt64(stat_.blksize); const blocks: i64 = clampedInt64(stat_.blocks); + const bTime = getBirthtime(stat_); const atime_ms: Float = toTimeMS(aTime); const mtime_ms: Float = toTimeMS(mTime); const ctime_ms: Float = toTimeMS(cTime); + const birthtime_ms: Float = toTimeMS(bTime); const atime_ns: u64 = if (big) toNanoseconds(aTime) else 0; const mtime_ns: u64 = if (big) toNanoseconds(mTime) else 0; const ctime_ns: u64 = if (big) toNanoseconds(cTime) else 0; - const birthtime_ms: Float = if (Environment.isLinux) 0 else toTimeMS(stat_.birthtime()); - const birthtime_ns: u64 = if (big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0; + const birthtime_ns: u64 = if (big) toNanoseconds(bTime) else 0; if (big) { return bun.jsc.fromJSHostCall(globalObject, @src(), Bun__createJSBigIntStatsObject, .{ @@ -121,12 +130,6 @@ pub fn StatType(comptime big: bool) type { birthtime_ms, ); } - - pub fn init(stat_: *const bun.Stat) @This() { - return @This(){ - .value = stat_.*, - }; - } }; } extern fn Bun__JSBigIntStatsObjectConstructor(*jsc.JSGlobalObject) jsc.JSValue; @@ -180,7 +183,7 @@ pub const Stats = union(enum) { big: StatsBig, small: StatsSmall, - pub inline fn init(stat_: *const bun.Stat, big: bool) Stats { + pub inline fn init(stat_: *const Syscall.PosixStat, big: bool) Stats { if (big) { return .{ .big = StatsBig.init(stat_) }; } else { @@ -207,4 +210,5 @@ const std = @import("std"); const bun = @import("bun"); const Environment = bun.Environment; +const Syscall = bun.sys; const jsc = bun.jsc; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index ffd20551e9..174563b5c7 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3799,10 +3799,17 @@ pub const NodeFS = struct { } pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { - return switch (Syscall.fstat(args.fd)) { - .result => |*result| .{ .result = .init(result, args.big_int) }, - .err => |err| .{ .err = err }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.fstatx(args.fd, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| .{ .result = .init(&result, args.big_int) }, + .err => |err| .{ .err = err }, + }; + } else { + return switch (Syscall.fstat(args.fd)) { + .result => |result| .{ .result = .init(&Syscall.PosixStat.init(&result), args.big_int) }, + .err => |err| .{ .err = err }, + }; + } } pub fn fsync(_: *NodeFS, args: Arguments.Fsync, _: Flavor) Maybe(Return.Fsync) { @@ -3876,15 +3883,27 @@ pub const NodeFS = struct { } pub fn lstat(this: *NodeFS, args: Arguments.Lstat, _: Flavor) Maybe(Return.Lstat) { - return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { - .result => |*result| Maybe(Return.Lstat){ .result = .{ .stats = .init(result, args.big_int) } }, - .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { - return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; - } - break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; - }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.lstatx(args.path.sliceZ(&this.sync_error_buf), &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = .init(&result, args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; + }, + }; + } else { + return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { + .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = .init(&Syscall.PosixStat.init(&result), args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; + }, + }; + } } pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, _: Flavor) Maybe(Return.Mkdir) { @@ -5701,21 +5720,35 @@ pub const NodeFS = struct { const path = args.path.sliceZ(&this.sync_error_buf); if (bun.StandaloneModuleGraph.get()) |graph| { if (graph.stat(path)) |*result| { - return .{ .result = .{ .stats = .init(result, args.big_int) } }; + return .{ .result = .{ .stats = .init(&Syscall.PosixStat.init(result), args.big_int) } }; } } - return switch (Syscall.stat(path)) { - .result => |*result| .{ - .result = .{ .stats = .init(result, args.big_int) }, - }, - .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { - return .{ .result = .{ .not_found = {} } }; - } - break :brk .{ .err = err.withPath(args.path.slice()) }; - }, - }; + if (Environment.isLinux and Syscall.supports_statx_on_linux.load(.monotonic)) { + return switch (Syscall.statx(path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })) { + .result => |result| .{ + .result = .{ .stats = .init(&result, args.big_int) }, + }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return .{ .result = .{ .not_found = {} } }; + } + break :brk .{ .err = err.withPath(args.path.slice()) }; + }, + }; + } else { + return switch (Syscall.stat(path)) { + .result => |result| .{ + .result = .{ .stats = .init(&Syscall.PosixStat.init(&result), args.big_int) }, + }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return .{ .result = .{ .not_found = {} } }; + } + break :brk .{ .err = err.withPath(args.path.slice()) }; + }, + }; + } } pub fn symlink(this: *NodeFS, args: Arguments.Symlink, _: Flavor) Maybe(Return.Symlink) { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index c10a391f3f..8ff99bde77 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -1,6 +1,6 @@ const log = bun.Output.scoped(.StatWatcher, .visible); -fn statToJSStats(globalThis: *jsc.JSGlobalObject, stats: *const bun.Stat, bigint: bool) bun.JSError!jsc.JSValue { +fn statToJSStats(globalThis: *jsc.JSGlobalObject, stats: *const bun.sys.PosixStat, bigint: bool) bun.JSError!jsc.JSValue { if (bigint) { return StatsBig.init(stats).toJS(globalThis); } else { @@ -192,7 +192,7 @@ pub const StatWatcher = struct { poll_ref: bun.Async.KeepAlive = .{}, - last_stat: bun.Stat, + last_stat: bun.sys.PosixStat, last_jsvalue: jsc.Strong.Optional, scheduler: bun.ptr.RefPtr(StatWatcherScheduler), @@ -352,7 +352,15 @@ pub const StatWatcher = struct { return; } - const stat = bun.sys.stat(this.path); + const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic)) + bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks }) + else brk: { + const result = bun.sys.stat(this.path); + break :brk switch (result) { + .result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) }, + .err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e }, + }; + }; switch (stat) { .result => |res| { // we store the stat, but do not call the callback @@ -362,7 +370,7 @@ pub const StatWatcher = struct { .err => { // on enoent, eperm, we call cb with two zeroed stat objects // and store previous stat as a zeroed stat object, and then call the callback. - this.last_stat = std.mem.zeroes(bun.Stat); + this.last_stat = std.mem.zeroes(bun.sys.PosixStat); this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, initialStatErrorOnMainThread)); }, } @@ -406,23 +414,39 @@ pub const StatWatcher = struct { /// Called from any thread pub fn restat(this: *StatWatcher) void { log("recalling stat", .{}); - const stat = bun.sys.stat(this.path); + const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic)) + bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks }) + else brk: { + const result = bun.sys.stat(this.path); + break :brk switch (result) { + .result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) }, + .err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e }, + }; + }; const res = switch (stat) { .result => |res| res, - .err => std.mem.zeroes(bun.Stat), + .err => std.mem.zeroes(bun.sys.PosixStat), }; - var compare = res; - const StatT = @TypeOf(compare); - if (@hasField(StatT, "st_atim")) { - compare.st_atim = this.last_stat.st_atim; - } else if (@hasField(StatT, "st_atimespec")) { - compare.st_atimespec = this.last_stat.st_atimespec; - } else if (@hasField(StatT, "atim")) { - compare.atim = this.last_stat.atim; - } - - if (std.mem.eql(u8, std.mem.asBytes(&compare), std.mem.asBytes(&this.last_stat))) return; + // Ignore atime changes when comparing stats + // Compare field-by-field to avoid false positives from padding bytes + if (res.dev == this.last_stat.dev and + res.ino == this.last_stat.ino and + res.mode == this.last_stat.mode and + res.nlink == this.last_stat.nlink and + res.uid == this.last_stat.uid and + res.gid == this.last_stat.gid and + res.rdev == this.last_stat.rdev and + res.size == this.last_stat.size and + res.blksize == this.last_stat.blksize and + res.blocks == this.last_stat.blocks and + res.mtim.sec == this.last_stat.mtim.sec and + res.mtim.nsec == this.last_stat.mtim.nsec and + res.ctim.sec == this.last_stat.ctim.sec and + res.ctim.nsec == this.last_stat.ctim.nsec and + res.birthtim.sec == this.last_stat.birthtim.sec and + res.birthtim.nsec == this.last_stat.birthtim.nsec) + return; this.last_stat = res; this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, swapAndCallListenerOnMainThread)); @@ -480,7 +504,7 @@ pub const StatWatcher = struct { // Instant.now will not fail on our target platforms. .last_check = std.time.Instant.now() catch unreachable, // InitStatTask is responsible for setting this - .last_stat = std.mem.zeroes(bun.Stat), + .last_stat = std.mem.zeroes(bun.sys.PosixStat), .last_jsvalue = .empty, .scheduler = vm.rareData().nodeFSStatWatcherScheduler(vm), .ref_count = .init(), diff --git a/src/sys.zig b/src/sys.zig index 4bf87f89f5..2062abc9ed 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -300,6 +300,7 @@ pub const Tag = enum(u8) { }; pub const Error = @import("./sys/Error.zig"); +pub const PosixStat = @import("./sys/PosixStat.zig").PosixStat; pub fn Maybe(comptime ReturnTypeT: type) type { return bun.api.node.Maybe(ReturnTypeT, Error); @@ -501,6 +502,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { log("stat({s}) = {d}", .{ bun.asByteSlice(path), rc }); if (Maybe(bun.Stat).errnoSysP(rc, .stat, path)) |err| return err; + return Maybe(bun.Stat){ .result = stat_ }; } } @@ -551,8 +553,133 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { log("fstat({}) = {d}", .{ fd, rc }); if (Maybe(bun.Stat).errnoSysFd(rc, .fstat, fd)) |err| return err; + return Maybe(bun.Stat){ .result = stat_ }; } + +pub const StatxField = enum(comptime_int) { + type = linux.STATX_TYPE, + mode = linux.STATX_MODE, + nlink = linux.STATX_NLINK, + uid = linux.STATX_UID, + gid = linux.STATX_GID, + atime = linux.STATX_ATIME, + mtime = linux.STATX_MTIME, + ctime = linux.STATX_CTIME, + btime = linux.STATX_BTIME, + ino = linux.STATX_INO, + size = linux.STATX_SIZE, + blocks = linux.STATX_BLOCKS, +}; + +// Linux Kernel v4.11 +pub var supports_statx_on_linux = std.atomic.Value(bool).init(true); + +/// Linux kernel makedev encoding for device numbers +/// From glibc sys/sysmacros.h and Linux kernel +/// dev_t layout (64 bits): +/// Bits 31-20: major high (12 bits) +/// Bits 19-8: minor high (12 bits) +/// Bits 7-0: minor low (8 bits) +inline fn makedev(major: u32, minor: u32) u64 { + const maj: u64 = major & 0xFFF; + const min: u64 = minor & 0xFFFFF; + return (maj << 8) | (min & 0xFF) | ((min & 0xFFF00) << 12); +} + +fn statxImpl(fd: bun.FileDescriptor, path: ?[*:0]const u8, flags: u32, mask: u32) Maybe(PosixStat) { + if (comptime !Environment.isLinux) { + @compileError("statx is only supported on Linux"); + } + + var buf: linux.Statx = undefined; + + while (true) { + const rc = linux.statx(@intCast(fd.cast()), if (path) |p| p else "", flags, mask, &buf); + + if (Maybe(PosixStat).errnoSys(rc, .statx)) |err| { + // Retry on EINTR + if (err.getErrno() == .INTR) continue; + + // Handle unsupported statx by setting flag and falling back + if (err.getErrno() == .NOSYS or err.getErrno() == .OPNOTSUPP) { + supports_statx_on_linux.store(false, .monotonic); + if (path) |p| { + const path_span = bun.span(p); + const fallback = if (flags & linux.AT.SYMLINK_NOFOLLOW != 0) lstat(path_span) else stat(path_span); + return switch (fallback) { + .result => |s| .{ .result = PosixStat.init(&s) }, + .err => |e| .{ .err = e }, + }; + } else { + return switch (fstat(fd)) { + .result => |s| .{ .result = PosixStat.init(&s) }, + .err => |e| .{ .err = e }, + }; + } + } + + return err; + } + + // Convert statx buffer to PosixStat structure + const stat_ = PosixStat{ + .dev = makedev(buf.dev_major, buf.dev_minor), + .ino = buf.ino, + .mode = buf.mode, + .nlink = buf.nlink, + .uid = buf.uid, + .gid = buf.gid, + .rdev = makedev(buf.rdev_major, buf.rdev_minor), + .size = @bitCast(buf.size), + .blksize = @intCast(buf.blksize), + .blocks = @bitCast(buf.blocks), + .atim = .{ .sec = buf.atime.sec, .nsec = buf.atime.nsec }, + .mtim = .{ .sec = buf.mtime.sec, .nsec = buf.mtime.nsec }, + .ctim = .{ .sec = buf.ctime.sec, .nsec = buf.ctime.nsec }, + .birthtim = if (buf.mask & linux.STATX_BTIME != 0) + .{ .sec = buf.btime.sec, .nsec = buf.btime.nsec } + else + .{ .sec = 0, .nsec = 0 }, + }; + + return .{ .result = stat_ }; + } +} + +pub fn fstatx(fd: bun.FileDescriptor, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(fd, null, linux.AT.EMPTY_PATH, mask); +} + +pub fn statx(path: [*:0]const u8, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(bun.FD.fromNative(std.posix.AT.FDCWD), path, 0, mask); +} + +pub fn lstatx(path: [*:0]const u8, comptime fields: []const StatxField) Maybe(PosixStat) { + const mask: u32 = comptime brk: { + var i: u32 = 0; + for (fields) |field| { + i |= @intFromEnum(field); + } + break :brk i; + }; + return statxImpl(bun.FD.fromNative(std.posix.AT.FDCWD), path, linux.AT.SYMLINK_NOFOLLOW, mask); +} + pub fn lutimes(path: [:0]const u8, atime: jsc.Node.TimeLike, mtime: jsc.Node.TimeLike) Maybe(void) { if (comptime Environment.isWindows) { return sys_uv.lutimes(path, atime, mtime); diff --git a/src/sys/PosixStat.zig b/src/sys/PosixStat.zig new file mode 100644 index 0000000000..c8975032af --- /dev/null +++ b/src/sys/PosixStat.zig @@ -0,0 +1,96 @@ +/// POSIX-like stat structure with birthtime support for node:fs +/// This extends the standard POSIX stat with birthtime (creation time) +pub const PosixStat = extern struct { + dev: u64, + ino: u64, + mode: u32, + nlink: u64, + uid: u32, + gid: u32, + rdev: u64, + size: i64, + blksize: i64, + blocks: i64, + + /// Access time + atim: bun.timespec, + /// Modification time + mtim: bun.timespec, + /// Change time (metadata) + ctim: bun.timespec, + /// Birth time (creation time) - may be zero if not supported + birthtim: bun.timespec, + + /// Convert platform-specific bun.Stat to PosixStat + pub fn init(stat_: *const bun.Stat) PosixStat { + if (Environment.isWindows) { + // Windows: all fields need casting + const atime_val = stat_.atime(); + const mtime_val = stat_.mtime(); + const ctime_val = stat_.ctime(); + const birthtime_val = stat_.birthtime(); + + return PosixStat{ + .dev = @intCast(stat_.dev), + .ino = @intCast(stat_.ino), + .mode = @intCast(stat_.mode), + .nlink = @intCast(stat_.nlink), + .uid = @intCast(stat_.uid), + .gid = @intCast(stat_.gid), + .rdev = @intCast(stat_.rdev), + .size = @intCast(stat_.size), + .blksize = @intCast(stat_.blksize), + .blocks = @intCast(stat_.blocks), + .atim = .{ .sec = atime_val.sec, .nsec = atime_val.nsec }, + .mtim = .{ .sec = mtime_val.sec, .nsec = mtime_val.nsec }, + .ctim = .{ .sec = ctime_val.sec, .nsec = ctime_val.nsec }, + .birthtim = .{ .sec = birthtime_val.sec, .nsec = birthtime_val.nsec }, + }; + } else { + // POSIX (Linux/macOS): use accessor methods and cast types + const atime_val = stat_.atime(); + const mtime_val = stat_.mtime(); + const ctime_val = stat_.ctime(); + const birthtime_val = if (Environment.isLinux) + bun.timespec.epoch + else + stat_.birthtime(); + + return PosixStat{ + .dev = @intCast(stat_.dev), + .ino = @intCast(stat_.ino), + .mode = @intCast(stat_.mode), + .nlink = @intCast(stat_.nlink), + .uid = @intCast(stat_.uid), + .gid = @intCast(stat_.gid), + .rdev = @intCast(stat_.rdev), + .size = @intCast(stat_.size), + .blksize = @intCast(stat_.blksize), + .blocks = @intCast(stat_.blocks), + .atim = .{ .sec = atime_val.sec, .nsec = atime_val.nsec }, + .mtim = .{ .sec = mtime_val.sec, .nsec = mtime_val.nsec }, + .ctim = .{ .sec = ctime_val.sec, .nsec = ctime_val.nsec }, + .birthtim = .{ .sec = birthtime_val.sec, .nsec = birthtime_val.nsec }, + }; + } + } + + pub fn atime(self: *const PosixStat) bun.timespec { + return self.atim; + } + + pub fn mtime(self: *const PosixStat) bun.timespec { + return self.mtim; + } + + pub fn ctime(self: *const PosixStat) bun.timespec { + return self.ctim; + } + + pub fn birthtime(self: *const PosixStat) bun.timespec { + return self.birthtim; + } +}; + +const bun = @import("bun"); +const Environment = bun.Environment; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 4e01d9bb5f..48f548c1f0 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -7,6 +7,7 @@ comptime { pub const log = bun.sys.syslog; pub const Error = bun.sys.Error; +pub const PosixStat = bun.sys.PosixStat; // libuv dont support openat (https://github.com/libuv/libuv/issues/4167) pub const openat = bun.sys.openat; diff --git a/test/js/bun/util/bun-file.test.ts b/test/js/bun/util/bun-file.test.ts index 48d543f58b..d4e8d5f1b3 100644 --- a/test/js/bun/util/bun-file.test.ts +++ b/test/js/bun/util/bun-file.test.ts @@ -15,7 +15,11 @@ test("delete() and stat() should work with unicode paths", async () => { expect(async () => { await Bun.file(filename).stat(); - }).toThrow(`ENOENT: no such file or directory, stat '${filename}'`); + }).toThrow( + process.platform === "linux" + ? `ENOENT: no such file or directory, statx '${filename}'` + : `ENOENT: no such file or directory, stat '${filename}'`, + ); await Bun.write(filename, "HI"); diff --git a/test/js/node/fs/fs-birthtime-linux.test.ts b/test/js/node/fs/fs-birthtime-linux.test.ts new file mode 100644 index 0000000000..e1cd536902 --- /dev/null +++ b/test/js/node/fs/fs-birthtime-linux.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "bun:test"; +import { isLinux, tempDirWithFiles } from "harness"; +import { chmodSync, closeSync, fstatSync, lstatSync, openSync, statSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +describe.skipIf(!isLinux)("birthtime", () => { + it("should return non-zero birthtime on Linux", () => { + const dir = tempDirWithFiles("birthtime-test", { + "test.txt": "initial content", + }); + + const filepath = join(dir, "test.txt"); + const stats = statSync(filepath); + + // On Linux with statx support, birthtime should be > 0 + expect(stats.birthtimeMs).toBeGreaterThan(0); + expect(stats.birthtime.getTime()).toBeGreaterThan(0); + expect(stats.birthtime.getFullYear()).toBeGreaterThanOrEqual(2025); + }); + + it("birthtime should remain constant while other timestamps change", () => { + const dir = tempDirWithFiles("birthtime-immutable", {}); + const filepath = join(dir, "immutable-test.txt"); + + // Create file and capture birthtime + writeFileSync(filepath, "original"); + const initialStats = statSync(filepath); + const birthtime = initialStats.birthtimeMs; + + expect(birthtime).toBeGreaterThan(0); + + // Wait a bit to ensure timestamps would differ + Bun.sleepSync(10); + + // Modify content (updates mtime and ctime) + writeFileSync(filepath, "modified"); + const afterModify = statSync(filepath); + + expect(afterModify.birthtimeMs).toBe(birthtime); + expect(afterModify.mtimeMs).toBeGreaterThan(initialStats.mtimeMs); + + // Wait again + Bun.sleepSync(10); + + // Change permissions (updates ctime) + chmodSync(filepath, 0o755); + const afterChmod = statSync(filepath); + + expect(afterChmod.birthtimeMs).toBe(birthtime); + expect(afterChmod.ctimeMs).toBeGreaterThan(afterModify.ctimeMs); + }); + + it("birthtime should work with lstat and fstat", () => { + const dir = tempDirWithFiles("birthtime-variants", { + "test.txt": "content", + }); + + const filepath = join(dir, "test.txt"); + + const statResult = statSync(filepath); + const lstatResult = lstatSync(filepath); + const fd = openSync(filepath, "r"); + const fstatResult = fstatSync(fd); + closeSync(fd); + + // All three should return the same birthtime + expect(statResult.birthtimeMs).toBeGreaterThan(0); + expect(lstatResult.birthtimeMs).toBe(statResult.birthtimeMs); + expect(fstatResult.birthtimeMs).toBe(statResult.birthtimeMs); + + expect(statResult.birthtime.getTime()).toBe(lstatResult.birthtime.getTime()); + expect(statResult.birthtime.getTime()).toBe(fstatResult.birthtime.getTime()); + }); + + it("birthtime should work with BigInt stats", () => { + const dir = tempDirWithFiles("birthtime-bigint", { + "test.txt": "content", + }); + + const filepath = join(dir, "test.txt"); + + const regularStats = statSync(filepath); + const bigintStats = statSync(filepath, { bigint: true }); + + expect(bigintStats.birthtimeMs).toBeGreaterThan(0n); + expect(bigintStats.birthtimeNs).toBeGreaterThan(0n); + + // birthtimeMs should be close (within rounding) + const regularMs = BigInt(Math.floor(regularStats.birthtimeMs)); + expect(bigintStats.birthtimeMs).toBe(regularMs); + + // birthtimeNs should have nanosecond precision + expect(bigintStats.birthtimeNs).toBeGreaterThanOrEqual(bigintStats.birthtimeMs * 1000000n); + }); + + it("birthtime should be less than or equal to all other timestamps on creation", () => { + const dir = tempDirWithFiles("birthtime-ordering", {}); + const filepath = join(dir, "new-file.txt"); + + writeFileSync(filepath, "new content"); + const stats = statSync(filepath); + + // birthtime should be <= all other times since it's when file was created + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.mtimeMs); + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.atimeMs); + expect(stats.birthtimeMs).toBeLessThanOrEqual(stats.ctimeMs); + }); +}); From 3c96c8a63d60777e523e7cd06e451ec148df88b5 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 05:25:30 -0700 Subject: [PATCH 027/391] Add Claude Code hooks to prevent common development mistakes (#23241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Added Claude Code hooks to prevent common development mistakes when working on the Bun codebase. ## Changes - Created `.claude/hooks/pre-bash-zig-build.js` - A pre-bash hook that validates commands - Created `.claude/settings.json` - Hook configuration ## Prevented Mistakes 1. **Running `zig build obj` directly** → Redirects to use `bun bd` 2. **Using `bun test` in development** → Must use `bun bd test` (or set `USE_SYSTEM_BUN=1`) 3. **Combining snapshot updates with test filters** → Prevents `-u`/`--update-snapshots` with `-t`/`--test-name-pattern` 4. **Running `bun bd` with timeout** → Build needs time to complete without timeout 5. **Running `bun bd test` from repo root** → Must specify a test file path to avoid running all tests ## Test plan - [x] Tested all validation rules with various command combinations - [x] Verified USE_SYSTEM_BUN=1 bypass works - [x] Verified file path detection works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- .claude/hooks/post-edit-zig-format.js | 90 +++++++++++++ .claude/hooks/pre-bash-zig-build.js | 175 ++++++++++++++++++++++++++ .claude/settings.json | 26 ++++ 3 files changed, 291 insertions(+) create mode 100755 .claude/hooks/post-edit-zig-format.js create mode 100755 .claude/hooks/pre-bash-zig-build.js create mode 100644 .claude/settings.json diff --git a/.claude/hooks/post-edit-zig-format.js b/.claude/hooks/post-edit-zig-format.js new file mode 100755 index 0000000000..3cf96837b9 --- /dev/null +++ b/.claude/hooks/post-edit-zig-format.js @@ -0,0 +1,90 @@ +#!/usr/bin/env bun +import { extname } from "path"; +import { spawnSync } from "child_process"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const filePath = toolInput.file_path; + +// Only process Write, Edit, and MultiEdit tools +if (!["Write", "Edit", "MultiEdit"].includes(toolName)) { + process.exit(0); +} + +const ext = extname(filePath); + +// Only format known files +if (!filePath) { + process.exit(0); +} + +function formatZigFile() { + try { + // Format the Zig file + const result = spawnSync("vendor/zig/zig.exe", ["fmt", filePath], { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }); + + if (result.error) { + console.error(`Failed to format ${filePath}: ${result.error.message}`); + process.exit(0); + } + + if (result.status !== 0) { + console.error(`zig fmt failed for ${filePath}:`); + if (result.stderr) { + console.error(result.stderr); + } + process.exit(0); + } + } catch (error) {} +} + +function formatTypeScriptFile() { + try { + // Format the TypeScript file + const result = spawnSync( + "./node_modules/.bin/prettier", + ["--plugin=prettier-plugin-organize-imports", "--config", ".prettierrc", "--write", filePath], + { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }, + ); + } catch (error) {} +} + +if (ext === ".zig") { + formatZigFile(); +} else if ( + [ + ".cjs", + ".css", + ".html", + ".js", + ".json", + ".jsonc", + ".jsx", + ".less", + ".mjs", + ".pcss", + ".postcss", + ".sass", + ".scss", + ".styl", + ".stylus", + ".toml", + ".ts", + ".tsx", + ".yaml", + ].includes(ext) +) { + formatTypeScriptFile(); +} else if (ext === ".cpp" || ext === ".c" || ext === ".h") { + formatCppFile(); +} + +process.exit(0); diff --git a/.claude/hooks/pre-bash-zig-build.js b/.claude/hooks/pre-bash-zig-build.js new file mode 100755 index 0000000000..24b47b06fc --- /dev/null +++ b/.claude/hooks/pre-bash-zig-build.js @@ -0,0 +1,175 @@ +#!/usr/bin/env bun +import { basename, extname } from "path"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const command = toolInput.command || ""; +const timeout = toolInput.timeout; +const cwd = input.cwd || ""; + +// Get environment variables from the hook context +// Note: We check process.env directly as env vars are inherited +let useSystemBun = process.env.USE_SYSTEM_BUN; + +if (toolName !== "Bash" || !command) { + process.exit(0); +} + +function denyWithReason(reason) { + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: reason, + }, + }; + console.log(JSON.stringify(output)); + process.exit(0); +} + +// Parse the command to extract argv0 and positional args +let tokens; +try { + // Simple shell parsing - split on spaces but respect quotes (both single and double) + tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map(t => t.replace(/^['"]|['"]$/g, "")) || []; +} catch { + process.exit(0); +} + +if (tokens.length === 0) { + process.exit(0); +} + +// Strip inline environment variable assignments (e.g., FOO=1 bun test) +const inlineEnv = new Map(); +let commandStart = 0; +while ( + commandStart < tokens.length && + /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[commandStart]) && + !tokens[commandStart].includes("/") +) { + const [name, value = ""] = tokens[commandStart].split("=", 2); + inlineEnv.set(name, value); + commandStart++; +} +if (commandStart >= tokens.length) { + process.exit(0); +} +tokens = tokens.slice(commandStart); +useSystemBun = inlineEnv.get("USE_SYSTEM_BUN") ?? useSystemBun; + +// Get the executable name (argv0) +const argv0 = basename(tokens[0], extname(tokens[0])); + +// Check if it's zig or zig.exe +if (argv0 === "zig") { + // Filter out flags (starting with -) to get positional arguments + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + + // Check if the positional args contain "build" followed by "obj" + if (positionalArgs.length >= 2 && positionalArgs[0] === "build" && positionalArgs[1] === "obj") { + denyWithReason("error: Use `bun bd` to build Bun and wait patiently"); + } +} + +// Check if argv0 is timeout and the command is "bun bd" +if (argv0 === "timeout") { + // Find the actual command after timeout and its arguments + const timeoutArgEndIndex = tokens.slice(1).findIndex(t => !t.startsWith("-") && !/^\d/.test(t)); + if (timeoutArgEndIndex === -1) { + process.exit(0); + } + + const actualCommandIndex = timeoutArgEndIndex + 1; + if (actualCommandIndex >= tokens.length) { + process.exit(0); + } + + const actualCommand = basename(tokens[actualCommandIndex]); + const restArgs = tokens.slice(actualCommandIndex + 1); + + // Check if it's "bun bd" or "bun-debug bd" without other positional args + if (actualCommand === "bun" || actualCommand.includes("bun-debug")) { + const positionalArgs = restArgs.filter(arg => !arg.startsWith("-")); + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } + } +} + +// Check if command is "bun .* test" or "bun-debug test" with -u/--update-snapshots AND -t/--test-name-pattern +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + + // Check if "test" is in positional args or "bd" followed by "test" + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + const hasTest = positionalArgs.includes("test") || (positionalArgs[0] === "bd" && positionalArgs[1] === "test"); + + if (hasTest) { + const hasUpdateSnapshots = allArgs.some(arg => arg === "-u" || arg === "--update-snapshots"); + const hasTestNamePattern = allArgs.some(arg => arg === "-t" || arg === "--test-name-pattern"); + + if (hasUpdateSnapshots && hasTestNamePattern) { + denyWithReason("error: Cannot use -u/--update-snapshots with -t/--test-name-pattern"); + } + } +} + +// Check if timeout option is set for "bun bd" command +if (timeout !== undefined && (argv0 === "bun" || argv0.includes("bun-debug"))) { + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } +} + +// Check if running "bun test " without USE_SYSTEM_BUN=1 +if ((argv0 === "bun" || argv0.includes("bun-debug")) && useSystemBun !== "1") { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "test" (not "bd test") + if (positionalArgs.length >= 1 && positionalArgs[0] === "test" && positionalArgs[0] !== "bd") { + denyWithReason( + "error: In development, use `bun bd test ` to test your changes. If you meant to use a release version, set USE_SYSTEM_BUN=1", + ); + } +} + +// Check if running "bun bd test" from bun repo root or test folder without a file path +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "bd test" + if (positionalArgs.length >= 2 && positionalArgs[0] === "bd" && positionalArgs[1] === "test") { + // Check if cwd is the bun repo root or test folder + const isBunRepoRoot = cwd === "/workspace/bun" || cwd.endsWith("/bun"); + const isTestFolder = cwd.endsWith("/bun/test"); + + if (isBunRepoRoot || isTestFolder) { + // Check if there's a file path argument (looks like a path: contains / or has test extension) + const hasFilePath = positionalArgs + .slice(2) + .some( + arg => + arg.includes("/") || + arg.endsWith(".test.ts") || + arg.endsWith(".test.js") || + arg.endsWith(".test.tsx") || + arg.endsWith(".test.jsx"), + ); + + if (!hasFilePath) { + denyWithReason( + "error: `bun bd test` from repo root or test folder will run all tests. Use `bun bd test ` with a specific test file.", + ); + } + } + } +} + +// Allow the command to proceed +process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..387633d5fd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash-zig-build.js" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-zig-format.js" + } + ] + } + ] + } +} From 13a3c4de607e84af4accc7a6b3d50a85415ac2bd Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 05:56:21 -0700 Subject: [PATCH 028/391] fix(install): fetch os/cpu metadata during yarn.lock migration (#23143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary During `yarn.lock` migration, OS/CPU package metadata was not being fetched from the npm registry when missing from `yarn.lock`. This caused packages with platform-specific requirements to not be properly marked, potentially leading to incorrect package installation behavior. ## Changes Updated `fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration` to conditionally fetch OS/CPU metadata: - **For yarn.lock migration**: Fetches OS/CPU metadata from npm registry when not present in yarn.lock (`update_os_cpu = true`) - **For pnpm-lock.yaml migration**: Skips OS/CPU fetching since pnpm-lock.yaml already includes this data (`update_os_cpu = false`) ### Files Modified - `src/install/lockfile.zig` - Added comptime `update_os_cpu` parameter and conditional logic to fetch OS/CPU metadata - `src/install/yarn.zig` - Pass `true` to enable OS/CPU fetching for yarn migrations - `src/install/pnpm.zig` - Pass `false` to skip OS/CPU fetching for pnpm migrations (already parsed from lockfile) ## Why This Approach - `yarn.lock` format often doesn't include OS/CPU constraints, requiring us to fetch from npm registry - `pnpm-lock.yaml` already parses OS/CPU during migration (lines 618-621 in pnpm.zig), making additional fetching redundant - Using a comptime parameter allows the compiler to optimize away the unused code path ## Testing - ✅ Debug build compiles successfully - Tested that the function correctly updates `pkg_meta.os` and `pkg_meta.arch` only when: - `update_os_cpu` is `true` (yarn migration) - Current values are `.all` (not already set) - Package metadata is available from npm registry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Dylan Conway Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/lockfile.zig | 101 ++++++++++++----- src/install/pnpm.zig | 2 +- src/install/yarn.zig | 2 +- .../yarn-lock-migration.test.ts.snap | 37 +++++- .../migration/yarn-lock-migration.test.ts | 107 ++++++++++++++++++ 5 files changed, 217 insertions(+), 32 deletions(-) diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index ee541146b4..60f0837180 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -951,7 +951,7 @@ const PendingResolution = struct { const PendingResolutions = std.ArrayList(PendingResolution); -pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, manager: *PackageManager) OOM!void { +pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, manager: *PackageManager, comptime update_os_cpu: bool) OOM!void { manager.populateManifestCache(.all) catch return; const pkgs = this.packages.slice(); @@ -960,40 +960,87 @@ pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, ma const pkg_name_hashes = pkgs.items(.name_hash); const pkg_resolutions = pkgs.items(.resolution); const pkg_bins = pkgs.items(.bin); + const pkg_metas = if (update_os_cpu) pkgs.items(.meta) else undefined; - for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin| { - switch (pkg_res.tag) { - .npm => { - const manifest = manager.manifests.byNameHash( - manager, - manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), - pkg_name_hash, - .load_from_memory_fallback_to_disk, - ) orelse { - continue; - }; + if (update_os_cpu) { + for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins, pkg_metas) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin, *pkg_meta| { + switch (pkg_res.tag) { + .npm => { + const manifest = manager.manifests.byNameHash( + manager, + manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), + pkg_name_hash, + .load_from_memory_fallback_to_disk, + ) orelse { + continue; + }; - const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { - continue; - }; + const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { + continue; + }; - var builder = manager.lockfile.stringBuilder(); + var builder = manager.lockfile.stringBuilder(); - var bin_extern_strings_count: u32 = 0; + var bin_extern_strings_count: u32 = 0; - bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); + bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); - try builder.allocate(); - defer builder.clamp(); + try builder.allocate(); + defer builder.clamp(); - var extern_strings_list = &manager.lockfile.buffers.extern_strings; - try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); - extern_strings_list.items.len += bin_extern_strings_count; - const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; + var extern_strings_list = &manager.lockfile.buffers.extern_strings; + try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); + extern_strings_list.items.len += bin_extern_strings_count; + const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; - pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); - }, - else => {}, + pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); + + // Update os/cpu metadata if not already set + if (pkg_meta.os == .all) { + pkg_meta.os = pkg.package.os; + } + if (pkg_meta.arch == .all) { + pkg_meta.arch = pkg.package.cpu; + } + }, + else => {}, + } + } + } else { + for (pkg_names, pkg_name_hashes, pkg_resolutions, pkg_bins) |pkg_name, pkg_name_hash, pkg_res, *pkg_bin| { + switch (pkg_res.tag) { + .npm => { + const manifest = manager.manifests.byNameHash( + manager, + manager.scopeForPackageName(pkg_name.slice(this.buffers.string_bytes.items)), + pkg_name_hash, + .load_from_memory_fallback_to_disk, + ) orelse { + continue; + }; + + const pkg = manifest.findByVersion(pkg_res.value.npm.version) orelse { + continue; + }; + + var builder = manager.lockfile.stringBuilder(); + + var bin_extern_strings_count: u32 = 0; + + bin_extern_strings_count += pkg.package.bin.count(manifest.string_buf, manifest.extern_strings_bin_entries, @TypeOf(&builder), &builder); + + try builder.allocate(); + defer builder.clamp(); + + var extern_strings_list = &manager.lockfile.buffers.extern_strings; + try extern_strings_list.ensureUnusedCapacity(manager.lockfile.allocator, bin_extern_strings_count); + extern_strings_list.items.len += bin_extern_strings_count; + const extern_strings = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_count ..]; + + pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder); + }, + else => {}, + } } } } diff --git a/src/install/pnpm.zig b/src/install/pnpm.zig index 33e733a6ad..e1b7cf0e8d 100644 --- a/src/install/pnpm.zig +++ b/src/install/pnpm.zig @@ -824,7 +824,7 @@ pub fn migratePnpmLockfile( try lockfile.resolve(log); - try lockfile.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager); + try lockfile.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager, false); try updatePackageJsonAfterMigration(allocator, manager, log, dir, found_patches); diff --git a/src/install/yarn.zig b/src/install/yarn.zig index 21af400e75..23e71140d5 100644 --- a/src/install/yarn.zig +++ b/src/install/yarn.zig @@ -1671,7 +1671,7 @@ pub fn migrateYarnLockfile( try this.resolve(log); - try this.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager); + try this.fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(manager, true); if (Environment.allow_assert) { try this.verifyData(); diff --git a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap index 3cbbd10486..3d4499931d 100644 --- a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap +++ b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap @@ -79,7 +79,7 @@ exports[`yarn.lock migration basic complex yarn.lock with multiple dependencies "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "fsevents": ["fsevents@2.3.3", "", {}, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "get-intrinsic": ["get-intrinsic@1.2.2", "", { "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA=="], @@ -290,7 +290,7 @@ exports[`yarn.lock migration basic migration with realistic complex yarn.lock: c "eslint": ["eslint@8.35.0", "", { "dependencies": { "@eslint/eslintrc": "^2.0.0", "@eslint/js": "8.35.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", "espree": "^9.4.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw=="], - "fsevents": ["fsevents@2.3.2", "", {}, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1176,7 +1176,7 @@ exports[`bun pm migrate for existing yarn.lock yarn-cli-repo: yarn-cli-repo 1`] "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="], - "fsevents": ["fsevents@1.2.4", "", { "dependencies": { "nan": "^2.9.2", "node-pre-gyp": "^0.10.0" } }, "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg=="], + "fsevents": ["fsevents@1.2.4", "", { "dependencies": { "nan": "^2.9.2", "node-pre-gyp": "^0.10.0" }, "os": "darwin" }, "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg=="], "function-bind": ["function-bind@1.1.1", "", {}, "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="], @@ -3161,3 +3161,34 @@ exports[`bun pm migrate for existing yarn.lock yarn-stuff/abbrev-link-target: ya } " `; + +exports[`bun pm migrate for existing yarn.lock yarn.lock with packages that have os/cpu requirements: os-cpu-yarn-migration 1`] = ` +"{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "os-cpu-test", + "dependencies": { + "fsevents": "^2.3.2", + "esbuild": "^0.17.0", + }, + }, + }, + "packages": { + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Mj/VEUqd5+2h0EBPdMzNdGXnGxbLPg6H5TF8xsHY4X5UAP0FUbDKJhtKu+6iLpIjKjWEvb5XrFyZdVy9OTg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + } +} +" +`; diff --git a/test/cli/install/migration/yarn-lock-migration.test.ts b/test/cli/install/migration/yarn-lock-migration.test.ts index 54329d56ec..da3fbd786e 100644 --- a/test/cli/install/migration/yarn-lock-migration.test.ts +++ b/test/cli/install/migration/yarn-lock-migration.test.ts @@ -1372,4 +1372,111 @@ describe("bun pm migrate for existing yarn.lock", () => { const bunLockContent = await Bun.file(join(tempDir, "bun.lock")).text(); expect(bunLockContent).toMatchSnapshot(folder); }); + + test("yarn.lock with packages that have os/cpu requirements", async () => { + const tempDir = tempDirWithFiles("yarn-migration-os-cpu", { + "package.json": JSON.stringify( + { + name: "os-cpu-test", + version: "1.0.0", + dependencies: { + fsevents: "^2.3.2", + esbuild: "^0.17.0", + }, + }, + null, + 2, + ), + "yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/android-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" + integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== + +"@esbuild/darwin-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" + integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== + +"@esbuild/darwin-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" + integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== + +"@esbuild/linux-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" + integrity sha512-ct1Mj/VEUqd5+2h0EBPdMzNdGXnGxbLPg6H5TF8xsHY4X5UAP0FUbDKJhtKu+6iLpIjKjWEvb5XrFyZdVy9OTg== + +"@esbuild/linux-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" + integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== + +esbuild@^0.17.0: + version "0.17.19" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" + integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== + optionalDependencies: + "@esbuild/android-arm" "0.17.19" + "@esbuild/android-arm64" "0.17.19" + "@esbuild/android-x64" "0.17.19" + "@esbuild/darwin-arm64" "0.17.19" + "@esbuild/darwin-x64" "0.17.19" + "@esbuild/freebsd-arm64" "0.17.19" + "@esbuild/freebsd-x64" "0.17.19" + "@esbuild/linux-arm" "0.17.19" + "@esbuild/linux-arm64" "0.17.19" + "@esbuild/linux-ia32" "0.17.19" + "@esbuild/linux-loong64" "0.17.19" + "@esbuild/linux-mips64el" "0.17.19" + "@esbuild/linux-ppc64" "0.17.19" + "@esbuild/linux-riscv64" "0.17.19" + "@esbuild/linux-s390x" "0.17.19" + "@esbuild/linux-x64" "0.17.19" + "@esbuild/netbsd-x64" "0.17.19" + "@esbuild/openbsd-x64" "0.17.19" + "@esbuild/sunos-x64" "0.17.19" + "@esbuild/win32-arm64" "0.17.19" + "@esbuild/win32-ia32" "0.17.19" + "@esbuild/win32-x64" "0.17.19" + +fsevents@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +`, + }); + + // Run bun pm migrate + const migrateResult = await Bun.spawn({ + cmd: [bunExe(), "pm", "migrate", "-f"], + cwd: tempDir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(migrateResult.stdout).text(), + new Response(migrateResult.stderr).text(), + migrateResult.exited, + ]); + + expect(exitCode).toBe(0); + expect(fs.existsSync(join(tempDir, "bun.lock"))).toBe(true); + + const bunLockContent = fs.readFileSync(join(tempDir, "bun.lock"), "utf8"); + expect(bunLockContent).toMatchSnapshot("os-cpu-yarn-migration"); + + // Verify that the lockfile contains the expected os/cpu metadata by checking the snapshot + // fsevents should have darwin os constraint, esbuild packages should have arch constraints + expect(bunLockContent).toContain("fsevents"); + expect(bunLockContent).toContain("@esbuild/linux-arm64"); + expect(bunLockContent).toContain("@esbuild/darwin-arm64"); + }); }); From db37c36d31e7bdcf8e369538c0d672d28faf5030 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 06:07:38 -0700 Subject: [PATCH 029/391] Update post-edit-zig-format.js --- .claude/hooks/post-edit-zig-format.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/.claude/hooks/post-edit-zig-format.js b/.claude/hooks/post-edit-zig-format.js index 3cf96837b9..e70d5eb99e 100755 --- a/.claude/hooks/post-edit-zig-format.js +++ b/.claude/hooks/post-edit-zig-format.js @@ -83,8 +83,6 @@ if (ext === ".zig") { ].includes(ext) ) { formatTypeScriptFile(); -} else if (ext === ".cpp" || ext === ".c" || ext === ".h") { - formatCppFile(); } process.exit(0); From 624911180f36cad33ed44f7d970ad20b85bede48 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 4 Oct 2025 06:51:21 -0700 Subject: [PATCH 030/391] fix(outdated): show catalog info without requiring --filter or -r (#23039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The `bun outdated` command now displays catalog dependencies with their workspace grouping even when run without the `--filter` or `-r` flags. ## What changed - Added detection for catalog dependencies in the outdated packages list - The workspace column is now shown when: - Using `--filter` or `-r` flags (existing behavior) - OR when there are catalog dependencies to display (new behavior) - When there are no catalog dependencies and no filtering, the workspace column remains hidden as before ## Why Previously, running `bun outdated` without any flags would not show which workspaces were using catalog dependencies, making it unclear where catalog entries were being used. This fix ensures catalog dependencies are properly grouped and displayed with their workspace information. ## Test ```bash # Create a workspace project with catalog dependencies mkdir test-catalog && cd test-catalog cat > package.json << 'JSON' { "name": "test-catalog", "workspaces": ["packages/*"], "catalog": { "react": "^17.0.0" } } JSON mkdir -p packages/{app1,app2} echo '{"name":"app1","dependencies":{"react":"catalog:"}}' > packages/app1/package.json echo '{"name":"app2","dependencies":{"react":"catalog:"}}' > packages/app2/package.json bun install bun outdated # Should now show catalog grouping without needing --filter ``` 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/cli/outdated_command.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 91f5cd64e7..4b722051fa 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -484,12 +484,17 @@ pub const OutdatedCommand = struct { // Recalculate max workspace length after grouping var new_max_workspace: usize = max_workspace; + var has_catalog_deps = false; for (grouped_ids.items) |item| { if (item.grouped_workspace_names) |names| { if (names.len > new_max_workspace) new_max_workspace = names.len; + has_catalog_deps = true; } } + // Show workspace column if filtered OR if there are catalog dependencies + const show_workspace_column = was_filtered or has_catalog_deps; + const package_column_inside_length = @max("Packages".len, max_name); const current_column_inside_length = @max("Current".len, max_current); const update_column_inside_length = @max("Update".len, max_update); @@ -500,7 +505,7 @@ pub const OutdatedCommand = struct { const column_right_pad = 1; const table = Table("blue", column_left_pad, column_right_pad, enable_ansi_colors).init( - &if (was_filtered) + &if (show_workspace_column) [_][]const u8{ "Package", "Current", @@ -515,7 +520,7 @@ pub const OutdatedCommand = struct { "Update", "Latest", }, - &if (was_filtered) + &if (show_workspace_column) [_]usize{ package_column_inside_length, current_column_inside_length, @@ -621,7 +626,7 @@ pub const OutdatedCommand = struct { version_buf.clearRetainingCapacity(); } - if (was_filtered) { + if (show_workspace_column) { Output.pretty("{s}", .{table.symbols.verticalEdge()}); for (0..column_left_pad) |_| Output.pretty(" ", .{}); From f0eb0472e6e43c4bd07738b762c8866814db4bec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 4 Oct 2025 06:52:20 -0700 Subject: [PATCH 031/391] Allow `--splitting` and `--compile` together (#23017) ### What does this PR do? ### How did you verify your code works? --- src/StandaloneModuleGraph.zig | 21 ++++++++++ src/bake/DevServer.zig | 1 + src/bundler/Chunk.zig | 13 ++++-- .../generateChunksInParallel.zig | 1 + .../linker_context/writeOutputFilesToDisk.zig | 1 + src/cli/build_command.zig | 6 --- .../bundler/bundler_compile_splitting.test.ts | 40 +++++++++++++++++++ test/bundler/expectBundled.ts | 28 ++++++++----- 8 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 test/bundler/bundler_compile_splitting.test.ts diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 0da83a40e6..9b045a1fad 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -431,6 +431,27 @@ pub const StandaloneModuleGraph = struct { } }; + if (comptime bun.Environment.is_canary or bun.Environment.isDebug) { + if (bun.getenvZ("BUN_FEATURE_FLAG_DUMP_CODE")) |dump_code_dir| { + const buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(buf); + const dest_z = bun.path.joinAbsStringBufZ(dump_code_dir, buf, &.{dest_path}, .auto); + + // Scoped block to handle dump failures without skipping module emission + dump: { + const file = bun.sys.File.makeOpen(dest_z, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664).unwrap() catch |err| { + Output.prettyErrorln("error: failed to open {s}: {s}", .{ dest_path, @errorName(err) }); + break :dump; + }; + defer file.close(); + file.writeAll(output_file.value.buffer.bytes).unwrap() catch |err| { + Output.prettyErrorln("error: failed to write {s}: {s}", .{ dest_path, @errorName(err) }); + break :dump; + }; + } + } + } + var module = CompiledModuleGraphFile{ .name = string_builder.fmtAppendCountZ("{s}{s}", .{ prefix, diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index e45dc06831..9264eeafa6 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2334,6 +2334,7 @@ pub fn finalizeBundle( result.chunks, null, false, + false, ); // Create an entry for this file. diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig index ac17d003f3..cdf6eec74c 100644 --- a/src/bundler/Chunk.zig +++ b/src/bundler/Chunk.zig @@ -142,6 +142,7 @@ pub const Chunk = struct { chunk: *Chunk, chunks: []Chunk, display_size: ?*usize, + force_absolute_path: bool, enable_source_map_shifts: bool, ) bun.OOM!CodeResult { return switch (enable_source_map_shifts) { @@ -153,6 +154,7 @@ pub const Chunk = struct { chunk, chunks, display_size, + force_absolute_path, source_map_shifts, ), }; @@ -167,10 +169,13 @@ pub const Chunk = struct { chunk: *Chunk, chunks: []Chunk, display_size: ?*usize, + force_absolute_path: bool, comptime enable_source_map_shifts: bool, ) bun.OOM!CodeResult { const additional_files = graph.input_files.items(.additional_files); const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file); + const relative_platform_buf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(relative_platform_buf); switch (this.*) { .pieces => |*pieces| { const entry_point_chunks_for_scb = linker_graph.files.items(.entry_point_chunk_index); @@ -224,10 +229,10 @@ pub const Chunk = struct { const cheap_normalizer = cheapPrefixNormalizer( import_prefix, - if (from_chunk_dir.len == 0) + if (from_chunk_dir.len == 0 or force_absolute_path) file_path else - bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false), ); count += cheap_normalizer[0].len + cheap_normalizer[1].len; }, @@ -316,10 +321,10 @@ pub const Chunk = struct { bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, - if (from_chunk_dir.len == 0) + if (from_chunk_dir.len == 0 or force_absolute_path) file_path else - bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 1cc1a05bf1..dcb091fb0c 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -340,6 +340,7 @@ pub fn generateChunksInParallel( chunk, chunks, &display_size, + c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build, chunk.content.sourcemap(c.options.source_maps) != .none, ); var code_result = _code_result catch @panic("Failed to allocate memory for output file"); diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index 2592cb57af..e49fd8c7e1 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -73,6 +73,7 @@ pub fn writeOutputFilesToDisk( chunk, chunks, &display_size, + c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build, chunk.content.sourcemap(c.options.source_maps) != .none, ) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)}); diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 6637e7007f..5590ef1581 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -96,12 +96,6 @@ pub const BuildCommand = struct { var was_renamed_from_index = false; if (ctx.bundler_options.compile) { - if (ctx.bundler_options.code_splitting) { - Output.prettyErrorln("error: cannot use --compile with --splitting", .{}); - Global.exit(1); - return; - } - if (ctx.bundler_options.outdir.len > 0) { Output.prettyErrorln("error: cannot use --compile with --outdir", .{}); Global.exit(1); diff --git a/test/bundler/bundler_compile_splitting.test.ts b/test/bundler/bundler_compile_splitting.test.ts new file mode 100644 index 0000000000..f80d0bfdc9 --- /dev/null +++ b/test/bundler/bundler_compile_splitting.test.ts @@ -0,0 +1,40 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + describe("compile with splitting", () => { + itBundled("compile/splitting/RelativePathsAcrossChunks", { + compile: true, + splitting: true, + backend: "cli", + files: { + "/src/app/entry.ts": /* js */ ` + console.log('app entry'); + import('../components/header').then(m => m.render()); + `, + "/src/components/header.ts": /* js */ ` + export async function render() { + console.log('header rendering'); + const nav = await import('./nav/menu'); + nav.show(); + } + `, + "/src/components/nav/menu.ts": /* js */ ` + export async function show() { + console.log('menu showing'); + const items = await import('./items'); + console.log('items:', items.list); + } + `, + "/src/components/nav/items.ts": /* js */ ` + export const list = ['home', 'about', 'contact'].join(','); + `, + }, + entryPoints: ["/src/app/entry.ts"], + outdir: "/build", + run: { + stdout: "app entry\nheader rendering\nmenu showing\nitems: home,about,contact", + }, + }); + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index a45de6d53a..972f5feff8 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -524,7 +524,15 @@ function expectBundled( if (metafile === true) metafile = "/metafile.json"; if (bundleErrors === true) bundleErrors = {}; if (bundleWarnings === true) bundleWarnings = {}; - const useOutFile = generateOutput == false ? false : outfile ? true : outdir ? false : entryPoints.length === 1; + const useOutFile = compile + ? true + : generateOutput == false + ? false + : outfile + ? true + : outdir + ? false + : entryPoints.length === 1; if (bundling === false && entryPoints.length > 1) { throw new Error("bundling:false only supports a single entry point"); @@ -1087,14 +1095,16 @@ function expectBundled( define: define ?? {}, throw: _throw ?? false, compile, - jsx: jsx ? { - runtime: jsx.runtime, - importSource: jsx.importSource, - factory: jsx.factory, - fragment: jsx.fragment, - sideEffects: jsx.sideEffects, - development: jsx.development, - } : undefined, + jsx: jsx + ? { + runtime: jsx.runtime, + importSource: jsx.importSource, + factory: jsx.factory, + fragment: jsx.fragment, + sideEffects: jsx.sideEffects, + development: jsx.development, + } + : undefined, } as BuildConfig; if (dotenv) { From 83060e4b3ed89645eac4c28339e4c5883925a2bf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 5 Oct 2025 04:28:25 -0700 Subject: [PATCH 032/391] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b0c2fa643d..4b95245f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude/settings.local.json .DS_Store .env .envrc @@ -190,4 +191,4 @@ scratch*.{js,ts,tsx,cjs,mjs} scripts/lldb-inline # We regenerate these in all the build scripts -cmake/sources/*.txt \ No newline at end of file +cmake/sources/*.txt From 67647c35220c56df1449e4a310d9b7e1ba071aed Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 5 Oct 2025 05:07:59 -0700 Subject: [PATCH 033/391] test(valkey): Improvements to valkey IPC interlock (#23252) ### What does this PR do? Adds a stronger IPC interlock in the failing subscriber test. ### How did you verify your code works? Hopefully CI. --- test/js/valkey/test-utils.ts | 34 ++++++++++++++------- test/js/valkey/valkey.failing-subscriber.ts | 4 ++- test/js/valkey/valkey.test.ts | 31 ++++++++++++++++--- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/test/js/valkey/test-utils.ts b/test/js/valkey/test-utils.ts index ee09d55884..e3443ea015 100644 --- a/test/js/valkey/test-utils.ts +++ b/test/js/valkey/test-utils.ts @@ -663,21 +663,33 @@ export function awaitableCounter(timeoutMs: number = 1000) { let activeResolvers: [number, NodeJS.Timeout, (value: number) => void][] = []; let currentCount = 0; - return { - increment: () => { - currentCount++; + const incrementBy = (count: number) => { + currentCount += count; - for (const [value, alarm, resolve] of activeResolvers) { - alarm.close(); + for (const [value, alarm, resolve] of activeResolvers) { + alarm.close(); - if (currentCount >= value) { - resolve(currentCount); - } + if (currentCount >= value) { + resolve(currentCount); } + } - // Remove resolved promises - activeResolvers = activeResolvers.filter(([value]) => currentCount < value); - }, + // Remove resolved promises + const remaining: typeof activeResolvers = []; + for (const [value, alarm, resolve] of activeResolvers) { + if (currentCount >= value) { + alarm.close(); + resolve(currentCount); + } else { + remaining.push([value, alarm, resolve]); + } + } + activeResolvers = remaining; + }; + + return { + incrementBy: incrementBy, + increment: incrementBy.bind(null, 1), count: () => currentCount, untilValue: (value: number) => diff --git a/test/js/valkey/valkey.failing-subscriber.ts b/test/js/valkey/valkey.failing-subscriber.ts index 379718e43c..461a059dcf 100644 --- a/test/js/valkey/valkey.failing-subscriber.ts +++ b/test/js/valkey/valkey.failing-subscriber.ts @@ -33,6 +33,7 @@ process.on("message", (msg: any) => { const CHANNEL = "error-callback-channel"; // We will wait for the parent process to tell us to start. +trySend({ event: "waiting-for-url" }); const { url, tlsPaths } = await redisUrl; const subscriber = new RedisClient(url, { tls: tlsPaths @@ -44,7 +45,6 @@ const subscriber = new RedisClient(url, { : undefined, }); await subscriber.connect(); -trySend({ event: "ready" }); let counter = 0; await subscriber.subscribe(CHANNEL, () => { @@ -58,3 +58,5 @@ await subscriber.subscribe(CHANNEL, () => { process.on("uncaughtException", e => { trySend({ event: "exception", exMsg: e.message }); }); + +trySend({ event: "ready" }); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 2c50270127..df4132a30c 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -6570,13 +6570,34 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ); }); + test("high volume pub/sub", async () => { + const channel = testChannel(); + + const MESSAGE_COUNT = 1000; + const MESSAGE_SIZE = 1024 * 1024; + + let byteCounter = awaitableCounter(5_000); // 5s timeout + const subscriber = await ctx.redis.duplicate(); + await subscriber.subscribe(channel, message => { + byteCounter.incrementBy(message.length); + }); + + for (let i = 0; i < MESSAGE_COUNT; i++) { + await ctx.redis.publish(channel, "X".repeat(MESSAGE_SIZE)); + } + + expect(await byteCounter.untilValue(MESSAGE_COUNT * MESSAGE_SIZE)).toBe(MESSAGE_COUNT * MESSAGE_SIZE); + subscriber.close(); + }); + test("callback errors don't crash the client", async () => { const channel = "error-callback-channel"; - const STEP_SUBSCRIBED = 1; - const STEP_FIRST_MESSAGE = 2; - const STEP_SECOND_MESSAGE = 3; - const STEP_THIRD_MESSAGE = 4; + const STEP_WAITING_FOR_URL = 1; + const STEP_SUBSCRIBED = 2; + const STEP_FIRST_MESSAGE = 3; + const STEP_SECOND_MESSAGE = 4; + const STEP_THIRD_MESSAGE = 5; const stepCounter = awaitableCounter(); let currentMessage: any = {}; @@ -6595,6 +6616,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }, }); + await stepCounter.untilValue(STEP_WAITING_FOR_URL); + expect(currentMessage.event).toBe("waiting-for-url"); subscriberProc.send({ event: "start", url: connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, From f0295ce0a55acc43d898b6769f1e3c14a793968c Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 5 Oct 2025 17:22:37 -0700 Subject: [PATCH 034/391] Fix bunfig.toml parsing with UTF-8 BOM (#23276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #23275 ### What does this PR do? This PR fixes a bug where `bunfig.toml` files starting with a UTF-8 BOM (byte order mark, `U+FEFF` or bytes `0xEF 0xBB 0xBF`) would fail to parse with an "Unexpected" error. The fix uses Bun's existing `File.toSource()` function with `convert_bom: true` option when loading config files. This properly detects and strips the BOM before parsing, matching the behavior of other file readers in Bun (like the JavaScript lexer which treats `0xFEFF` as whitespace). **Changes:** - Modified `src/cli/Arguments.zig` to use `bun.sys.File.toSource()` with BOM conversion instead of manually reading the file - Simplified the config loading code by removing intermediate file handle and buffer logic ### How did you verify your code works? Added comprehensive regression tests in `test/regression/issue/23275.test.ts` that verify: 1. ✅ `bunfig.toml` with UTF-8 BOM parses correctly without errors 2. ✅ `bunfig.toml` without BOM still works (regression test) 3. ✅ `bunfig.toml` with BOM and actual config content parses the content correctly All three tests pass with the debug build: ``` 3 pass 0 fail 11 expect() calls Ran 3 tests across 1 file. [6.41s] ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/cli/Arguments.zig | 18 ++---- test/internal/ban-limits.json | 2 +- test/regression/issue/23275.test.ts | 99 +++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 test/regression/issue/23275.test.ts diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 4b55ba7446..1d4bea63b8 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -212,11 +212,11 @@ pub const test_only_params = [_]ParamType{ pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_path: [:0]const u8, ctx: Command.Context, comptime cmd: Command.Tag) !void { - var config_file = switch (bun.sys.openA(config_path, bun.O.RDONLY, 0)) { - .result => |fd| fd.stdFile(), + const source = switch (bun.sys.File.toSource(config_path, allocator, .{ .convert_bom = true })) { + .result => |s| s, .err => |err| { if (auto_loaded) return; - Output.prettyErrorln("{}\nwhile opening config \"{s}\"", .{ + Output.prettyErrorln("{}\nwhile reading config \"{s}\"", .{ err, config_path, }); @@ -224,16 +224,6 @@ pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_pa }, }; - defer config_file.close(); - const contents = config_file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { - if (auto_loaded) return; - Output.prettyErrorln("error: {s} reading config \"{s}\"", .{ - @errorName(err), - config_path, - }); - Global.exit(1); - }; - js_ast.Stmt.Data.Store.create(); js_ast.Expr.Data.Store.create(); defer { @@ -245,7 +235,7 @@ pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_pa ctx.log.level = original_level; } ctx.log.level = logger.Log.Level.warn; - try Bunfig.parse(allocator, &logger.Source.initPathString(bun.asByteSlice(config_path), contents), ctx, cmd); + try Bunfig.parse(allocator, &source, ctx, cmd); } fn getHomeConfigPath(buf: *bun.PathBuffer) ?[:0]const u8 { diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index 39f2c58676..9102540fcb 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -8,7 +8,7 @@ ".jsBoolean(false)": 0, ".jsBoolean(true)": 0, ".stdDir()": 41, - ".stdFile()": 18, + ".stdFile()": 17, "// autofix": 167, ": [^=]+= undefined,$": 256, "== alloc.ptr": 0, diff --git a/test/regression/issue/23275.test.ts b/test/regression/issue/23275.test.ts new file mode 100644 index 0000000000..d6c5db1168 --- /dev/null +++ b/test/regression/issue/23275.test.ts @@ -0,0 +1,99 @@ +// https://github.com/oven-sh/bun/issues/23275 +// UTF-8 BOM in bunfig.toml should not cause parsing errors + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("bunfig.toml with UTF-8 BOM should parse correctly", async () => { + // UTF-8 BOM is the byte sequence: 0xEF 0xBB 0xBF + const utf8BOM = "\uFEFF"; + + using dir = tempDir("bunfig-bom", { + "bunfig.toml": + utf8BOM + + ` +[install] +exact = true +`, + "index.ts": `console.log("test");`, + "package.json": JSON.stringify({ + name: "test-bom", + version: "1.0.0", + }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should not have the "Unexpected" error that was reported in the issue + expect(stderr).not.toContain("Unexpected"); + expect(stderr).not.toContain("error:"); + expect(stdout).toContain("test"); + expect(exitCode).toBe(0); +}); + +test("bunfig.toml without BOM should still work", async () => { + using dir = tempDir("bunfig-no-bom", { + "bunfig.toml": ` +[install] +exact = true +`, + "index.ts": `console.log("test");`, + "package.json": JSON.stringify({ + name: "test-no-bom", + version: "1.0.0", + }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("Unexpected"); + expect(stderr).not.toContain("error:"); + expect(stdout).toContain("test"); + expect(exitCode).toBe(0); +}); + +test("bunfig.toml with BOM and actual content should parse the content correctly", async () => { + const utf8BOM = "\uFEFF"; + + using dir = tempDir("bunfig-bom-content", { + "bunfig.toml": + utf8BOM + + ` +logLevel = "debug" + +[install] +production = true +`, + "index.ts": `console.log("hello");`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("hello"); + expect(stderr).not.toContain("Unexpected"); + expect(exitCode).toBe(0); +}); From fcbd57ac48c157f43fa5216d484161a400a8ea2b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sun, 5 Oct 2025 17:23:59 -0700 Subject: [PATCH 035/391] Bring `Bun.YAML` to 90% passing yaml-test-suite (#23265) ### What does this PR do? Fixes bugs in the parser bringing it to 90% passing the official [yaml-test-suite](https://github.com/yaml/yaml-test-suite) (362/400 passing tests) Still missing from our parser: |- and |+ (about 5%), and cyclic references. Translates the yaml-test-suite to our tests. fixes #22659 fixes #22392 fixes #22286 ### How did you verify your code works? Added tests for yaml-test-suite and each of the linked issues --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/interchange/yaml.zig | 828 ++- .../import-attributes.test.ts | 2 +- .../bun/yaml/__snapshots__/yaml.test.ts.snap | 171 + test/js/bun/yaml/fixtures/AHatInTime.yaml | 167 + .../yaml/translate_yaml_test_suite_to_bun.py | 1049 +++ test/js/bun/yaml/yaml-test-suite.test.ts | 6405 +++++++++++++++++ test/js/bun/yaml/yaml.test.ts | 61 +- 7 files changed, 8438 insertions(+), 245 deletions(-) create mode 100644 test/js/bun/yaml/__snapshots__/yaml.test.ts.snap create mode 100644 test/js/bun/yaml/fixtures/AHatInTime.yaml create mode 100644 test/js/bun/yaml/translate_yaml_test_suite_to_bun.py create mode 100644 test/js/bun/yaml/yaml-test-suite.test.ts diff --git a/src/interchange/yaml.zig b/src/interchange/yaml.zig index b76a0af3a8..28f7de06fa 100644 --- a/src/interchange/yaml.zig +++ b/src/interchange/yaml.zig @@ -278,6 +278,8 @@ pub fn Parser(comptime enc: Encoding) type { context: Context.Stack, block_indents: Indent.Stack, + explicit_document_start_line: ?Line, + // anchors: Anchors, anchors: bun.StringHashMap(Expr), // aliases: PendingAliases, @@ -299,13 +301,12 @@ pub fn Parser(comptime enc: Encoding) type { stack_check: bun.StackCheck, - const Whitespace = struct { - pos: Pos, - unit: enc.unit(), - - pub const space: Whitespace = .{ .unit = ' ', .pos = .zero }; - pub const tab: Whitespace = .{ .unit = '\t', .pos = .zero }; - pub const newline: Whitespace = .{ .unit = '\n', .pos = .zero }; + const Whitespace = union(enum) { + source: struct { + pos: Pos, + unit: enc.unit(), + }, + new: enc.unit(), }; pub fn init(allocator: std.mem.Allocator, input: []const enc.unit()) @This() { @@ -320,6 +321,7 @@ pub fn Parser(comptime enc: Encoding) type { // .literal = null, .context = .init(allocator), .block_indents = .init(allocator), + .explicit_document_start_line = null, // .anchors = .{ .map = .init(allocator) }, .anchors = .init(allocator), // .aliases = .{ .list = .init(allocator) }, @@ -407,10 +409,10 @@ pub fn Parser(comptime enc: Encoding) type { try log.addError(source, e.pos.loc(), "Unexpected EOF"); }, .unexpected_token => |e| { - try log.addError(source, e.pos.loc(), "Expected token"); + try log.addError(source, e.pos.loc(), "Unexpected token"); }, .unexpected_character => |e| { - try log.addError(source, e.pos.loc(), "Expected character"); + try log.addError(source, e.pos.loc(), "Unexpected character"); }, .invalid_directive => |e| { try log.addError(source, e.pos.loc(), "Invalid directive"); @@ -486,6 +488,10 @@ pub fn Parser(comptime enc: Encoding) type { } }; + fn unexpectedToken() error{UnexpectedToken} { + return error.UnexpectedToken; + } + pub fn parse(self: *@This()) ParseError!Stream { try self.scan(.{ .first_scan = true }); @@ -693,31 +699,39 @@ pub fn Parser(comptime enc: Encoding) type { try self.scan(.{}); } + self.explicit_document_start_line = null; + if (self.token.data == .document_start) { + self.explicit_document_start_line = self.token.line; try self.scan(.{}); } else if (directives.items.len > 0) { // if there's directives they must end with '---' - return error.UnexpectedToken; + return unexpectedToken(); } const root = try self.parseNode(.{}); - // If document_start or document_end follows, consume it + // If document_start it needs to create a new document. + // If document_end, consume as many as possible. They should + // not create new documents. switch (self.token.data) { .eof => {}, - .document_start => { - try self.scan(.{}); - }, + .document_start => {}, .document_end => { const document_end_line = self.token.line; try self.scan(.{}); + // consume all bare documents + while (self.token.data == .document_end) { + try self.scan(.{}); + } + if (self.token.line == document_end_line) { - return error.UnexpectedToken; + return unexpectedToken(); } }, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -747,7 +761,7 @@ pub fn Parser(comptime enc: Encoding) type { } if (self.token.data != .collect_entry) { - return error.UnexpectedToken; + return unexpectedToken(); } try self.scan(.{}); @@ -803,7 +817,7 @@ pub fn Parser(comptime enc: Encoding) type { }, .mapping_value => {}, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -874,9 +888,6 @@ pub fn Parser(comptime enc: Encoding) type { const sequence_indent = self.token.indent; // const sequence_line = self.token.line; - // try self.context.set(.block_in); - // defer self.context.unset(.block_in); - try self.block_indents.push(sequence_indent); defer self.block_indents.pop(); @@ -934,6 +945,45 @@ pub fn Parser(comptime enc: Encoding) type { break :item try self.parseNode(.{}); }, + .tag, + .anchor, + => item: { + // consume anchor and/or tag, then decide if the next node + // should be parsed. + var has_tag: ?Token(enc) = null; + var has_anchor: ?Token(enc) = null; + + next: switch (self.token.data) { + .tag => { + if (has_tag != null) { + return unexpectedToken(); + } + has_tag = self.token; + + try self.scan(.{ .additional_parent_indent = entry_indent.add(1), .tag = self.token.data.tag }); + continue :next self.token.data; + }, + .anchor => |anchor| { + _ = anchor; + if (has_anchor != null) { + return unexpectedToken(); + } + has_anchor = self.token; + + const tag = if (has_tag) |tag| tag.data.tag else .none; + try self.scan(.{ .additional_parent_indent = entry_indent.add(1), .tag = tag }); + continue :next self.token.data; + }, + .sequence_entry => { + if (self.token.indent.isLessThanOrEqual(sequence_indent)) { + const tag = if (has_tag) |tag| tag.data.tag else .none; + break :item tag.resolveNull(entry_start.add(2).loc()); + } + break :item try self.parseNode(.{ .scanned_tag = has_tag, .scanned_anchor = has_anchor }); + }, + else => break :item try self.parseNode(.{ .scanned_tag = has_tag, .scanned_anchor = has_anchor }), + } + }, else => try self.parseNode(.{}), }; @@ -951,6 +1001,16 @@ pub fn Parser(comptime enc: Encoding) type { mapping_indent: Indent, mapping_line: Line, ) ParseError!Expr { + if (self.explicit_document_start_line) |explicit_document_start_line| { + if (mapping_line == explicit_document_start_line) { + // TODO: more specific error + return error.UnexpectedToken; + } + } + + try self.block_indents.push(mapping_indent); + defer self.block_indents.pop(); + var props: std.ArrayList(G.Property) = .init(self.allocator); { @@ -958,32 +1018,41 @@ pub fn Parser(comptime enc: Encoding) type { // defer self.context.unset(.block_in); // get the first value - try self.block_indents.push(mapping_indent); - defer self.block_indents.pop(); const mapping_value_start = self.token.start; const mapping_value_line = self.token.line; - try self.scan(.{}); - const value: Expr = switch (self.token.data) { - .sequence_entry => value: { - if (self.token.line == mapping_value_line) { - return error.UnexpectedToken; + // it's a !!set entry + .mapping_key => value: { + if (self.token.line == mapping_line) { + return unexpectedToken(); } - - if (self.token.indent.isLessThan(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } - - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + break :value .init(E.Null, .{}, mapping_value_start.loc()); }, else => value: { - if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } + try self.scan(.{}); - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + switch (self.token.data) { + .sequence_entry => { + if (self.token.line == mapping_value_line) { + return unexpectedToken(); + } + + if (self.token.indent.isLessThan(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + else => { + if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + } }, }; @@ -1028,14 +1097,17 @@ pub fn Parser(comptime enc: Encoding) type { try self.context.set(.block_in); defer self.context.unset(.block_in); + var previous_line = mapping_line; + while (switch (self.token.data) { .eof, .document_start, .document_end, => false, else => true, - } and self.token.indent == mapping_indent and self.token.line != mapping_line) { + } and self.token.indent == mapping_indent and self.token.line != previous_line) { const key_line = self.token.line; + previous_line = key_line; const explicit_key = self.token.data == .mapping_key; const key = try self.parseNode(.{ .current_mapping_indent = mapping_indent }); @@ -1051,44 +1123,53 @@ pub fn Parser(comptime enc: Encoding) type { }); continue; } - return error.UnexpectedToken; + return unexpectedToken(); }, .mapping_value => { if (key_line != self.token.line) { return error.MultilineImplicitKey; } }, + .mapping_key => {}, else => { - return error.UnexpectedToken; + return unexpectedToken(); }, } - try self.block_indents.push(mapping_indent); - defer self.block_indents.pop(); - const mapping_value_line = self.token.line; const mapping_value_start = self.token.start; - try self.scan(.{}); - const value: Expr = switch (self.token.data) { - .sequence_entry => value: { + // it's a !!set entry + .mapping_key => value: { if (self.token.line == key_line) { - return error.UnexpectedToken; + return unexpectedToken(); } - - if (self.token.indent.isLessThan(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } - - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + break :value .init(E.Null, .{}, mapping_value_start.loc()); }, else => value: { - if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { - break :value .init(E.Null, .{}, mapping_value_start.loc()); - } + try self.scan(.{}); - break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + switch (self.token.data) { + .sequence_entry => { + if (self.token.line == key_line) { + return unexpectedToken(); + } + + if (self.token.indent.isLessThan(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + else => { + if (self.token.line != mapping_value_line and self.token.indent.isLessThanOrEqual(mapping_indent)) { + break :value .init(E.Null, .{}, mapping_value_start.loc()); + } + + break :value try self.parseNode(.{ .current_mapping_indent = mapping_indent }); + }, + } }, }; @@ -1237,6 +1318,8 @@ pub fn Parser(comptime enc: Encoding) type { const ParseNodeOptions = struct { current_mapping_indent: ?Indent = null, explicit_mapping_key: bool = false, + scanned_tag: ?Token(enc) = null, + scanned_anchor: ?Token(enc) = null, }; fn parseNode(self: *@This(), opts: ParseNodeOptions) ParseError!Expr { @@ -1247,6 +1330,14 @@ pub fn Parser(comptime enc: Encoding) type { // c-ns-properties var node_props: NodeProperties = .{}; + if (opts.scanned_tag) |tag| { + try node_props.setTag(tag); + } + + if (opts.scanned_anchor) |anchor| { + try node_props.setAnchor(anchor); + } + const node: Expr = node: switch (self.token.data) { .eof, .document_start, @@ -1273,8 +1364,19 @@ pub fn Parser(comptime enc: Encoding) type { }, .alias => |alias| { - if (node_props.hasAnchorOrTag()) { - return error.UnexpectedToken; + const alias_start = self.token.start; + const alias_indent = self.token.indent; + const alias_line = self.token.line; + + if (node_props.has_anchor) |anchor| { + if (anchor.line == alias_line) { + return unexpectedToken(); + } + } + if (node_props.has_tag) |tag| { + if (tag.line == alias_line) { + return unexpectedToken(); + } } var copy = self.anchors.get(alias.slice(self.input)) orelse { @@ -1289,10 +1391,35 @@ pub fn Parser(comptime enc: Encoding) type { }; // update position from the anchor node to the alias node. - copy.loc = self.token.start.loc(); + copy.loc = alias_start.loc(); try self.scan(.{}); + if (self.token.data == .mapping_value) { + if (alias_line != self.token.line and !opts.explicit_mapping_key) { + return error.MultilineImplicitKey; + } + + if (self.context.get() == .flow_key) { + return copy; + } + + if (opts.current_mapping_indent) |current_mapping_indent| { + if (current_mapping_indent == alias_indent) { + return copy; + } + } + + const map = try self.parseBlockMapping( + copy, + alias_start, + alias_indent, + alias_line, + ); + + return map; + } + break :node copy; }, @@ -1346,17 +1473,17 @@ pub fn Parser(comptime enc: Encoding) type { if (node_props.hasAnchorOrTag()) { break :node .init(E.Null, .{}, self.pos.loc()); } - return error.UnexpectedToken; + return unexpectedToken(); }, .sequence_entry => { if (node_props.anchorLine()) |anchor_line| { if (anchor_line == self.token.line) { - return error.UnexpectedToken; + return unexpectedToken(); } } if (node_props.tagLine()) |tag_line| { if (tag_line == self.token.line) { - return error.UnexpectedToken; + return unexpectedToken(); } } @@ -1400,6 +1527,8 @@ pub fn Parser(comptime enc: Encoding) type { if (implicit_key_anchors.mapping_anchor) |mapping_anchor| { try self.anchors.put(mapping_anchor.slice(self.input), parent_map); } + + break :node parent_map; } break :node map; }, @@ -1411,7 +1540,7 @@ pub fn Parser(comptime enc: Encoding) type { // if (node_props.anchorLine()) |anchor_line| { // if (anchor_line == self.token.line) { - // return error.UnexpectedToken; + // return unexpectedToken(); // } // } @@ -1441,11 +1570,11 @@ pub fn Parser(comptime enc: Encoding) type { }, .mapping_value => { if (self.context.get() == .flow_key) { - return .init(E.Null, .{}, self.token.start.loc()); + break :node .init(E.Null, .{}, self.token.start.loc()); } if (opts.current_mapping_indent) |current_mapping_indent| { if (current_mapping_indent == self.token.indent) { - return .init(E.Null, .{}, self.token.start.loc()); + break :node .init(E.Null, .{}, self.token.start.loc()); } } const first_key: Expr = .init(E.Null, .{}, self.token.start.loc()); @@ -1461,7 +1590,7 @@ pub fn Parser(comptime enc: Encoding) type { const scalar_indent = self.token.indent; const scalar_line = self.token.line; - try self.scan(.{ .tag = node_props.tag() }); + try self.scan(.{ .tag = node_props.tag(), .outside_context = true }); if (self.token.data == .mapping_value) { // this might be the start of a new object with an implicit key @@ -1541,10 +1670,10 @@ pub fn Parser(comptime enc: Encoding) type { break :node scalar.data.toExpr(scalar_start, self.input); }, .directive => { - return error.UnexpectedToken; + return unexpectedToken(); }, .reserved => { - return error.UnexpectedToken; + return unexpectedToken(); }, }; @@ -1558,11 +1687,16 @@ pub fn Parser(comptime enc: Encoding) type { return error.MultipleTags; } + const resolved = switch (node.data) { + .e_null => node_props.tag().resolveNull(node.loc), + else => node, + }; + if (node_props.anchor()) |anchor| { - try self.anchors.put(anchor.slice(self.input), node); + try self.anchors.put(anchor.slice(self.input), resolved); } - return node; + return resolved; } fn next(self: *const @This()) enc.unit() { @@ -1688,11 +1822,36 @@ pub fn Parser(comptime enc: Encoding) type { try ctx.str_builder.appendSourceSlice(off, end); } + // may or may not contain whitespace + pub fn appendUnknownSourceSlice(ctx: *@This(), off: Pos, end: Pos) OOM!void { + for (off.cast()..end.cast()) |_pos| { + const pos: Pos = .from(_pos); + const unit = ctx.parser.input[pos.cast()]; + switch (unit) { + ' ', + '\t', + '\r', + '\n', + => { + try ctx.str_builder.appendSourceWhitespace(unit, pos); + }, + else => { + ctx.checkAppend(); + try ctx.str_builder.appendSource(unit, pos); + }, + } + } + } + pub fn append(ctx: *@This(), unit: enc.unit()) OOM!void { ctx.checkAppend(); try ctx.str_builder.append(unit); } + pub fn appendWhitespace(ctx: *@This(), unit: enc.unit()) OOM!void { + try ctx.str_builder.appendWhitespace(unit); + } + pub fn appendSlice(ctx: *@This(), str: []const enc.unit()) OOM!void { ctx.checkAppend(); try ctx.str_builder.appendSlice(str); @@ -1706,6 +1865,14 @@ pub fn Parser(comptime enc: Encoding) type { try ctx.str_builder.appendNTimes(unit, n); } + pub fn appendWhitespaceNTimes(ctx: *@This(), unit: enc.unit(), n: usize) OOM!void { + if (n == 0) { + return; + } + + try ctx.str_builder.appendWhitespaceNTimes(unit, n); + } + const Keywords = enum { null, Null, @@ -1798,7 +1965,7 @@ pub fn Parser(comptime enc: Encoding) type { pub fn tryResolveNumber( ctx: *@This(), parser: *Parser(enc), - first_char: enum { positive, negative, dot, none }, + first_char: enum { positive, negative, dot, other }, ) ResolveError!void { const nan = std.math.nan(f64); const inf = std.math.inf(f64); @@ -1913,7 +2080,7 @@ pub fn Parser(comptime enc: Encoding) type { } } }, - .none => {}, + .other => {}, } const start = parser.pos; @@ -1926,7 +2093,9 @@ pub fn Parser(comptime enc: Encoding) type { var @"-" = false; var hex = false; - parser.inc(1); + if (first_char != .negative and first_char != .positive) { + parser.inc(1); + } var first = true; @@ -1945,7 +2114,12 @@ pub fn Parser(comptime enc: Encoding) type { '\n', '\r', ':', - => break :end .{ parser.pos, true }, + => { + if (first and (first_char == .positive or first_char == .negative)) { + break :end .{ parser.pos, false }; + } + break :end .{ parser.pos, true }; + }, ',', ']', @@ -1993,6 +2167,7 @@ pub fn Parser(comptime enc: Encoding) type { 'e', 'E', => { + first = false; if (e) { hex = true; } @@ -2008,12 +2183,12 @@ pub fn Parser(comptime enc: Encoding) type { => |c| { hex = true; - defer first = false; if (first) { if (c == 'b' or c == 'B') { break :end .{ parser.pos, false }; } } + first = false; parser.inc(1); continue :end parser.next(); @@ -2076,7 +2251,7 @@ pub fn Parser(comptime enc: Encoding) type { }, }; - try ctx.appendSourceSlice(start, end); + try ctx.appendUnknownSourceSlice(start, end); if (!valid) { return; @@ -2165,7 +2340,7 @@ pub fn Parser(comptime enc: Encoding) type { }, else => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, } @@ -2181,13 +2356,41 @@ pub fn Parser(comptime enc: Encoding) type { return ctx.done(); } + switch (self.context.get()) { + .block_out, + .block_in, + .flow_in, + => {}, + .flow_key => { + switch (self.peek(1)) { + ',', + '[', + ']', + '{', + '}', + => { + return ctx.done(); + }, + else => {}, + } + }, + } + try ctx.appendSource(':', self.pos); self.inc(1); continue :next self.next(); }, '#' => { - if (self.pos == .zero or self.input[self.pos.sub(1).cast()] == ' ') { + const prev = self.input[self.pos.sub(1).cast()]; + if (self.pos == .zero or switch (prev) { + ' ', + '\t', + '\r', + '\n', + => true, + else => false, + }) { return ctx.done(); } @@ -2253,11 +2456,14 @@ pub fn Parser(comptime enc: Encoding) type { } } + // clear the leading whitespace before the newline. + ctx.parser.whitespace_buf.clearRetainingCapacity(); + if (lines == 0 and !self.isEof()) { - try ctx.append(' '); + try ctx.appendWhitespace(' '); } - try ctx.appendNTimes('\n', lines); + try ctx.appendWhitespaceNTimes('\n', lines); continue :next self.next(); }, @@ -2461,7 +2667,7 @@ pub fn Parser(comptime enc: Encoding) type { }, '0'...'9' => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, @@ -2479,7 +2685,7 @@ pub fn Parser(comptime enc: Encoding) type { }, else => { - try ctx.tryResolveNumber(self, .none); + try ctx.tryResolveNumber(self, .other); continue :next self.next(); }, } @@ -2507,6 +2713,12 @@ pub fn Parser(comptime enc: Encoding) type { var chomp: ?Chomp = null; next: switch (self.next()) { + 0 => { + return .{ + indent_indicator orelse .default, + chomp orelse .default, + }; + }, '1'...'9' => |digit| { if (indent_indicator != null) { return error.UnexpectedCharacter; @@ -2564,6 +2776,11 @@ pub fn Parser(comptime enc: Encoding) type { // the first newline is always excluded from a literal self.inc(1); + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } + return .{ indent_indicator orelse .default, chomp orelse .default, @@ -2582,10 +2799,102 @@ pub fn Parser(comptime enc: Encoding) type { }; fn scanAutoIndentedLiteralScalar(self: *@This(), chomp: Chomp, folded: bool, start: Pos, line: Line) ScanLiteralScalarError!Token(enc) { - var leading_newlines: usize = 0; - var text: std.ArrayList(enc.unit()) = .init(self.allocator); + const LiteralScalarCtx = struct { + chomp: Chomp, + leading_newlines: usize, + text: std.ArrayList(enc.unit()), + start: Pos, + content_indent: Indent, + previous_indent: Indent, + max_leading_indent: Indent, + line: Line, + folded: bool, - const content_indent: Indent, const first = next: switch (self.next()) { + pub fn done(ctx: *@This(), was_eof: bool) OOM!Token(enc) { + switch (ctx.chomp) { + .keep => { + if (was_eof) { + try ctx.text.appendNTimes('\n', ctx.leading_newlines + 1); + } else if (ctx.text.items.len != 0) { + try ctx.text.appendNTimes('\n', ctx.leading_newlines); + } + }, + .clip => { + if (was_eof or ctx.text.items.len != 0) { + try ctx.text.append('\n'); + } + }, + .strip => { + // no trailing newlines + }, + } + + return .scalar(.{ + .start = ctx.start, + .indent = ctx.content_indent, + .line = ctx.line, + .resolved = .{ + .data = .{ .string = .{ .list = ctx.text } }, + .multiline = true, + }, + }); + } + + const AppendError = OOM || error{UnexpectedCharacter}; + + pub fn append(ctx: *@This(), c: enc.unit()) AppendError!void { + if (ctx.text.items.len == 0) { + if (ctx.content_indent.isLessThan(ctx.max_leading_indent)) { + return error.UnexpectedCharacter; + } + } + switch (ctx.folded) { + true => { + switch (ctx.leading_newlines) { + 0 => { + try ctx.text.append(c); + }, + 1 => { + if (ctx.previous_indent == ctx.content_indent) { + try ctx.text.appendSlice(&.{ ' ', c }); + } else { + try ctx.text.appendSlice(&.{ '\n', c }); + } + ctx.leading_newlines = 0; + }, + else => { + // leading_newlines because -1 for '\n\n' and +1 for c + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines - 1); + ctx.text.appendAssumeCapacity(c); + ctx.leading_newlines = 0; + }, + } + }, + false => { + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.text.appendAssumeCapacity(c); + ctx.leading_newlines = 0; + }, + } + } + }; + + var ctx: LiteralScalarCtx = .{ + .chomp = chomp, + .text = .init(self.allocator), + .folded = folded, + .start = start, + .line = line, + + .leading_newlines = 0, + .content_indent = .none, + .previous_indent = .none, + .max_leading_indent = .none, + }; + + ctx.content_indent, const first = next: switch (self.next()) { 0 => { return .scalar(.{ .start = start, @@ -2607,7 +2916,11 @@ pub fn Parser(comptime enc: Encoding) type { '\n' => { self.newline(); self.inc(1); - leading_newlines += 1; + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } + ctx.leading_newlines += 1; continue :next self.next(); }, @@ -2619,6 +2932,10 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(1); } + if (ctx.max_leading_indent.isLessThan(indent)) { + ctx.max_leading_indent = indent; + } + self.line_indent = indent; continue :next self.next(); @@ -2629,30 +2946,11 @@ pub fn Parser(comptime enc: Encoding) type { }, }; - var previous_indent = content_indent; + ctx.previous_indent = ctx.content_indent; next: switch (first) { 0 => { - switch (chomp) { - .keep => { - try text.appendNTimes('\n', leading_newlines + 1); - }, - .clip => { - try text.append('\n'); - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); + return ctx.done(true); }, '\r' => { @@ -2662,7 +2960,7 @@ pub fn Parser(comptime enc: Encoding) type { continue :next '\n'; }, '\n' => { - leading_newlines += 1; + ctx.leading_newlines += 1; self.newline(); self.inc(1); newlines: switch (self.next()) { @@ -2673,44 +2971,47 @@ pub fn Parser(comptime enc: Encoding) type { continue :newlines '\n'; }, '\n' => { - leading_newlines += 1; + ctx.leading_newlines += 1; self.newline(); self.inc(1); + if (self.next() == '\t') { + // tab for indentation + return error.UnexpectedCharacter; + } continue :newlines self.next(); }, ' ' => { - var indent: Indent = .from(1); - self.inc(1); + var indent: Indent = .from(0); while (self.next() == ' ') { indent.inc(1); - if (content_indent.isLessThan(indent)) { + if (ctx.content_indent.isLessThan(indent)) { switch (folded) { true => { - switch (leading_newlines) { + switch (ctx.leading_newlines) { 0 => { - try text.append(' '); + try ctx.text.append(' '); }, else => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - text.appendAssumeCapacity(' '); - leading_newlines = 0; + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.text.appendAssumeCapacity(' '); + ctx.leading_newlines = 0; }, } }, else => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - leading_newlines = 0; - text.appendAssumeCapacity(' '); + try ctx.text.ensureUnusedCapacity(ctx.leading_newlines + 1); + ctx.text.appendNTimesAssumeCapacity('\n', ctx.leading_newlines); + ctx.leading_newlines = 0; + ctx.text.appendAssumeCapacity(' '); }, } } self.inc(1); } - if (content_indent.isLessThan(indent)) { - previous_indent = self.line_indent; + if (ctx.content_indent.isLessThan(indent)) { + ctx.previous_indent = self.line_indent; } self.line_indent = indent; @@ -2720,91 +3021,54 @@ pub fn Parser(comptime enc: Encoding) type { } }, + '-' => { + if (self.line_indent == .none and self.remainStartsWith("---") and self.isAnyOrEofAt(" \t\n\r", 3)) { + return ctx.done(false); + } + + if (self.block_indents.get()) |block_indent| { + if (self.line_indent.isLessThanOrEqual(block_indent)) { + return ctx.done(false); + } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); + } + + try ctx.append('-'); + + self.inc(1); + continue :next self.next(); + }, + + '.' => { + if (self.line_indent == .none and self.remainStartsWith("...") and self.isAnyOrEofAt(" \t\n\r", 3)) { + return ctx.done(false); + } + + if (self.block_indents.get()) |block_indent| { + if (self.line_indent.isLessThanOrEqual(block_indent)) { + return ctx.done(false); + } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); + } + + try ctx.append('.'); + + self.inc(1); + continue :next self.next(); + }, + else => |c| { if (self.block_indents.get()) |block_indent| { if (self.line_indent.isLessThanOrEqual(block_indent)) { - switch (chomp) { - .keep => { - if (text.items.len != 0) { - try text.appendNTimes('\n', leading_newlines); - } - }, - .clip => { - if (text.items.len != 0) { - try text.append('\n'); - } - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); - } else if (self.line_indent.isLessThan(content_indent)) { - switch (chomp) { - .keep => { - if (text.items.len != 0) { - try text.appendNTimes('\n', leading_newlines); - } - }, - .clip => { - if (text.items.len != 0) { - try text.append('\n'); - } - }, - .strip => { - // no trailing newlines - }, - } - return .scalar(.{ - .start = start, - .indent = content_indent, - .line = line, - .resolved = .{ - .data = .{ .string = .{ .list = text } }, - .multiline = true, - }, - }); + return ctx.done(false); } + } else if (self.line_indent.isLessThan(ctx.content_indent)) { + return ctx.done(false); } - switch (folded) { - true => { - switch (leading_newlines) { - 0 => { - try text.append(c); - }, - 1 => { - if (previous_indent == content_indent) { - try text.appendSlice(&.{ ' ', c }); - } else { - try text.appendSlice(&.{ '\n', c }); - } - leading_newlines = 0; - }, - else => { - // leading_newlines because -1 for '\n\n' and +1 for c - try text.ensureUnusedCapacity(leading_newlines); - text.appendNTimesAssumeCapacity('\n', leading_newlines - 1); - text.appendAssumeCapacity(c); - leading_newlines = 0; - }, - } - }, - false => { - try text.ensureUnusedCapacity(leading_newlines + 1); - text.appendNTimesAssumeCapacity('\n', leading_newlines); - text.appendAssumeCapacity(c); - leading_newlines = 0; - }, - } + try ctx.append(c); self.inc(1); continue :next self.next(); @@ -2948,26 +3212,22 @@ pub fn Parser(comptime enc: Encoding) type { const scalar_indent = self.line_indent; var text: std.ArrayList(enc.unit()) = .init(self.allocator); - var nl = false; - next: switch (self.next()) { 0 => return error.UnexpectedCharacter, '.' => { - if (nl and self.remainStartsWith("...") and self.isSWhiteOrBCharAt(3)) { + if (self.line_indent == .none and self.remainStartsWith("...") and self.isSWhiteOrBCharAt(3)) { return error.UnexpectedDocumentEnd; } - nl = false; try text.append('.'); self.inc(1); continue :next self.next(); }, '-' => { - if (nl and self.remainStartsWith("---") and self.isSWhiteOrBCharAt(3)) { + if (self.line_indent == .none and self.remainStartsWith("---") and self.isSWhiteOrBCharAt(3)) { return error.UnexpectedDocumentStart; } - nl = false; try text.append('-'); self.inc(1); continue :next self.next(); @@ -2988,14 +3248,12 @@ pub fn Parser(comptime enc: Encoding) type { return error.UnexpectedCharacter; } } - nl = true; continue :next self.next(); }, ' ', '\t', => { - nl = false; const off = self.pos; self.inc(1); self.skipSWhite(); @@ -3006,7 +3264,6 @@ pub fn Parser(comptime enc: Encoding) type { }, '"' => { - nl = false; self.inc(1); return .scalar(.{ .start = start, @@ -3023,7 +3280,6 @@ pub fn Parser(comptime enc: Encoding) type { }, '\\' => { - nl = false; self.inc(1); switch (self.next()) { '\r', @@ -3094,7 +3350,6 @@ pub fn Parser(comptime enc: Encoding) type { }, else => |c| { - nl = false; try text.append(c); self.inc(1); continue :next self.next(); @@ -3246,6 +3501,38 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(1); var range = self.stringRange(); try self.trySkipNsTagChars(); + + // s-separate + switch (self.next()) { + 0, + ' ', + '\t', + '\r', + '\n', + => {}, + + ',', + '[', + ']', + '{', + '}', + => { + switch (self.context.get()) { + .block_out, + .block_in, + => { + return error.UnexpectedCharacter; + }, + .flow_in, + .flow_key, + => {}, + } + }, + else => { + return error.UnexpectedCharacter; + }, + } + const shorthand = range.end(); const tag: NodeTag = tag: { @@ -3367,6 +3654,8 @@ pub fn Parser(comptime enc: Encoding) type { /// (or in compact collections). First scan needs to /// count indentation. first_scan: bool = false, + + outside_context: bool = false, }; fn scan(self: *@This(), opts: ScanOptions) ScanError!void { @@ -3477,7 +3766,7 @@ pub fn Parser(comptime enc: Encoding) type { .flow_key, => { self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, } @@ -3507,7 +3796,7 @@ pub fn Parser(comptime enc: Encoding) type { .line = self.line, }); - return error.UnexpectedToken; + return unexpectedToken(); }, .block_in, .block_out, @@ -3641,10 +3930,12 @@ pub fn Parser(comptime enc: Encoding) type { switch (self.context.get()) { .block_in, .block_out, + .flow_in, => { // scanPlainScalar }, - .flow_in, .flow_key => { + .flow_key, + => { self.inc(1); break :next .mappingValue(.{ .start = start, @@ -3653,7 +3944,6 @@ pub fn Parser(comptime enc: Encoding) type { }); }, } - // scanPlainScalar }, } @@ -3861,7 +4151,7 @@ pub fn Parser(comptime enc: Encoding) type { => {}, } self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, '>' => { const start = self.pos; @@ -3878,7 +4168,7 @@ pub fn Parser(comptime enc: Encoding) type { => {}, } self.token.start = start; - return error.UnexpectedToken; + return unexpectedToken(); }, '\'' => { self.inc(1); @@ -3907,7 +4197,7 @@ pub fn Parser(comptime enc: Encoding) type { .indent = self.line_indent, .line = self.line, }); - return error.UnexpectedToken; + return unexpectedToken(); }, inline '\r', @@ -3929,8 +4219,8 @@ pub fn Parser(comptime enc: Encoding) type { .flow_key, => { if (self.block_indents.get()) |block_indent| { - if (self.token.line != previous_token_line and self.token.indent.isLessThan(block_indent)) { - return error.UnexpectedToken; + if (!opts.outside_context and self.token.line != previous_token_line and self.token.indent.isLessThanOrEqual(block_indent)) { + return unexpectedToken(); } } }, @@ -4025,9 +4315,17 @@ pub fn Parser(comptime enc: Encoding) type { /// /// positions `pos` on the next newline, or eof. Errors fn trySkipToNewLine(self: *@This()) error{UnexpectedCharacter}!void { - self.skipSWhite(); + var whitespace = false; + + if (self.isSWhite()) { + whitespace = true; + self.skipSWhite(); + } if (self.isChar('#')) { + if (!whitespace) { + return error.UnexpectedCharacter; + } self.inc(1); while (!self.isChar('\n') and !self.isChar('\r')) { self.inc(1); @@ -4285,34 +4583,60 @@ pub fn Parser(comptime enc: Encoding) type { } fn drainWhitespace(self: *@This()) OOM!void { - for (self.parser.whitespace_buf.items) |ws| { - if (comptime Environment.ci_assert) { - const actual = self.parser.input[ws.pos.cast()]; - bun.assert(actual == ws.unit); - } + const parser = self.parser; + defer parser.whitespace_buf.clearRetainingCapacity(); - switch (self.str) { - .range => |*range| { - if (range.isEmpty()) { - range.off = ws.pos; - range.end = ws.pos; + for (parser.whitespace_buf.items) |ws| { + switch (ws) { + .source => |source| { + if (comptime Environment.ci_assert) { + const actual = self.parser.input[source.pos.cast()]; + bun.assert(actual == source.unit); } - bun.assert(range.end == ws.pos); + switch (self.str) { + .range => |*range| { + if (range.isEmpty()) { + range.off = source.pos; + range.end = source.pos; + } - range.end = ws.pos.add(1); + bun.assert(range.end == source.pos); + + range.end = source.pos.add(1); + }, + .list => |*list| { + try list.append(source.unit); + }, + } }, - .list => |*list| { - try list.append(ws.unit); + .new => |unit| { + switch (self.str) { + .range => |range| { + var list: std.ArrayList(enc.unit()) = try .initCapacity(parser.allocator, range.len() + 1); + list.appendSliceAssumeCapacity(range.slice(parser.input)); + list.appendAssumeCapacity(unit); + self.str = .{ .list = list }; + }, + .list => |*list| { + try list.append(unit); + }, + } }, } } - - self.parser.whitespace_buf.clearRetainingCapacity(); } pub fn appendSourceWhitespace(self: *@This(), unit: enc.unit(), pos: Pos) OOM!void { - try self.parser.whitespace_buf.append(.{ .unit = unit, .pos = pos }); + try self.parser.whitespace_buf.append(.{ .source = .{ .unit = unit, .pos = pos } }); + } + + pub fn appendWhitespace(self: *@This(), unit: enc.unit()) OOM!void { + try self.parser.whitespace_buf.append(.{ .new = unit }); + } + + pub fn appendWhitespaceNTimes(self: *@This(), unit: enc.unit(), n: usize) OOM!void { + try self.parser.whitespace_buf.appendNTimes(.{ .new = unit }, n); } pub fn appendSourceSlice(self: *@This(), off: Pos, end: Pos) OOM!void { @@ -4484,6 +4808,24 @@ pub fn Parser(comptime enc: Encoding) type { /// '!!unknown' unknown: String.Range, + + pub fn resolveNull(this: NodeTag, loc: logger.Loc) Expr { + return switch (this) { + .none, + .bool, + .int, + .float, + .null, + .verbatim, + .unknown, + => .init(E.Null, .{}, loc), + + // non-specific tags become seq, map, or str + .non_specific, + .str, + => .init(E.String, .{}, loc), + }; + } }; pub const NodeScalar = union(enum) { diff --git a/test/js/bun/import-attributes/import-attributes.test.ts b/test/js/bun/import-attributes/import-attributes.test.ts index da0e55d668..6931edcf55 100644 --- a/test/js/bun/import-attributes/import-attributes.test.ts +++ b/test/js/bun/import-attributes/import-attributes.test.ts @@ -313,7 +313,7 @@ test("jsonc", async () => { }, "yaml": { "default": { - "// my json ": null, + "// my json": null, "key": "👩‍👧‍👧value", }, "key": "👩‍👧‍👧value", diff --git a/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap b/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap new file mode 100644 index 0000000000..d09ae64a30 --- /dev/null +++ b/test/js/bun/yaml/__snapshots__/yaml.test.ts.snap @@ -0,0 +1,171 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`Bun.YAML parse issue 22286 2`] = ` +{ + "A Hat in Time": { + "ActPlando": { + "Dead Bird Studio Basement": "The Big Parade", + }, + "ActRandomizer": "insanity", + "BabyTrapWeight": 0, + "BadgeSellerMaxItems": 8, + "BadgeSellerMinItems": 5, + "BaseballBat": true, + "CTRLogic": { + "nothing": 0, + "scooter": 1, + "sprint": 0, + "time_stop_only": 0, + }, + "ChapterCostIncrement": 5, + "ChapterCostMinDifference": 5, + "CompassBadgeMode": "closest", + "DWAutoCompleteBonuses": true, + "DWEnableBonus": false, + "DWExcludeAnnoyingBonuses": true, + "DWExcludeAnnoyingContracts": true, + "DWExcludeCandles": true, + "DWShuffle": false, + "DWShuffleCountMax": 25, + "DWShuffleCountMin": 18, + "DWTimePieceRequirement": 15, + "DeathWishOnly": false, + "EnableDLC1": false, + "EnableDLC2": true, + "EnableDeathWish": false, + "EndGoal": { + "finale": 1, + "rush_hour": 0, + "seal_the_deal": 0, + }, + "ExcludeTour": false, + "FinalChapterMaxCost": 35, + "FinalChapterMinCost": 30, + "FinaleShuffle": false, + "HatItems": true, + "HighestChapterCost": 25, + "LaserTrapWeight": 0, + "LogicDifficulty": "moderate", + "LowestChapterCost": 5, + "MaxExtraTimePieces": "random-range-high-5-8", + "MaxPonCost": 80, + "MetroMaxPonCost": 50, + "MetroMinPonCost": 10, + "MinExtraYarn": "random-range-middle-5-15", + "MinPonCost": 20, + "NoPaintingSkips": true, + "NoTicketSkips": "rush_hour", + "NyakuzaThugMaxShopItems": 4, + "NyakuzaThugMinShopItems": 1, + "ParadeTrapWeight": 0, + "RandomizeHatOrder": "time_stop_last", + "ShipShapeCustomTaskGoal": 0, + "ShuffleActContracts": true, + "ShuffleAlpineZiplines": true, + "ShuffleStorybookPages": true, + "ShuffleSubconPaintings": true, + "StartWithCompassBadge": true, + "StartingChapter": { + "1": 1, + "2": 1, + "3": 1, + }, + "Tasksanity": false, + "TasksanityCheckCount": 18, + "TasksanityTaskStep": 1, + "TimePieceBalancePercent": "random-range-low-20-35", + "TrapChance": 0, + "UmbrellaLogic": true, + "YarnAvailable": "random-range-middle-40-50", + "YarnBalancePercent": 25, + "YarnCostMax": 8, + "YarnCostMin": 5, + "accessibility": { + "full": 1, + "minimal": 0, + }, + "death_link": false, + "exclude_locations": [ + "Queen Vanessa's Manor - Bedroom Chest", + "Queen Vanessa's Manor - Hall Chest", + "Act Completion (The Big Parade)", + ], + "non_local_items": [ + "Hookshot Badge", + "Umbrella", + "Dweller Mask", + ], + "priority_locations": [ + "Act Completion (Award Ceremony)", + "Badge Seller - Item 1", + "Badge Seller - Item 2", + "Mafia Boss Shop Item", + "Bluefin Tunnel Thug - Item 1", + "Green Clean Station Thug A - Item 1", + "Green Clean Station Thug B - Item 1", + "Main Station Thug A - Item 1", + "Main Station Thug B - Item 1", + "Main Station Thug C - Item 1", + "Pink Paw Station Thug - Item 1", + "Yellow Overpass Thug A - Item 1", + "Yellow Overpass Thug B - Item 1", + "Yellow Overpass Thug C - Item 1", + ], + "progression_balancing": "random-range-middle-40-50", + "start_inventory_from_pool": { + "Sprint Hat": 1, + }, + }, + "game": "A Hat in Time", + "name": "niyrme-AHiT{NUMBER}", + "requires": { + "version": "0.6.2", + }, + "x-options-async": { + "A Hat in Time": { + "+non_local_items": [ + "Brewing Hat", + "Ice Hat", + ], + "ChapterCostIncrement": 7, + "ChapterCostMinDifference": 7, + "EndGoal": { + "finale": 9, + "rush_hour": 1, + "seal_the_deal": 0, + }, + "FinalChapterMaxCost": 50, + "FinalChapterMinCost": 40, + "HighestChapterCost": 40, + "LowestChapterCost": 10, + "NoPaintingSkips": false, + "death_link": false, + "priority_locations": [], + "progression_balancing": "random-range-low-10-30", + }, + }, + "x-options-sync": { + "A Hat in Time": { + "+start_inventory_from_pool": { + "Badge Pin": 1, + }, + "+triggers": [ + { + "option_category": "A Hat in Time", + "option_name": "EndGoal", + "option_result": "finale", + "options": { + "A Hat in Time": { + "EnableDLC2": false, + "FinalChapterMaxCost": 35, + "FinalChapterMinCost": 25, + "MaxPonCost": 100, + "MinPonCost": 30, + }, + }, + }, + ], + }, + }, +} +`; diff --git a/test/js/bun/yaml/fixtures/AHatInTime.yaml b/test/js/bun/yaml/fixtures/AHatInTime.yaml new file mode 100644 index 0000000000..155a2491e4 --- /dev/null +++ b/test/js/bun/yaml/fixtures/AHatInTime.yaml @@ -0,0 +1,167 @@ +game: &AHiT "A Hat in Time" + +name: "niyrme-AHiT{NUMBER}" + +requires: + version: 0.6.2 + +*AHiT : + # game + progression_balancing: "random-range-middle-40-50" + accessibility: + "full": 1 + "minimal": 0 + death_link: false + + # general + &EndGoal EndGoal: + &EndGoal_Finale finale: 1 + &EndGoal_Rush rush_hour: 0 + &EndGoal_Seal seal_the_deal: 0 + ShuffleStorybookPages: true + ShuffleAlpineZiplines: true + ShuffleSubconPaintings: true + ShuffleActContracts: true + MinPonCost: 20 + MaxPonCost: 80 + BadgeSellerMinItems: 5 + BadgeSellerMaxItems: 8 + # https://docs.google.com/document/d/1x9VLSQ5davfx1KGamR9T0mD5h69_lDXJ6H7Gq7knJRI + LogicDifficulty: "moderate" + NoPaintingSkips: true + CTRLogic: + "time_stop_only": 0 + "scooter": 1 + "sprint": 0 + "nothing": 0 + + # acts + ActRandomizer: "insanity" + StartingChapter: + 1: 1 + 2: 1 + 3: 1 + LowestChapterCost: 5 + HighestChapterCost: 25 + ChapterCostIncrement: 5 + ChapterCostMinDifference: 5 + &GoalMinCost FinalChapterMinCost: 30 + &GoalMaxCost FinalChapterMaxCost: 35 + FinaleShuffle: false + + # items + StartWithCompassBadge: true + CompassBadgeMode: "closest" + RandomizeHatOrder: "time_stop_last" + YarnAvailable: "random-range-middle-40-50" + YarnCostMin: 5 + YarnCostMax: 8 + MinExtraYarn: "random-range-middle-5-15" + HatItems: true + UmbrellaLogic: true + MaxExtraTimePieces: "random-range-high-5-8" + YarnBalancePercent: 25 + TimePieceBalancePercent: "random-range-low-20-35" + + # DLC: Seal the Deal + EnableDLC1: false + Tasksanity: false + TasksanityTaskStep: 1 + TasksanityCheckCount: 18 + ShipShapeCustomTaskGoal: 0 + ExcludeTour: false + + # DLC: Nyakuza Metro + &DLCNyakuza EnableDLC2: true + MetroMinPonCost: 10 + MetroMaxPonCost: 50 + NyakuzaThugMinShopItems: 1 + NyakuzaThugMaxShopItems: 4 + BaseballBat: true + NoTicketSkips: "rush_hour" + + # Death Wish + EnableDeathWish: false + DWTimePieceRequirement: 15 + DWShuffle: false + DWShuffleCountMin: 18 + DWShuffleCountMax: 25 + DWEnableBonus: false + DWAutoCompleteBonuses: true + DWExcludeAnnoyingContracts: true + DWExcludeAnnoyingBonuses: true + DWExcludeCandles: true + DeathWishOnly: false + + # traps + TrapChance: 0 + BabyTrapWeight: 0 + LaserTrapWeight: 0 + ParadeTrapWeight: 0 + + # plando, item & location options + non_local_items: + - "Hookshot Badge" + - "Umbrella" + - "Dweller Mask" + start_inventory_from_pool: + "Sprint Hat": 1 + exclude_locations: + - "Queen Vanessa's Manor - Bedroom Chest" + - "Queen Vanessa's Manor - Hall Chest" + - "Act Completion (The Big Parade)" + priority_locations: + - "Act Completion (Award Ceremony)" + - "Badge Seller - Item 1" + - "Badge Seller - Item 2" + - "Mafia Boss Shop Item" + # Nyakuza DLC + - "Bluefin Tunnel Thug - Item 1" + - "Green Clean Station Thug A - Item 1" + - "Green Clean Station Thug B - Item 1" + - "Main Station Thug A - Item 1" + - "Main Station Thug B - Item 1" + - "Main Station Thug C - Item 1" + - "Pink Paw Station Thug - Item 1" + - "Yellow Overpass Thug A - Item 1" + - "Yellow Overpass Thug B - Item 1" + - "Yellow Overpass Thug C - Item 1" + + ActPlando: + "Dead Bird Studio Basement": "The Big Parade" + +x-options-sync: + *AHiT : + +start_inventory_from_pool: + "Badge Pin": 1 + +triggers: + - option_category: *AHiT + option_name: *EndGoal + option_result: *EndGoal_Finale + options: + *AHiT : + MinPonCost: 30 + MaxPonCost: 100 + *GoalMinCost : 25 + *GoalMaxCost : 35 + *DLCNyakuza : false + +x-options-async: + *AHiT : + progression_balancing: "random-range-low-10-30" + death_link: false + LowestChapterCost: 10 + HighestChapterCost: 40 + ChapterCostIncrement: 7 + ChapterCostMinDifference: 7 + *EndGoal : + *EndGoal_Finale : 9 + *EndGoal_Rush : 1 + *EndGoal_Seal : 0 + NoPaintingSkips: false + *GoalMinCost : 40 + *GoalMaxCost : 50 + +non_local_items: + - "Brewing Hat" + - "Ice Hat" + priority_locations: [] \ No newline at end of file diff --git a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py new file mode 100644 index 0000000000..002af37c51 --- /dev/null +++ b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py @@ -0,0 +1,1049 @@ +#!/usr/bin/env python3 + +import os +import json +import glob +import yaml +import subprocess +import sys +import re +import argparse + +def escape_js_string(s): + """Escape a string for use in JavaScript string literals.""" + result = [] + for char in s: + if char == '\\': + result.append('\\\\') + elif char == '"': + result.append('\\"') + elif char == '\n': + result.append('\\n') + elif char == '\t': + result.append('\\t') + elif char == '\r': + result.append('\\r') + elif char == '\b': + result.append('\\b') + elif char == '\f': + result.append('\\f') + elif ord(char) < 0x20 or ord(char) == 0x7F: + # Control characters - use \xNN notation + result.append(f'\\x{ord(char):02x}') + else: + result.append(char) + return ''.join(result) + +def format_js_string(content): + """Format content for JavaScript string literal.""" + # For JavaScript we'll use template literals for multiline strings + # unless they contain backticks or ${ + if '`' in content or '${' in content: + # Use regular string with escaping + escaped = escape_js_string(content) + return f'"{escaped}"' + elif '\n' in content: + # Use template literal for multiline + # But we still need to escape backslashes + escaped_content = content.replace('\\', '\\\\') # Escape backslashes + return f'`{escaped_content}`' + else: + # Short single line - use regular string + escaped = escape_js_string(content) + return f'"{escaped}"' + +def has_anchors_or_aliases(yaml_content): + """Check if YAML content has anchors (&) or aliases (*).""" + return '&' in yaml_content or '*' in yaml_content + +def stringify_map_keys(obj, from_yaml_package=False): + """Recursively stringify all map keys to match Bun's YAML behavior. + + Args: + obj: The object to process + from_yaml_package: If True, empty string keys came from yaml package converting null keys + and should be converted to "null". If False, empty strings are intentional. + """ + if isinstance(obj, dict): + new_dict = {} + for key, value in obj.items(): + # Convert key to string + if key is None: + # Actual None/null key should become "null" + str_key = "null" + elif key == "" and from_yaml_package: + # Empty string from yaml package (was originally a null key in YAML) + # should be converted to "null" to match Bun's behavior + str_key = "null" + elif key == "": + # Empty string from official test JSON or explicit empty string + # should stay as empty string + str_key = "" + elif isinstance(key, str) and from_yaml_package: + # Check if this is a stringified collection from yaml package + # yaml package converts [a, b] to "[ a, b ]" but Bun/JS uses "a,b" + if key.startswith('[ ') and key.endswith(' ]'): + # This looks like a stringified array from yaml package + # Extract the content and convert to JS array.toString() format + inner = key[2:-2] # Remove "[ " and " ]" + # Split by comma and space, then join with just comma + elements = [] + for elem in inner.split(','): + elem = elem.strip() + # Remove anchor notation (&name) from the element + # Anchors appear as "&name value" in the stringified form + if '&' in elem: + # Remove the anchor part (e.g., "&b b" becomes "b") + parts = elem.split() + if len(parts) > 1 and parts[0].startswith('&'): + elem = ' '.join(parts[1:]) + elements.append(elem) + str_key = ','.join(elements) + elif key.startswith('{ ') and key.endswith(' }'): + # This looks like a stringified object from yaml package + # JavaScript Object.toString() returns "[object Object]" + str_key = "[object Object]" + elif key.startswith('*'): + # This is an alias reference that wasn't resolved by yaml package + # This shouldn't happen in well-formed output, but handle it + # For now, keep it as-is but this might need special handling + str_key = key + else: + str_key = str(key) + else: + # All other keys get stringified + str_key = str(key) + # Recursively process value + new_dict[str_key] = stringify_map_keys(value, from_yaml_package) + return new_dict + elif isinstance(obj, list): + return [stringify_map_keys(item, from_yaml_package) for item in obj] + else: + return obj + +def json_to_js_literal(obj, indent_level=1, seen_objects=None, var_declarations=None): + """Convert JSON object to JavaScript literal, handling shared references.""" + if seen_objects is None: + seen_objects = {} + if var_declarations is None: + var_declarations = [] + + indent = " " * indent_level + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "true" if obj else "false" + elif isinstance(obj, (int, float)): + # Handle special float values + if obj != obj: # NaN + return "NaN" + elif obj == float('inf'): + return "Infinity" + elif obj == float('-inf'): + return "-Infinity" + return str(obj) + elif isinstance(obj, str): + escaped = escape_js_string(obj) + return f'"{escaped}"' + elif isinstance(obj, list): + if len(obj) == 0: + return "[]" + + # Check for complex nested structures + if any(isinstance(item, (list, dict)) for item in obj): + items = [] + for item in obj: + item_str = json_to_js_literal(item, indent_level + 1, seen_objects, var_declarations) + items.append(f"{indent} {item_str}") + return "[\n" + ",\n".join(items) + f"\n{indent}]" + else: + # Simple array - inline + items = [json_to_js_literal(item, indent_level, seen_objects, var_declarations) for item in obj] + return "[" + ", ".join(items) + "]" + elif isinstance(obj, dict): + if len(obj) == 0: + return "{}" + + # Check if this is a simple object + is_simple = all(not isinstance(v, (list, dict)) for v in obj.values()) + + if is_simple and len(obj) <= 3: + # Simple object - inline + pairs = [] + for key, value in obj.items(): + if key.isidentifier() and not key.startswith('$'): + key_str = key + else: + key_str = f'"{escape_js_string(key)}"' + value_str = json_to_js_literal(value, indent_level, seen_objects, var_declarations) + pairs.append(f"{key_str}: {value_str}") + return "{ " + ", ".join(pairs) + " }" + else: + # Complex object - multiline + pairs = [] + for key, value in obj.items(): + if key.isidentifier() and not key.startswith('$'): + key_str = key + else: + key_str = f'"{escape_js_string(key)}"' + value_str = json_to_js_literal(value, indent_level + 1, seen_objects, var_declarations) + pairs.append(f"{indent} {key_str}: {value_str}") + return "{\n" + ",\n".join(pairs) + f"\n{indent}}}" + else: + # Fallback + return json.dumps(obj) + +def parse_test_events(event_file): + """Parse test.event file to infer expected JSON structure. + + Event format: + +STR - Stream start + +DOC - Document start + +MAP - Map start + +SEQ - Sequence start + =VAL - Value (scalar) + =ALI - Alias + -MAP - Map end + -SEQ - Sequence end + -DOC - Document end + -STR - Stream end + """ + if not os.path.exists(event_file): + return None + + with open(event_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + docs = [] + stack = [] + current_doc = None + in_key = False + pending_key = None + + for line in lines: + line = line.rstrip('\n') + if not line: + continue + + if line.startswith('+DOC'): + stack = [] + current_doc = None + in_key = False + pending_key = None + + elif line.startswith('+MAP'): + new_map = {} + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(new_map) + elif isinstance(parent, dict) and pending_key is not None: + parent[pending_key] = new_map + pending_key = None + in_key = False + else: + current_doc = new_map + stack.append(new_map) + + elif line.startswith('+SEQ'): + new_seq = [] + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(new_seq) + elif isinstance(parent, dict) and pending_key is not None: + parent[pending_key] = new_seq + pending_key = None + in_key = False + else: + current_doc = new_seq + stack.append(new_seq) + + elif line.startswith('=VAL'): + # Extract value after =VAL + value = line[4:].strip() + if value.startswith(':'): + value = value[1:].strip() if len(value) > 1 else '' + + # Convert special values + if value == '': + value = '' + elif value == '': + value = ' ' + + if stack: + parent = stack[-1] + if isinstance(parent, list): + parent.append(value) + elif isinstance(parent, dict): + if in_key or pending_key is None: + # This is a key + pending_key = value + in_key = False + else: + # This is a value for the pending key + parent[pending_key] = value + pending_key = None + else: + # Scalar document + current_doc = value + + elif line.startswith('-MAP') or line.startswith('-SEQ'): + if stack: + completed = stack.pop() + # If this was the last item and we have a pending key, it means empty value + if isinstance(stack[-1] if stack else None, dict) and pending_key is not None: + (stack[-1] if stack else {})[pending_key] = None + pending_key = None + + elif line.startswith('-DOC'): + if current_doc is not None: + docs.append(current_doc) + current_doc = None + stack = [] + pending_key = None + + return docs if docs else None + +def detect_shared_references(yaml_content): + """Detect anchors and their aliases in YAML to identify shared references.""" + # Find all anchors and their aliases + anchor_pattern = r'&(\w+)' + alias_pattern = r'\*(\w+)' + + anchors = re.findall(anchor_pattern, yaml_content) + aliases = re.findall(alias_pattern, yaml_content) + + # Return anchors that are referenced by aliases + shared_refs = [] + for anchor in set(anchors): + if anchor in aliases: + shared_refs.append(anchor) + + return shared_refs + +def generate_expected_with_shared_refs(json_data, yaml_content): + """Generate expected object with shared references for anchors/aliases.""" + shared_refs = detect_shared_references(yaml_content) + + if not shared_refs: + # No shared references, generate simple literal + return json_to_js_literal(json_data) + + # For simplicity, when there are anchors/aliases, we'll generate the expected + # object but note that some values might be shared references + # This is a simplified approach - in reality we'd need to track which values + # are aliased to generate exact shared references + + # Generate with a comment about shared refs + result = json_to_js_literal(json_data) + + # Add comment about shared references + comment = f" // Note: Original YAML has anchors/aliases: {', '.join(shared_refs)}\n" + comment += " // Some values in the parsed result may be shared object references\n" + + return comment + " const expected = " + result + ";" + +def get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=True): + """Use yaml package (eemeli/yaml) or js-yaml to get expected output.""" + # Create a temporary JavaScript file to parse the YAML + if use_eemeli_yaml: + # Use eemeli/yaml which is more spec-compliant + js_code = f''' +const YAML = require('/Users/dylan/yamlz-3/node_modules/yaml'); + +const input = {format_js_string(yaml_content)}; + +try {{ + const docs = YAML.parseAllDocuments(input); + const results = docs.map(doc => doc.toJSON()); + console.log(JSON.stringify(results)); +}} catch (e) {{ + console.log(JSON.stringify({{"error": e.message}})); +}} +''' + else: + # Fallback to js-yaml + js_code = f''' +const yaml = require('/Users/dylan/yamlz-3/node_modules/js-yaml'); + +const input = {format_js_string(yaml_content)}; + +try {{ + const docs = yaml.loadAll(input); + console.log(JSON.stringify(docs)); +}} catch (e) {{ + console.log(JSON.stringify({{"error": e.message}})); +}} +''' + + # Write to temp file and execute with node + temp_js = '/tmp/parse_yaml_temp.js' + with open(temp_js, 'w') as f: + f.write(js_code) + + try: + result = subprocess.run(['node', temp_js], capture_output=True, text=True, timeout=5) + if result.returncode == 0 and result.stdout.strip(): + output = json.loads(result.stdout.strip()) + if isinstance(output, dict) and 'error' in output: + return None, output['error'] + return output, None + else: + return None, result.stderr or "Failed to parse" + except subprocess.TimeoutExpired: + return None, "Timeout" + except Exception as e: + return None, str(e) + finally: + if os.path.exists(temp_js): + os.remove(temp_js) + +def generate_test(test_dir, test_name, check_ast=True, use_js_yaml=False, use_yaml_pkg=False): + """Generate a single Bun test case from a yaml-test-suite directory. + + Args: + test_dir: Directory containing the test files + test_name: Name for the test + check_ast: If True, validate parsed AST. If False, only check parse success/failure. + use_js_yaml: If True, generate test using js-yaml instead of Bun's YAML + use_yaml_pkg: If True, generate test using yaml package instead of Bun's YAML + """ + + yaml_file = os.path.join(test_dir, "in.yaml") + json_file = os.path.join(test_dir, "in.json") + desc_file = os.path.join(test_dir, "===") + + # Read YAML content + if not os.path.exists(yaml_file): + return None + + with open(yaml_file, 'r', encoding='utf-8') as f: + yaml_content = f.read() + + # Read test description + description = "" + if os.path.exists(desc_file): + with open(desc_file, 'r', encoding='utf-8') as f: + description = f.read().strip().replace('\n', ' ') + + # Check if this is an error test (has 'error' file) + error_file = os.path.join(test_dir, "error") + is_error_test = os.path.exists(error_file) + + # For js-yaml, check if it actually can parse this + js_yaml_fails = False + js_yaml_error_msg = None + if use_js_yaml and not is_error_test: + # Quick check if js-yaml will fail on this + yaml_js_docs, yaml_js_error = get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=False) + if yaml_js_error: + js_yaml_fails = True + js_yaml_error_msg = yaml_js_error + + # If js-yaml fails but spec says it should pass, generate a special test + if use_js_yaml and js_yaml_fails and not is_error_test: + formatted_content = format_js_string(yaml_content) + return f''' +test.skip("{test_name}", () => {{ + // {description} + // SKIPPED: js-yaml fails but spec says this should pass + // js-yaml error: {js_yaml_error_msg} + const input = {formatted_content}; + + // js-yaml is stricter than the YAML spec - it fails on this valid YAML + // The official test suite says this should parse successfully +}}); +''' + + if is_error_test: + # Generate error test + formatted_content = format_js_string(yaml_content) + if use_js_yaml: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail (using js-yaml) + const input = {formatted_content}; + + expect(() => {{ + return jsYaml.load(input); + }}).toThrow(); +}}); +''' + elif use_yaml_pkg: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail (using yaml package) + const input = {formatted_content}; + + expect(() => {{ + return yamlPkg.parse(input); + }}).toThrow(); +}}); +''' + else: + return f''' +test("{test_name}", () => {{ + // {description} + // Error test - expecting parse to fail + const input: string = {formatted_content}; + + expect(() => {{ + return YAML.parse(input); + }}).toThrow(); +}}); +''' + + # Special handling for known problematic tests + if test_name == "yaml-test-suite/X38W": + # X38W has alias key that creates duplicate - yaml package doesn't handle this correctly + # The correct output is just one key "a,b" with value ["c", "b", "d"] + test = f''' +test("{test_name}", () => {{ + // {description} + // Special case: *a references the same array as first key, creating duplicate key + const input: string = {format_js_string(yaml_content)}; + + const parsed = YAML.parse(input); + + const expected: any = {{ + "a,b": ["c", "b", "d"] + }}; + + expect(parsed).toEqual(expected); +}}); +''' + return test + + # Get expected data from official test suite JSON file if available + json_data = None + has_json = False + + if os.path.exists(json_file): + with open(json_file, 'r', encoding='utf-8') as f: + json_content = f.read().strip() + + if not json_content: + json_data = [None] # Empty file represents null document + has_json = True + else: + try: + # Try single document + single_doc = json.loads(json_content) + json_data = [single_doc] + has_json = True + except json.JSONDecodeError: + # Try to parse as multiple JSON objects concatenated + decoder = json.JSONDecoder() + idx = 0 + docs = [] + while idx < len(json_content): + json_content_from_idx = json_content[idx:].lstrip() + if not json_content_from_idx: + break + try: + obj, end_idx = decoder.raw_decode(json_content_from_idx) + docs.append(obj) + idx += len(json_content[idx:]) - len(json_content_from_idx) + end_idx + except json.JSONDecodeError: + break + + if docs: + json_data = docs + has_json = True + else: + # Last resort: Try multi-document (one JSON per line) + docs = [] + for line in json_content.split('\n'): + line = line.strip() + if line: + try: + docs.append(json.loads(line)) + except: + pass + + if docs: + json_data = docs + has_json = True + + # If no JSON from test suite, use yaml package as reference + # (Skip test.event parsing for now as it's too simplistic) + if not has_json: + yaml_docs, yaml_error = get_expected_from_yaml_parser(yaml_content, use_eemeli_yaml=True) + if yaml_error: + # yaml package couldn't parse it, but maybe Bun's YAML can + # Just check that it doesn't throw + formatted_content = format_js_string(yaml_content) + return f''' +test("{test_name}", () => {{ + // {description} + // Parse test - yaml package couldn't parse, checking YAML behavior + const input = {formatted_content}; + + // Test may pass or fail, we're just documenting behavior + try {{ + const parsed = YAML.parse(input); + // Successfully parsed + expect(parsed).toBeDefined(); + }} catch (e) {{ + // Failed to parse + expect(e).toBeDefined(); + }} +}}); +''' + else: + json_data = yaml_docs + + # If not checking AST, just verify parse success + if not check_ast: + formatted_content = format_js_string(yaml_content) + if use_js_yaml: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled, using js-yaml) + const input = {formatted_content}; + + const parsed = jsYaml.load(input); + expect(parsed).toBeDefined(); +}}); +''' + elif use_yaml_pkg: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled, using yaml package) + const input = {formatted_content}; + + const parsed = yamlPkg.parse(input); + expect(parsed).toBeDefined(); +}}); +''' + else: + return f''' +test("{test_name}", () => {{ + // {description} + // Success test - expecting parse to succeed (AST checking disabled) + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); + expect(parsed).toBeDefined(); +}}); +''' + + # Format the YAML content for JavaScript + formatted_content = format_js_string(yaml_content) + + # Generate the test + comment = f"// {description}" + event_file = os.path.join(test_dir, "test.event") + if not os.path.exists(json_file) and os.path.exists(event_file): + comment += " (using test.event for expected values)" + elif not os.path.exists(json_file): + comment += " (using yaml package for expected values)" + + # Check if YAML has anchors/aliases + has_refs = has_anchors_or_aliases(yaml_content) + + # Handle multi-document YAML + # Only check the actual parsed data to determine if it's multi-document + # Document markers like --- and ... don't reliably indicate multiple documents + is_multi_doc = json_data and len(json_data) > 1 + + if is_multi_doc: + # Multi-document test - YAML.parse will return an array + if use_js_yaml: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = jsYaml.loadAll(input); +''' + elif use_yaml_pkg: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = yamlPkg.parseAllDocuments(input).map(doc => doc.toJSON()); +''' + else: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); +''' + + # Generate expected array + if has_refs: + test += ''' + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references +''' + + # Apply key stringification to match Bun's behavior + # from_yaml_package=!has_json: True if data came from yaml package, False if from official JSON + stringified_data = stringify_map_keys(json_data, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_data) + test += f''' + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected); +}}); +''' + else: + # Single document test + expected_value = json_data[0] if json_data else None + + if use_js_yaml: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = jsYaml.load(input); +''' + elif use_yaml_pkg: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = yamlPkg.parse(input); +''' + else: + test = f''' +test("{test_name}", () => {{ + {comment} + const input: string = {formatted_content}; + + const parsed = YAML.parse(input); +''' + + # Generate expected value + if has_refs: + # For tests with anchors/aliases, we need to handle shared references + # Check specific patterns in YAML + if '*' in yaml_content and '&' in yaml_content: + # Has both anchors and aliases - need to create shared references + test += ''' + // This YAML has anchors and aliases - creating shared references +''' + # Try to identify simple cases + if 'bill-to: &' in yaml_content and 'ship-to: *' in yaml_content: + # Common pattern: bill-to/ship-to sharing + test += ''' + const sharedAddress: any = ''' + # Find the shared object from expected data + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + if isinstance(stringified_value, dict): + if 'bill-to' in stringified_value: + shared_obj = stringified_value.get('bill-to') + test += json_to_js_literal(shared_obj) + ';' + # Now create expected with shared ref + test += ''' + const expected = ''' + # Build object with shared reference + test += '{\n' + for key, value in stringified_value.items(): + if key == 'bill-to': + test += f' "bill-to": sharedAddress,\n' + elif key == 'ship-to' and value == shared_obj: + test += f' "ship-to": sharedAddress,\n' + else: + test += f' "{escape_js_string(key)}": {json_to_js_literal(value)},\n' + test = test.rstrip(',\n') + '\n };' + else: + # Fallback to regular generation + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Fallback to regular generation + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Generic anchor/alias case + # Look for patterns like "- &anchor value" and "- *anchor" + anchor_matches = re.findall(r'&(\w+)\s+(.+?)(?:\n|$)', yaml_content) + alias_matches = re.findall(r'\*(\w+)', yaml_content) + + if anchor_matches and alias_matches: + # Build shared values based on anchors + anchor_vars = {} + for anchor_name, _ in anchor_matches: + if anchor_name in [a for a in alias_matches]: + # This anchor is referenced + anchor_vars[anchor_name] = f'shared_{anchor_name}' + + if anchor_vars and isinstance(expected_value, (list, dict)): + # Try to detect which values are shared + test += f''' + // Detected anchors that are referenced: {', '.join(anchor_vars.keys())} +''' + # For now, just generate the expected normally with a note + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # Has anchors but no aliases, or vice versa + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + else: + # No anchors/aliases - simple case + stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str};''' + + test += ''' + + expect(parsed).toEqual(expected); +}); +''' + + return test + +def main(): + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Translate yaml-test-suite to Bun tests') + parser.add_argument('--no-ast-check', action='store_true', + help='Only check if parsing succeeds/fails, do not validate AST') + parser.add_argument('--with-js-yaml', action='store_true', + help='Also generate a companion test file using js-yaml for validation') + parser.add_argument('--with-yaml', action='store_true', + help='Also generate a companion test file using yaml package for validation') + args = parser.parse_args() + + check_ast = not args.no_ast_check + with_js_yaml = args.with_js_yaml + with_yaml = args.with_yaml + + # Check if yaml package is installed (for getting expected values) + yaml_pkg_found = False + try: + # Try local node_modules first + subprocess.run(['node', '-e', "require('./node_modules/yaml')"], capture_output=True, check=True, cwd='/Users/dylan/yamlz-3') + yaml_pkg_found = True + except: + try: + # Try global install + subprocess.run(['node', '-e', "require('yaml')"], capture_output=True, check=True) + yaml_pkg_found = True + except: + pass + + if not yaml_pkg_found and check_ast: + print("Error: yaml package is not installed. Please run: npm install yaml") + print("Note: yaml package is only required when checking AST. Use --no-ast-check to skip AST validation.") + sys.exit(1) + + # Get all test directories + test_dirs = [] + yaml_test_suite_path = '/Users/dylan/yamlz-3/yaml-test-suite' + + for entry in glob.glob(f'{yaml_test_suite_path}/*'): + if os.path.isdir(entry) and os.path.basename(entry) not in ['.git', 'name', 'tags']: + # Check if this is a test directory (has in.yaml) + if os.path.exists(os.path.join(entry, 'in.yaml')): + test_dirs.append(entry) + else: + # Check for subdirectories with in.yaml (multi-doc tests) + for subdir in glob.glob(os.path.join(entry, '*')): + if os.path.isdir(subdir) and os.path.exists(os.path.join(subdir, 'in.yaml')): + test_dirs.append(subdir) + + test_dirs = sorted(test_dirs) + + print(f"Found {len(test_dirs)} test directories in yaml-test-suite") + if not check_ast: + print("AST checking disabled - will only verify parse success/failure") + + # Generate a sample test first + if test_dirs: + print("\nGenerating sample test...") + # Look for a test with anchors/aliases + sample_dir = None + for td in test_dirs: + yaml_file = os.path.join(td, "in.yaml") + if os.path.exists(yaml_file): + with open(yaml_file, 'r') as f: + content = f.read() + if '&' in content and '*' in content: + sample_dir = td + break + + if not sample_dir: + sample_dir = test_dirs[0] + + test_id = sample_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-test-suite/{test_id}" + + sample_test = generate_test(sample_dir, test_name, check_ast) + if sample_test: + print(f"Sample test for {test_id}:") + print(sample_test[:800] + "..." if len(sample_test) > 800 else sample_test) + + # Generate all tests + print("\nGenerating all tests...") + + mode_comment = "// AST validation disabled - only checking parse success/failure" if not check_ast else "// Using YAML.parse() with eemeli/yaml package as reference" + + output = f'''// Tests translated from official yaml-test-suite +{mode_comment} +// Total: {len(test_dirs)} test directories + +import {{ test, expect }} from "bun:test"; +import {{ YAML }} from "bun"; + +''' + + successful = 0 + failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-test-suite/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast) + if test_case: + output += test_case + successful += 1 + else: + print(f" Skipped {test_name}: returned None") + failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + failed += 1 + + # Write the output file to Bun's test directory + output_dir = '/Users/dylan/code/bun/test/js/bun/yaml' + os.makedirs(output_dir, exist_ok=True) + + filename = os.path.join(output_dir, 'yaml-test-suite.test.ts') + with open(filename, 'w', encoding='utf-8') as f: + f.write(output) + + print(f"\nGenerated {filename}") + print(f" Successful: {successful} tests") + print(f" Failed/Skipped: {failed} tests") + print(f" Total: {len(test_dirs)} directories processed") + + # Generate js-yaml companion tests if requested + if with_js_yaml: + print("\nGenerating js-yaml companion tests...") + + js_yaml_output = f'''// Tests translated from official yaml-test-suite +// Using js-yaml for validation of test translations +// Total: {len(test_dirs)} test directories + +import {{ test, expect }} from "bun:test"; +const jsYaml = require("js-yaml"); + +''' + + js_yaml_successful = 0 + js_yaml_failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"js-yaml/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing js-yaml {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast, use_js_yaml=True) + if test_case: + js_yaml_output += test_case + js_yaml_successful += 1 + else: + js_yaml_failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + js_yaml_failed += 1 + + # Write js-yaml test file + js_yaml_filename = os.path.join(output_dir, 'yaml-test-suite-js-yaml.test.ts') + with open(js_yaml_filename, 'w', encoding='utf-8') as f: + f.write(js_yaml_output) + + print(f"\nGenerated js-yaml companion: {js_yaml_filename}") + print(f" Successful: {js_yaml_successful} tests") + print(f" Failed/Skipped: {js_yaml_failed} tests") + + + # Generate yaml package companion tests if requested + if with_yaml: + print("\nGenerating yaml package companion tests...") + + yaml_output = f'''// Tests translated from official yaml-test-suite +// Using yaml package (eemeli/yaml) for validation of test translations +// Total: {len(test_dirs)} test directories +// Note: Requires 'yaml' package to be installed: npm install yaml + +import {{ test, expect }} from "bun:test"; +import * as yamlPkg from "yaml"; + +''' + + yaml_successful = 0 + yaml_failed = 0 + + for i, test_dir in enumerate(test_dirs): + test_id = test_dir.replace(yaml_test_suite_path + '/', '') + test_name = f"yaml-pkg/{test_id}" + + if (i + 1) % 50 == 0: + print(f" Processing yaml package {i+1}/{len(test_dirs)}...") + + try: + test_case = generate_test(test_dir, test_name, check_ast, use_yaml_pkg=True) + if test_case: + yaml_output += test_case + yaml_successful += 1 + else: + yaml_failed += 1 + except Exception as e: + print(f" Error with {test_name}: {e}") + yaml_failed += 1 + + # Write yaml package test file + yaml_filename = os.path.join(output_dir, 'yaml-test-suite-yaml-pkg.test.ts') + with open(yaml_filename, 'w', encoding='utf-8') as f: + f.write(yaml_output) + + print(f"\nGenerated yaml package companion: {yaml_filename}") + print(f" Successful: {yaml_successful} tests") + print(f" Failed/Skipped: {yaml_failed} tests") + + print(f"\nTo run tests: cd {output_dir} && bun test") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/test/js/bun/yaml/yaml-test-suite.test.ts b/test/js/bun/yaml/yaml-test-suite.test.ts new file mode 100644 index 0000000000..ce10104203 --- /dev/null +++ b/test/js/bun/yaml/yaml-test-suite.test.ts @@ -0,0 +1,6405 @@ +// Tests translated from official yaml-test-suite (6e6c296) +// Using YAML.parse() with eemeli/yaml package as reference +// Total: 402 test directories + +import { YAML } from "bun"; +import { expect, test } from "bun:test"; + +test("yaml-test-suite/229Q", () => { + // Spec Example 2.4. Sequence of Mappings + const input: string = `- + name: Mark McGwire + hr: 65 + avg: 0.278 +- + name: Sammy Sosa + hr: 63 + avg: 0.288 +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { name: "Mark McGwire", hr: 65, avg: 0.278 }, + { name: "Sammy Sosa", hr: 63, avg: 0.288 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/236B", () => { + // Invalid value after mapping + // Error test - expecting parse to fail + const input: string = `foo: + bar +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/26DV", () => { + // Whitespace around colon in mappings + const input: string = `"top1" : + "key1" : &alias1 scalar1 +'top2' : + 'key2' : &alias2 scalar2 +top3: &node3 + *alias1 : scalar3 +top4: + *alias2 : scalar4 +top5 : + scalar5 +top6: + &anchor6 'key6' : scalar6 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: alias1, alias2 + + const expected: any = { + top1: { key1: "scalar1" }, + top2: { key2: "scalar2" }, + top3: { scalar1: "scalar3" }, + top4: { scalar2: "scalar4" }, + top5: "scalar5", + top6: { key6: "scalar6" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/27NA", () => { + // Spec Example 5.9. Directive Indicator + const input: string = `%YAML 1.2 +--- text +`; + + const parsed = YAML.parse(input); + + const expected: any = "text"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2AUY", () => { + // Tags in Block Sequence + const input: string = ` - !!str a + - b + - !!int 42 + - d +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", "b", 42, "d"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2CMS", () => { + // Invalid mapping in plain multiline + // Error test - expecting parse to fail + const input: string = `this + is + invalid: x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2EBW", () => { + // Allowed characters in keys + const input: string = + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~: safe\n?foo: safe question mark\n:foo: safe colon\n-foo: safe dash\nthis is#not: a comment\n"; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe", + "?foo": "safe question mark", + ":foo": "safe colon", + "-foo": "safe dash", + "this is#not": "a comment", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2G84/00", () => { + // Literal modifers + // Error test - expecting parse to fail + const input: string = `--- |0 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2G84/01", () => { + // Literal modifers + // Error test - expecting parse to fail + const input: string = `--- |10 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/2G84/02", () => { + // Literal modifers + const input: string = "--- |1-"; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2G84/03", () => { + // Literal modifers + const input: string = "--- |1+"; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2JQS", () => { + // Block Mapping with Missing Keys (using test.event for expected values) + const input: string = `: a +: b +`; + + const parsed = YAML.parse(input); + + const expected: any = { null: "b" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2LFX", () => { + // Spec Example 6.13. Reserved Directives [1.3] + const input: string = `%FOO bar baz # Should be ignored + # with a warning. +--- +"foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2SXE", () => { + // Anchors With Colon in Name + const input: string = `&a: key: &a value +foo: + *a: +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { key: "value", foo: "key" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/2XXW", () => { + // Spec Example 2.25. Unordered Sets + const input: string = `# Sets are represented as a +# Mapping where each key is +# associated with a null value +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griff +`; + + const parsed = YAML.parse(input); + + const expected: any = { "Mark McGwire": null, "Sammy Sosa": null, "Ken Griff": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/33X3", () => { + // Three explicit integers in a block sequence + const input: string = `--- +- !!int 1 +- !!int -2 +- !!int 33 +`; + + const parsed = YAML.parse(input); + + const expected: any = [1, -2, 33]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/35KP", () => { + // Tags for Root Objects + const input: string = `--- !!map +? a +: b +--- !!seq +- !!str c +--- !!str +d +e +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, ["c"], "d e"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/36F6", () => { + // Multiline plain scalar with empty line + const input: string = `--- +plain: a + b + + c +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "a b\nc" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3ALJ", () => { + // Block Sequence in Block Sequence + const input: string = `- - s1_i1 + - s1_i2 +- s2 +`; + + const parsed = YAML.parse(input); + + const expected: any = [["s1_i1", "s1_i2"], "s2"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3GZX", () => { + // Spec Example 7.1. Alias Nodes + const input: string = `First occurrence: &anchor Foo +Second occurrence: *anchor +Override anchor: &anchor Bar +Reuse anchor: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { + "First occurrence": "Foo", + "Second occurrence": "Foo", + "Override anchor": "Bar", + "Reuse anchor": "Bar", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3HFZ", () => { + // Invalid content after document end marker + // Error test - expecting parse to fail + const input: string = `--- +key: value +... invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/3MYT", () => { + // Plain Scalar looking like key, comment, anchor and tag + const input: string = `--- +k:#foo + &a !t s +`; + + const parsed = YAML.parse(input); + + const expected: any = "k:#foo &a !t s"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3R3P", () => { + // Single block sequence with anchor + const input: string = `&sequence +- a +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/00", () => { + // Leading tabs in double quoted + const input: string = `"1 leading + \\ttab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 leading \ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/01", () => { + // Leading tabs in double quoted + const input: string = `"2 leading + \\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 leading \ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/02", () => { + // Leading tabs in double quoted + const input: string = `"3 leading + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 leading tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/03", () => { + // Leading tabs in double quoted + const input: string = `"4 leading + \\t tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "4 leading \t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/04", () => { + // Leading tabs in double quoted + const input: string = `"5 leading + \\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "5 leading \t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3RLN/05", () => { + // Leading tabs in double quoted + const input: string = `"6 leading + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "6 leading tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/3UYS", () => { + // Escaped slash in double quotes + const input: string = `escaped slash: "a\\/b" +`; + + const parsed = YAML.parse(input); + + const expected: any = { "escaped slash": "a/b" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4ABK", () => { + // Flow Mapping Separate Values (using test.event for expected values) + const input: string = `{ +unquoted : "separate", +http://foo.com, +omitted value:, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { unquoted: "separate", "http://foo.com": null, "omitted value": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4CQQ", () => { + // Spec Example 2.18. Multi-line Flow Scalars + const input: string = `plain: + This unquoted scalar + spans many lines. + +quoted: "So does this + quoted scalar.\\n" +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "This unquoted scalar spans many lines.", quoted: "So does this quoted scalar.\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4EJS", () => { + // Invalid tabs as indendation in a mapping + // Error test - expecting parse to fail + const input: string = `--- +a: + b: + c: value +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/4FJ6", () => { + // Nested implicit complex keys (using test.event for expected values) + const input: string = `--- +[ + [ a, [ [[b,c]]: d, e]]: 23 +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "[\n a,\n [\n {\n ? [ [ b, c ] ]\n : d\n },\n e\n ]\n]": 23 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4GC6", () => { + // Spec Example 7.7. Single Quoted Characters + const input: string = `'here''s to "quotes"' +`; + + const parsed = YAML.parse(input); + + const expected: any = 'here\'s to "quotes"'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4H7K", () => { + // Flow sequence with invalid extra closing bracket + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c ] ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4HVU", () => { + // Wrong indendation in Sequence + // Error test - expecting parse to fail + const input: string = `key: + - ok + - also ok + - wrong +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4JVG", () => { + // Scalar value with two anchors + // Error test - expecting parse to fail + const input: string = `top1: &node1 + &k1 key1: val1 +top2: &node2 + &v2 val2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/4MUZ/00", () => { + // Flow mapping colon on line after key + const input: string = `{"foo" +: "bar"} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4MUZ/01", () => { + // Flow mapping colon on line after key + const input: string = `{"foo" +: bar} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4MUZ/02", () => { + // Flow mapping colon on line after key + const input: string = `{foo +: bar} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4Q9F", () => { + // Folded Block Scalar [1.3] + const input: string = `--- > + ab + cd + + ef + + + gh +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab cd\nef\n\ngh\n"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/4QFQ", () => { + // Spec Example 8.2. Block Indentation Indicator [1.3] + const input: string = `- | + detected +- > + + + # detected +- |1 + explicit +- > + detected +`; + + const parsed = YAML.parse(input); + + const expected: any = ["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4RWC", () => { + // Trailing spaces after flow collection + const input: string = ` [1, 2, 3] + `; + + const parsed = YAML.parse(input); + + const expected: any = [1, 2, 3]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4UYU", () => { + // Colon in Double Quoted String + const input: string = `"foo: bar\\": baz" +`; + + const parsed = YAML.parse(input); + + const expected: any = 'foo: bar": baz'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4V8U", () => { + // Plain scalar with backslashes + const input: string = `--- +plain\\value\\with\\backslashes +`; + + const parsed = YAML.parse(input); + + const expected: any = "plain\\value\\with\\backslashes"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4WA9", () => { + // Literal scalars + const input: string = `- aaa: |2 + xxx + bbb: | + xxx +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ aaa: "xxx\n", bbb: "xxx\n" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/4ZYM", () => { + // Spec Example 6.4. Line Prefixes + const input: string = `plain: text + lines +quoted: "text + lines" +block: | + text + lines +`; + + const parsed = YAML.parse(input); + + const expected: any = { plain: "text lines", quoted: "text lines", block: "text\n \tlines\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/52DL", () => { + // Explicit Non-Specific Tag [1.3] + const input: string = `--- +! a +`; + + const parsed = YAML.parse(input); + + const expected: any = "a"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/54T7", () => { + // Flow Mapping + const input: string = `{foo: you, bar: far} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "you", bar: "far" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/55WF", () => { + // Invalid escape in double quoted string + // Error test - expecting parse to fail + const input: string = `--- +"\\." +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/565N", () => { + // Construct Binary + const input: string = `canonical: !!binary "\\ + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\\ + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\\ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\\ + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" +generic: !!binary | + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5 + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs= +description: + The binary value above is a tiny arrow encoded as a gif image. +`; + + const parsed = YAML.parse(input); + + const expected: any = { + canonical: + "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=", + generic: + "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\nOTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\n+f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\nAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=\n", + description: "The binary value above is a tiny arrow encoded as a gif image.", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/57H4", () => { + // Spec Example 8.22. Block Collection Nodes + const input: string = `sequence: !!seq +- entry +- !!seq + - nested +mapping: !!map + foo: bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["entry", ["nested"]], + mapping: { foo: "bar" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/58MP", () => { + // Flow mapping edge cases + const input: string = `{x: :x} +`; + + const parsed = YAML.parse(input); + + const expected: any = { x: ":x" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5BVJ", () => { + // Spec Example 5.7. Block Scalar Indicators + const input: string = `literal: | + some + text +folded: > + some + text +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "some\ntext\n", folded: "some text\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5C5M", () => { + // Spec Example 7.15. Flow Mappings + const input: string = `- { one : two , three: four , } +- {five: six,seven : eight} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { one: "two", three: "four" }, + { five: "six", seven: "eight" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5GBF", () => { + // Spec Example 6.5. Empty Lines + const input: string = `Folding: + "Empty line + + as a line feed" +Chomping: | + Clipped empty lines + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { Folding: "Empty line\nas a line feed", Chomping: "Clipped empty lines\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5KJE", () => { + // Spec Example 7.13. Flow Sequence + const input: string = `- [ one, two, ] +- [three ,four] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["one", "two"], + ["three", "four"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5LLU", () => { + // Block scalar with wrong indented line after spaces only + // Error test - expecting parse to fail + const input: string = `block scalar: > + + + + invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/5MUD", () => { + // Colon and adjacent value on next line + const input: string = `--- +{ "foo" + :bar } +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5NYZ", () => { + // Spec Example 6.9. Separated Comment + const input: string = `key: # Comment + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5T43", () => { + // Colon at the beginning of adjacent flow scalar + const input: string = `- { "key":value } +- { "key"::value } +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ key: "value" }, { key: ":value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5TRB", () => { + // Invalid document-start marker in doublequoted tring + // Error test - expecting parse to fail + const input: string = `--- +" +--- +" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/5TYM", () => { + // Spec Example 6.21. Local Tag Prefix + const input: string = `%TAG !m! !my- +--- # Bulb here +!m!light fluorescent +... +%TAG !m! !my- +--- # Color here +!m!light green +`; + + const parsed = YAML.parse(input); + + const expected: any = ["fluorescent", "green"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/5U3A", () => { + // Sequence on same Line as Mapping Key + // Error test - expecting parse to fail + const input: string = `key: - a + - b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/5WE3", () => { + // Spec Example 8.17. Explicit Block Mapping Entries + const input: string = `? explicit key # Empty value +? | + block key +: - one # Explicit compact + - two # block value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "explicit key": null, + "block key\n": ["one", "two"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/62EZ", () => { + // Invalid block mapping key on same line as previous key + // Error test - expecting parse to fail + const input: string = `--- +x: { y: z }in: valid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/652Z", () => { + // Question mark at start of flow key + const input: string = `{ ?foo: bar, +bar: 42 +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { "?foo": "bar", bar: 42 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/65WH", () => { + // Single Entry Block Sequence + const input: string = `- foo +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6BCT", () => { + // Spec Example 6.3. Separation Spaces + const input: string = `- foo: bar +- - baz + - baz +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ foo: "bar" }, ["baz", "baz"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6BFJ", () => { + // Mapping, key and flow sequence item anchors (using test.event for expected values) + const input: string = `--- +&mapping +&key [ &item a, b, c ]: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { "a,b,c": "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6CA3", () => { + // Tab indented top flow + const input: string = ` [ + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = []; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6CK3", () => { + // Spec Example 6.26. Tag Shorthands + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +- !local foo +- !!str bar +- !e!tag%21 baz +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", "baz"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6FWR", () => { + // Block Scalar Keep + const input: string = `--- |+ + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab\n\n \n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6H3V", () => { + // Backslashes in singlequotes + const input: string = `'foo: bar\\': baz' +`; + + const parsed = YAML.parse(input); + + const expected: any = { "foo: bar\\": "baz'" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6HB6", () => { + // Spec Example 6.1. Indentation Spaces + const input: string = ` # Leading comment line spaces are + # neither content nor indentation. + +Not indented: + By one space: | + By four + spaces + Flow style: [ # Leading spaces + By two, # in flow style + Also by two, # are neither + Still by two # content nor + ] # indentation. +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Not indented": { + "By one space": "By four\n spaces\n", + "Flow style": ["By two", "Also by two", "Still by two"], + }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6JQW", () => { + // Spec Example 2.13. In literals, newlines are preserved + const input: string = `# ASCII Art +--- | + \\//||\\/|| + // || ||__ +`; + + const parsed = YAML.parse(input); + + const expected: any = "\\//||\\/||\n// || ||__\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6JTT", () => { + // Flow sequence without closing bracket + // Error test - expecting parse to fail + const input: string = `--- +[ [ a, b, c ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/6JWB", () => { + // Tags for Block Objects + const input: string = `foo: !!seq + - !!str a + - !!map + key: !!str value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: ["a", { key: "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6KGN", () => { + // Anchor for empty node + const input: string = `--- +a: &anchor +b: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { a: null, b: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6LVF", () => { + // Spec Example 6.13. Reserved Directives + const input: string = `%FOO bar baz # Should be ignored + # with a warning. +--- "foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6M2F", () => { + // Aliases in Explicit Block Mapping (using test.event for expected values) + const input: string = `? &a a +: &b b +: *a +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { a: "b", null: "a" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6PBE", () => { + // Zero-indented sequences in explicit mapping keys (using test.event for expected values) + const input: string = `--- +? +- a +- b +: +- c +- d +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "a,b": ["c", "d"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6S55", () => { + // Invalid scalar at the end of sequence + // Error test - expecting parse to fail + const input: string = `key: + - bar + - baz + invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/6SLA", () => { + // Allowed characters in quoted mapping key + const input: string = `"foo\\nbar:baz\\tx \\\\$%^&*()x": 23 +'x\\ny:z\\tx $%^&*()x': 24 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { "foo\nbar:baz\tx \\$%^&*()x": 23, "x\\ny:z\\tx $%^&*()x": 24 }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/6VJK", () => { + // Spec Example 2.15. Folded newlines are preserved for "more indented" and blank lines + const input: string = `> + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! +`; + + const parsed = YAML.parse(input); + + const expected: any = + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6WLZ", () => { + // Spec Example 6.18. Primary Tag Handle [1.3] + const input: string = `# Private +--- +!foo "bar" +... +# Global +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar", "bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6WPF", () => { + // Spec Example 6.8. Flow Folding [1.3] + const input: string = `--- +" + foo + + bar + + baz +" +`; + + const parsed = YAML.parse(input); + + const expected: any = " foo\nbar\nbaz "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6XDY", () => { + // Two document start markers + const input: string = `--- +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/6ZKB", () => { + // Spec Example 9.6. Stream + const input: string = `Document +--- +# Empty +... +%YAML 1.2 +--- +matches %: 20 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Document", null, { "matches %": 20 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/735Y", () => { + // Spec Example 8.20. Block Node Types + const input: string = `- + "flow in block" +- > + Block scalar +- !!map # Block collection + foo : bar +`; + + const parsed = YAML.parse(input); + + const expected: any = ["flow in block", "Block scalar\n", { foo: "bar" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/74H7", () => { + // Tags in Implicit Mapping + const input: string = `!!str a: b +c: !!int 42 +e: !!str f +g: h +!!str 23: !!bool false +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "b", + c: 42, + e: "f", + g: "h", + "23": false, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/753E", () => { + // Block Scalar Strip [1.3] + const input: string = `--- |- + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7A4E", () => { + // Spec Example 7.6. Double Quoted Lines + const input: string = `" 1st non-empty + + 2nd non-empty + 3rd non-empty " +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7BMT", () => { + // Node and Mapping Key Anchors [1.3] + const input: string = `--- +top1: &node1 + &k1 key1: one +top2: &node2 # comment + key2: two +top3: + &k3 key3: three +top4: &node4 + &k4 key4: four +top5: &node5 + key5: five +top6: &val6 + six +top7: + &val7 seven +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: { key1: "one" }, + top2: { key2: "two" }, + top3: { key3: "three" }, + top4: { key4: "four" }, + top5: { key5: "five" }, + top6: "six", + top7: "seven", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7BUB", () => { + // Spec Example 2.10. Node for “Sammy Sosa” appears twice in this document + const input: string = `--- +hr: + - Mark McGwire + # Following node labeled SS + - &SS Sammy Sosa +rbi: + - *SS # Subsequent occurrence + - Ken Griffey +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: SS + + const expected: any = { + hr: ["Mark McGwire", "Sammy Sosa"], + rbi: ["Sammy Sosa", "Ken Griffey"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7FWL", () => { + // Spec Example 6.24. Verbatim Tags + const input: string = `! foo : + ! baz +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "baz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7LBH", () => { + // Multiline double quoted implicit keys + // Error test - expecting parse to fail + const input: string = `"a\\nb": 1 +"c + d": 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/7MNF", () => { + // Missing colon + // Error test - expecting parse to fail + const input: string = `top1: + key1: val1 +top2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/7T8X", () => { + // Spec Example 8.10. Folded Lines - 8.13. Final Empty Lines + const input: string = `> + + folded + line + + next + line + * bullet + + * list + * lines + + last + line + +# Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7TMG", () => { + // Comment in flow sequence before comma + const input: string = `--- +[ word1 +# comment +, word2] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["word1", "word2"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/7W2P", () => { + // Block Mapping with Missing Values + const input: string = `? a +? b +c: +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: null, b: null, c: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7Z25", () => { + // Bare document after document end marker + const input: string = `--- +scalar1 +... +key: value +`; + + const parsed = YAML.parse(input); + + const expected: any = ["scalar1", { key: "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/7ZZ5", () => { + // Empty flow collections + const input: string = `--- +nested sequences: +- - - [] +- - - {} +key1: [] +key2: {} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "nested sequences": [[[[]]], [[{}]]], + key1: [], + key2: {}, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/82AN", () => { + // Three dashes and content without space + const input: string = `---word1 +word2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "---word1 word2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/87E4", () => { + // Spec Example 7.8. Single Quoted Implicit Keys + const input: string = `'implicit block key' : [ + 'implicit flow key' : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8CWC", () => { + // Plain mapping key ending with colon + const input: string = `--- +key ends with two colons::: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { "key ends with two colons::": "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8G76", () => { + // Spec Example 6.10. Comment Lines + const input: string = ` # Comment + + + +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8KB6", () => { + // Multiline plain flow mapping key without value + const input: string = `--- +- { single line, a: b} +- { multi + line, a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "single line": null, a: "b" }, + { "multi line": null, a: "b" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8MK2", () => { + // Explicit Non-Specific Tag + const input: string = `! a +`; + + const parsed = YAML.parse(input); + + const expected: any = "a"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8QBE", () => { + // Block Sequence in Block Mapping + const input: string = `key: + - item1 + - item2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: ["item1", "item2"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8UDB", () => { + // Spec Example 7.14. Flow Sequence Entries + const input: string = `[ +"double + quoted", 'single + quoted', +plain + text, [ nested ], +single: pair, +] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["double quoted", "single quoted", "plain text", ["nested"], { single: "pair" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/8XDJ", () => { + // Comment in plain multiline value + // Error test - expecting parse to fail + const input: string = `key: word1 +# xxx + word2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/8XYN", () => { + // Anchor with unicode character + const input: string = `--- +- &😁 unicode anchor +`; + + const parsed = YAML.parse(input); + + const expected: any = ["unicode anchor"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/93JH", () => { + // Block Mappings in Block Sequence + const input: string = ` - key: value + key2: value2 + - + key3: value3 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ key: "value", key2: "value2" }, { key3: "value3" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/93WF", () => { + // Spec Example 6.6. Line Folding [1.3] + const input: string = `--- >- + trimmed + + + + as + space +`; + + const parsed = YAML.parse(input); + + const expected: any = "trimmed\n\n\nas space"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96L6", () => { + // Spec Example 2.14. In the folded scalars, newlines become spaces + const input: string = `--- > + Mark McGwire's + year was crippled + by a knee injury. +`; + + const parsed = YAML.parse(input); + + const expected: any = "Mark McGwire's year was crippled by a knee injury.\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96NN/00", () => { + // Leading tab content in literals + const input: string = `foo: |- + bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\tbar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/96NN/01", () => { + // Leading tab content in literals + const input: string = `foo: |- + bar`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\tbar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/98YD", () => { + // Spec Example 5.5. Comment Indicator + const input: string = `# Comment only. +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9BXH", () => { + // Multiline doublequoted flow mapping key without value + const input: string = `--- +- { "single line", a: b} +- { "multi + line", a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { "single line": null, a: "b" }, + { "multi line": null, a: "b" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9C9N", () => { + // Wrong indented flow sequence + // Error test - expecting parse to fail + const input: string = `--- +flow: [a, +b, +c] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9CWY", () => { + // Invalid scalar at the end of mapping + // Error test - expecting parse to fail + const input: string = `key: + - item1 + - item2 +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9DXL", () => { + // Spec Example 9.6. Stream [1.3] + const input: string = `Mapping: Document +--- +# Empty +... +%YAML 1.2 +--- +matches %: 20 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ Mapping: "Document" }, null, { "matches %": 20 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9FMG", () => { + // Multi-level Mapping Indent + const input: string = `a: + b: + c: d + e: + f: g +h: i +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: { + b: { c: "d" }, + e: { f: "g" }, + }, + h: "i", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9HCY", () => { + // Need document footer before directives + // Error test - expecting parse to fail + const input: string = `!foo "bar" +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9J7A", () => { + // Simple Mapping Indent + const input: string = `foo: + bar: baz +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: { bar: "baz" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9JBA", () => { + // Invalid comment after end of flow sequence + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c, ]#invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9KAX", () => { + // Various combinations of tags and anchors + const input: string = `--- +&a1 +!!str +scalar1 +--- +!!str +&a2 +scalar2 +--- +&a3 +!!str scalar3 +--- +&a4 !!map +&a5 !!str key5: value4 +--- +a6: 1 +&anchor6 b6: 2 +--- +!!map +&a8 !!str key8: value7 +--- +!!map +!!str &a10 key10: value9 +--- +!!str &a11 +value11 +`; + + const parsed = YAML.parse(input); + + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references + + const expected: any = [ + "scalar1", + "scalar2", + "scalar3", + { key5: "value4" }, + { a6: 1, b6: 2 }, + { key8: "value7" }, + { key10: "value9" }, + "value11", + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9KBC", () => { + // Mapping starting at --- line + // Error test - expecting parse to fail + const input: string = `--- key1: value1 + key2: value2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9MAG", () => { + // Flow sequence with invalid comma at the beginning + // Error test - expecting parse to fail + const input: string = `--- +[ , a, b, c ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9MMA", () => { + // Directive by itself with no document + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/9MMW", () => { + // Single Pair Implicit Entries (using test.event for expected values) + const input: string = `- [ YAML : separate ] +- [ "JSON like":adjacent ] +- [ {JSON: like}:adjacent ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [[{ YAML: "separate" }], [{ "JSON like": "adjacent" }], [{ "[object Object]": "adjacent" }]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9MQT/00", () => { + // Scalar doc with '...' in content + const input: string = `--- "a +...x +b" +`; + + const parsed = YAML.parse(input); + + const expected: any = "a ...x b"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9MQT/01", () => { + // Scalar doc with '...' in content + // Error test - expecting parse to fail + const input: string = `--- "a +... x +b" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/9SA2", () => { + // Multiline double quoted flow mapping key + const input: string = `--- +- { "single line": value} +- { "multi + line": value} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "single line": "value" }, { "multi line": "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9SHH", () => { + // Spec Example 5.8. Quoted Scalar Indicators + const input: string = `single: 'text' +double: "text" +`; + + const parsed = YAML.parse(input); + + const expected: any = { single: "text", double: "text" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9TFX", () => { + // Spec Example 7.6. Double Quoted Lines [1.3] + const input: string = `--- +" 1st non-empty + + 2nd non-empty + 3rd non-empty " +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9U5K", () => { + // Spec Example 2.12. Compact Nested Mapping + const input: string = `--- +# Products purchased +- item : Super Hoop + quantity: 1 +- item : Basketball + quantity: 4 +- item : Big Shoes + quantity: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { item: "Super Hoop", quantity: 1 }, + { item: "Basketball", quantity: 4 }, + { item: "Big Shoes", quantity: 1 }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9WXW", () => { + // Spec Example 6.18. Primary Tag Handle + const input: string = `# Private +!foo "bar" +... +# Global +%TAG ! tag:example.com,2000:app/ +--- +!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar", "bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/9YRD", () => { + // Multiline Scalar at Top Level + const input: string = `a +b + c +d + +e +`; + + const parsed = YAML.parse(input); + + const expected: any = "a b c d\ne"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/A2M4", () => { + // Spec Example 6.2. Indentation Indicators + const input: string = `? a +: - b + - - c + - d +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", ["c", "d"]], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/A6F9", () => { + // Spec Example 8.4. Chomping Final Line Break + const input: string = `strip: |- + text +clip: | + text +keep: |+ + text +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "text", clip: "text\n", keep: "text\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/A984", () => { + // Multiline Scalar in Mapping + const input: string = `a: b + c +d: + e + f +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "b c", d: "e f" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AB8U", () => { + // Sequence entry that looks like two with wrong indentation + const input: string = `- single multiline + - sequence entry +`; + + const parsed = YAML.parse(input); + + const expected: any = ["single multiline - sequence entry"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AVM7", () => { + // Empty Stream + const input: string = ""; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AZ63", () => { + // Sequence With Same Indentation as Parent Mapping + const input: string = `one: +- 2 +- 3 +four: 5 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + one: [2, 3], + four: 5, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/AZW3", () => { + // Lookahead test cases + const input: string = `- bla"keks: foo +- bla]keks: foo +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ 'bla"keks': "foo" }, { "bla]keks": "foo" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/B3HG", () => { + // Spec Example 8.9. Folded Scalar [1.3] + const input: string = `--- > + folded + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded text\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/B63P", () => { + // Directive without document + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +... +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BD7L", () => { + // Invalid mapping after sequence + // Error test - expecting parse to fail + const input: string = `- item1 +- item2 +invalid: x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BEC7", () => { + // Spec Example 6.14. “YAML” directive + const input: string = `%YAML 1.3 # Attempt parsing + # with a warning +--- +"foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/BF9H", () => { + // Trailing comment in multiline plain scalar + // Error test - expecting parse to fail + const input: string = `--- +plain: a + b # end of scalar + c +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BS4K", () => { + // Comment between plain scalar lines + // Error test - expecting parse to fail + const input: string = `word1 # comment +word2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/BU8L", () => { + // Node Anchor and Tag on Seperate Lines + const input: string = `key: &anchor + !!map + a: b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: { a: "b" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/C2DT", () => { + // Spec Example 7.18. Flow Mapping Adjacent Values + const input: string = `{ +"adjacent":value, +"readable": value, +"empty": +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { adjacent: "value", readable: "value", empty: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/C2SP", () => { + // Flow Mapping Key on two lines + // Error test - expecting parse to fail + const input: string = `[23 +]: 42 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/C4HZ", () => { + // Spec Example 2.24. Global Tags + const input: string = `%TAG ! tag:clarkevans.com,2002: +--- !shape + # Use the ! handle for presenting + # tag:clarkevans.com,2002:circle +- !circle + center: &ORIGIN {x: 73, y: 129} + radius: 7 +- !line + start: *ORIGIN + finish: { x: 89, y: 102 } +- !label + start: *ORIGIN + color: 0xFFEEBB + text: Pretty vector drawing. +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: ORIGIN + + const expected: any = [ + { + center: { x: 73, y: 129 }, + radius: 7, + }, + { + start: { x: 73, y: 129 }, + finish: { x: 89, y: 102 }, + }, + { + start: { x: 73, y: 129 }, + color: 16772795, + text: "Pretty vector drawing.", + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CC74", () => { + // Spec Example 6.20. Tag Handles + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +!e!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = "bar"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CFD4", () => { + // Empty implicit key in single pair flow sequences (using test.event for expected values) + const input: string = `- [ : empty key ] +- [: another empty key] +`; + + const parsed = YAML.parse(input); + + const expected: any = [[{ null: "empty key" }], [{ null: "another empty key" }]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CML9", () => { + // Missing comma in flow + // Error test - expecting parse to fail + const input: string = `key: [ word1 +# xxx + word2 ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CN3R", () => { + // Various location of anchors in flow sequence + const input: string = `&flowseq [ + a: b, + &c c: d, + { &e e: f }, + &g { g: h } +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, { c: "d" }, { e: "f" }, { g: "h" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CPZ3", () => { + // Doublequoted scalar starting with a tab + const input: string = `--- +tab: "\\tstring" +`; + + const parsed = YAML.parse(input); + + const expected: any = { tab: "\tstring" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CQ3W", () => { + // Double quoted string without closing quote + // Error test - expecting parse to fail + const input: string = `--- +key: "missing closing quote +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CT4Q", () => { + // Spec Example 7.20. Single Pair Explicit Entry + const input: string = `[ +? foo + bar : baz +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "foo bar": "baz" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CTN5", () => { + // Flow sequence with invalid extra comma + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c, , ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CUP7", () => { + // Spec Example 5.6. Node Property Indicators + const input: string = `anchored: !local &anchor value +alias: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { anchored: "value", alias: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/CVW2", () => { + // Invalid comment after comma + // Error test - expecting parse to fail + const input: string = `--- +[ a, b, c,#invalid +] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/CXX2", () => { + // Mapping with anchor on document start line + // Error test - expecting parse to fail + const input: string = `--- &anchor a: b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/D49Q", () => { + // Multiline single quoted implicit keys + // Error test - expecting parse to fail + const input: string = `'a\\nb': 1 +'c + d': 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/D83L", () => { + // Block scalar indicator order + const input: string = `- |2- + explicit indent and chomp +- |-2 + chomp and explicit indent +`; + + const parsed = YAML.parse(input); + + const expected: any = ["explicit indent and chomp", "chomp and explicit indent"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/D88J", () => { + // Flow Sequence in Block Mapping + const input: string = `a: [b, c] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", "c"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/D9TU", () => { + // Single Pair Block Mapping + const input: string = `foo: bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DBG4", () => { + // Spec Example 7.10. Plain Characters + const input: string = `# Outside flow collection: +- ::vector +- ": - ()" +- Up, up, and away! +- -123 +- http://example.com/foo#bar +# Inside flow collection: +- [ ::vector, + ": - ()", + "Up, up and away!", + -123, + http://example.com/foo#bar ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + "::vector", + ": - ()", + "Up, up, and away!", + -123, + "http://example.com/foo#bar", + ["::vector", ": - ()", "Up, up and away!", -123, "http://example.com/foo#bar"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DC7X", () => { + // Various trailing tabs + const input: string = `a: b +seq: + - a +c: d #X +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "b", + seq: ["a"], + c: "d", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/00", () => { + // Trailing tabs in double quoted + const input: string = `"1 trailing\\t + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/01", () => { + // Trailing tabs in double quoted + const input: string = `"2 trailing\\t + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/02", () => { + // Trailing tabs in double quoted + const input: string = `"3 trailing\\ + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/03", () => { + // Trailing tabs in double quoted + const input: string = `"4 trailing\\ + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "4 trailing\t tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/04", () => { + // Trailing tabs in double quoted + const input: string = `"5 trailing + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "5 trailing tab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DE56/05", () => { + // Trailing tabs in double quoted + const input: string = `"6 trailing + tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "6 trailing tab"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/DFF7", () => { + // Spec Example 7.16. Flow Mapping Entries (using test.event for expected values) + const input: string = `{ +? explicit: entry, +implicit: entry, +? +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { explicit: "entry", implicit: "entry", null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DHP8", () => { + // Flow Sequence + const input: string = `[foo, bar, 42] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", 42]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK3J", () => { + // Zero indented block scalar with line that looks like a comment + const input: string = `--- > +line1 +# no comment +line3 +`; + + const parsed = YAML.parse(input); + + const expected: any = "line1 # no comment line3\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK4H", () => { + // Implicit key followed by newline + // Error test - expecting parse to fail + const input: string = `--- +[ key + : value ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/00", () => { + // Tabs that look like indentation + const input: string = `foo: + bar +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/01", () => { + // Tabs that look like indentation + // Error test - expecting parse to fail + const input: string = `foo: "bar + baz" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/02", () => { + // Tabs that look like indentation + const input: string = `foo: "bar + baz" +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar baz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/03", () => { + // Tabs that look like indentation + const input: string = ` +foo: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/04", () => { + // Tabs that look like indentation + const input: string = `foo: 1 + +bar: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/05", () => { + // Tabs that look like indentation + const input: string = `foo: 1 + +bar: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2 }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/DK95/06", () => { + // Tabs that look like indentation + // Error test - expecting parse to fail + const input: string = `foo: + a: 1 + b: 2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DK95/07", () => { + // Tabs that look like indentation + const input: string = `%YAML 1.2 + +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DK95/08", () => { + // Tabs that look like indentation + const input: string = `foo: "bar + baz " +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar baz \t \t " }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/DMG6", () => { + // Wrong indendation in Map + // Error test - expecting parse to fail + const input: string = `key: + ok: 1 + wrong: 2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/DWX9", () => { + // Spec Example 8.8. Literal Content + const input: string = `| + + + literal + + + text + + # Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\n\nliteral\n \n\ntext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/E76Z", () => { + // Aliases in Implicit Block Mapping + const input: string = `&a a: &b b +*b : *a +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a + + const expected: any = { a: "b", b: "a" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EB22", () => { + // Missing document-end marker before directive + // Error test - expecting parse to fail + const input: string = `--- +scalar1 # comment +%YAML 1.2 +--- +scalar2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/EHF6", () => { + // Tags for Flow Objects + const input: string = `!!map { + k: !!seq + [ a, !!str b] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + k: ["a", "b"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EW3V", () => { + // Wrong indendation in mapping + // Error test - expecting parse to fail + const input: string = `k1: v1 + k2: v2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/EX5H", () => { + // Multiline Scalar at Top Level [1.3] + const input: string = `--- +a +b + c +d + +e +`; + + const parsed = YAML.parse(input); + + const expected: any = "a b c d\ne"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/EXG3", () => { + // Three dashes and content without space [1.3] + const input: string = `--- +---word1 +word2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "---word1 word2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/F2C7", () => { + // Anchors and Tags + const input: string = ` - &a !!str a + - !!int 2 + - !!int &c 4 + - &d d +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", 2, 4, "d"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/F3CP", () => { + // Nested flow collections on one line + const input: string = `--- +{ a: [b, c, { d: [e, f] } ] } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: [ + "b", + "c", + { + d: ["e", "f"], + }, + ], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/F6MC", () => { + // More indented lines at the beginning of folded block scalars + const input: string = `--- +a: >2 + more indented + regular +b: >2 + + + more indented + regular +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: " more indented\nregular\n", b: "\n\n more indented\nregular\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/F8F9", () => { + // Spec Example 8.5. Chomping Trailing Lines + const input: string = ` # Strip + # Comments: +strip: |- + # text + + # Clip + # comments: + +clip: | + # text + + # Keep + # comments: + +keep: |+ + # text + + # Trail + # comments. +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "# text", clip: "# text\n", keep: "# text\n\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FBC9", () => { + // Allowed characters in plain scalars + const input: string = + "safe: a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\n !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\nsafe question mark: ?foo\nsafe colon: :foo\nsafe dash: -foo\n"; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { + safe: "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~", + "safe question mark": "?foo", + "safe colon": ":foo", + "safe dash": "-foo", + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/FH7J", () => { + // Tags on Empty Scalars (using test.event for expected values) + const input: string = `- !!str +- + !!null : a + b: !!str +- !!str : !!null +`; + + const parsed = YAML.parse(input); + + const expected: any = ["", { null: "a", b: "" }, { null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FP8R", () => { + // Zero indented block scalar + const input: string = `--- > +line1 +line2 +line3 +`; + + const parsed = YAML.parse(input); + + const expected: any = "line1 line2 line3\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FQ7F", () => { + // Spec Example 2.1. Sequence of Scalars + const input: string = `- Mark McGwire +- Sammy Sosa +- Ken Griffey +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Mark McGwire", "Sammy Sosa", "Ken Griffey"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/FRK4", () => { + // Spec Example 7.3. Completely Empty Flow Nodes (using test.event for expected values) + const input: string = `{ + ? foo :, + : bar, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: null, null: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FTA2", () => { + // Single block sequence with anchor and explicit document start + const input: string = `--- &sequence +- a +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/FUP4", () => { + // Flow Sequence in Flow Sequence + const input: string = `[a, [b, c]] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["a", ["b", "c"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G4RS", () => { + // Spec Example 2.17. Quoted Scalars + const input: string = `unicode: "Sosa did fine.\\u263A" +control: "\\b1998\\t1999\\t2000\\n" +hex esc: "\\x0d\\x0a is \\r\\n" + +single: '"Howdy!" he cried.' +quoted: ' # Not a ''comment''.' +tie-fighter: '|\\-*-/|' +`; + + const parsed = YAML.parse(input); + + const expected: any = { + unicode: "Sosa did fine.☺", + control: "\b1998\t1999\t2000\n", + "hex esc": "\r\n is \r\n", + single: '"Howdy!" he cried.', + quoted: " # Not a 'comment'.", + "tie-fighter": "|\\-*-/|", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G5U8", () => { + // Plain dashes in flow sequence + // Error test - expecting parse to fail + const input: string = `--- +- [-, -] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/G7JE", () => { + // Multiline implicit keys + // Error test - expecting parse to fail + const input: string = `a\\nb: 1 +c + d: 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/G992", () => { + // Spec Example 8.9. Folded Scalar + const input: string = `> + folded + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded text\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/G9HC", () => { + // Invalid anchor in zero indented sequence + // Error test - expecting parse to fail + const input: string = `--- +seq: +&anchor +- a +- b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/GDY7", () => { + // Comment that looks like a mapping key + // Error test - expecting parse to fail + const input: string = `key: value +this is #not a: key +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/GH63", () => { + // Mixed Block Mapping (explicit to implicit) + const input: string = `? a +: 1.3 +fifteen: d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 1.3, fifteen: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/GT5M", () => { + // Node anchor in sequence + // Error test - expecting parse to fail + const input: string = `- item1 +&node +- item2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/H2RW", () => { + // Blank lines + const input: string = `foo: 1 + +bar: 2 + +text: | + a + + b + + c + + d +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: 1, bar: 2, text: "a\n \nb\n\nc\n\nd\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/H3Z8", () => { + // Literal unicode + const input: string = `--- +wanted: love ♥ and peace ☮ +`; + + const parsed = YAML.parse(input); + + const expected: any = { wanted: "love ♥ and peace ☮" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/H7J7", () => { + // Node anchor not indented + // Error test - expecting parse to fail + const input: string = `key: &x +!!map + a: b +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/H7TQ", () => { + // Extra words on %YAML directive + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 foo +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HM87/00", () => { + // Scalars in flow start with syntax char + const input: string = `[:x] +`; + + const parsed = YAML.parse(input); + + const expected: any = [":x"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HM87/01", () => { + // Scalars in flow start with syntax char + const input: string = `[?x] +`; + + const parsed = YAML.parse(input); + + const expected: any = ["?x"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HMK4", () => { + // Spec Example 2.16. Indentation determines scope + const input: string = `name: Mark McGwire +accomplishment: > + Mark set a major league + home run record in 1998. +stats: | + 65 Home Runs + 0.278 Batting Average +`; + + const parsed = YAML.parse(input); + + const expected: any = { + name: "Mark McGwire", + accomplishment: "Mark set a major league home run record in 1998.\n", + stats: "65 Home Runs\n0.278 Batting Average\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HMQ5", () => { + // Spec Example 6.23. Node Properties + const input: string = `!!str &a1 "foo": + !!str bar +&a2 baz : *a1 +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a1 + + const expected: any = { foo: "bar", baz: "foo" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HRE5", () => { + // Double quoted scalar with escaped single quote + // Error test - expecting parse to fail + const input: string = `--- +double: "quoted \\' scalar" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HS5T", () => { + // Spec Example 7.12. Plain Lines + const input: string = `1st non-empty + + 2nd non-empty + 3rd non-empty +`; + + const parsed = YAML.parse(input); + + const expected: any = "1st non-empty\n2nd non-empty 3rd non-empty"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/HU3P", () => { + // Invalid Mapping in plain scalar + // Error test - expecting parse to fail + const input: string = `key: + word1 word2 + no: key +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/HWV9", () => { + // Document-end marker + const input: string = `... +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J3BT", () => { + // Spec Example 5.12. Tabs and Spaces + const input: string = `# Tabs and spaces +quoted: "Quoted " +block: | + void main() { + printf("Hello, world!\\n"); + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { quoted: "Quoted \t", block: 'void main() {\n\tprintf("Hello, world!\\n");\n}\n' }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J5UC", () => { + // Multiple Pair Block Mapping + const input: string = `foo: blue +bar: arrr +baz: jazz +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "blue", bar: "arrr", baz: "jazz" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J7PZ", () => { + // Spec Example 2.26. Ordered Mappings + const input: string = `# The !!omap tag is one of the optional types +# introduced for YAML 1.1. In 1.2, it is not +# part of the standard tags and should not be +# enabled by default. +# Ordered maps are represented as +# A sequence of mappings, with +# each mapping having one key +--- !!omap +- Mark McGwire: 65 +- Sammy Sosa: 63 +- Ken Griffy: 58 +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "Mark McGwire": 65 }, { "Sammy Sosa": 63 }, { "Ken Griffy": 58 }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J7VC", () => { + // Empty Lines Between Mapping Elements + const input: string = `one: 2 + + +three: 4 +`; + + const parsed = YAML.parse(input); + + const expected: any = { one: 2, three: 4 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/J9HZ", () => { + // Spec Example 2.9. Single Document with Two Comments + const input: string = `--- +hr: # 1998 hr ranking + - Mark McGwire + - Sammy Sosa +rbi: + # 1998 rbi ranking + - Sammy Sosa + - Ken Griffey +`; + + const parsed = YAML.parse(input); + + const expected: any = { + hr: ["Mark McGwire", "Sammy Sosa"], + rbi: ["Sammy Sosa", "Ken Griffey"], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/00", () => { + // Trailing whitespace in streams + const input: string = `- |+ + + +`; + + const parsed = YAML.parse(input); + + const expected: any = ["\n\n"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/01", () => { + // Trailing whitespace in streams + const input: string = `- |+ + +`; + + const parsed = YAML.parse(input); + + const expected: any = ["\n"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/JEF9/02", () => { + // Trailing whitespace in streams + const input: string = `- |+ + `; + + const parsed = YAML.parse(input); + + const expected: any = ["\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JHB9", () => { + // Spec Example 2.7. Two Documents in a Stream + const input: string = `# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["Mark McGwire", "Sammy Sosa", "Ken Griffey"], + ["Chicago Cubs", "St Louis Cardinals"], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JKF3", () => { + // Multiline unidented double quoted block key + // Error test - expecting parse to fail + const input: string = `- - "bar +bar": x +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/JQ4R", () => { + // Spec Example 8.14. Block Sequence + const input: string = `block sequence: + - one + - two : three +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "block sequence": ["one", { two: "three" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JR7V", () => { + // Question marks in scalars + const input: string = `- a?string +- another ? string +- key: value? +- [a?string] +- [another ? string] +- {key: value? } +- {key: value?} +- {key?: value } +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + "a?string", + "another ? string", + { key: "value?" }, + ["a?string"], + ["another ? string"], + { key: "value?" }, + { key: "value?" }, + { "key?": "value" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JS2J", () => { + // Spec Example 6.29. Node Anchors + const input: string = `First occurrence: &anchor Value +Second occurrence: *anchor +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = { "First occurrence": "Value", "Second occurrence": "Value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JTV5", () => { + // Block Mapping with Multiline Scalars + const input: string = `? a + true +: null + d +? e + 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = { "a true": "null d", "e 42": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/JY7Z", () => { + // Trailing content that looks like a mapping + // Error test - expecting parse to fail + const input: string = `key1: "quoted1" +key2: "quoted2" no key: nor value +key3: "quoted3" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/K3WX", () => { + // Colon and adjacent value after comment on next line + const input: string = `--- +{ "foo" # comment + :bar } +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K4SU", () => { + // Multiple Entry Block Sequence + const input: string = `- foo +- bar +- 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "bar", 42]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K527", () => { + // Spec Example 6.6. Line Folding + const input: string = `>- + trimmed + + + + as + space +`; + + const parsed = YAML.parse(input); + + const expected: any = "trimmed\n\n\nas space"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/K54U", () => { + // Tab after document header + const input: string = `--- scalar +`; + + const parsed = YAML.parse(input); + + const expected: any = "scalar"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/K858", () => { + // Spec Example 8.6. Empty Scalar Chomping + const input: string = `strip: >- + +clip: > + +keep: |+ + +`; + + const parsed = YAML.parse(input); + + const expected: any = { strip: "", clip: "", keep: "\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/00", () => { + // Inline tabs in double quoted + const input: string = `"1 inline\\ttab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/01", () => { + // Inline tabs in double quoted + const input: string = `"2 inline\\ tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "2 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KH5V/02", () => { + // Inline tabs in double quoted + const input: string = `"3 inline tab" +`; + + const parsed = YAML.parse(input); + + const expected: any = "3 inline\ttab"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/KK5P", () => { + // Various combinations of explicit block mappings (using test.event for expected values) + const input: string = `complex1: + ? - a +complex2: + ? - a + : b +complex3: + ? - a + : > + b +complex4: + ? > + a + : +complex5: + ? - a + : - b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + complex1: { a: null }, + complex2: { a: "b" }, + complex3: { a: "b\n" }, + complex4: { "a\n": null }, + complex5: { + a: ["b"], + }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KMK3", () => { + // Block Submapping + const input: string = `foo: + bar: 1 +baz: 2 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: { bar: 1 }, + baz: 2, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/KS4U", () => { + // Invalid item after end of flow sequence + // Error test - expecting parse to fail + const input: string = `--- +[ +sequence item +] +invalid item +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/KSS4", () => { + // Scalars on --- line + const input: string = `--- "quoted +string" +--- &node foo +`; + + const parsed = YAML.parse(input); + + // Note: Original YAML may have anchors/aliases + // Some values in the parsed result may be shared object references + + const expected: any = ["quoted string", "foo"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L24T/00", () => { + // Trailing line of spaces + const input: string = `foo: | + x + +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "x\n \n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L24T/01", () => { + // Trailing line of spaces + const input: string = `foo: | + x + `; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "x\n \n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L383", () => { + // Two scalar docs with trailing comments + const input: string = `--- foo # comment +--- foo # comment +`; + + const parsed = YAML.parse(input); + + const expected: any = ["foo", "foo"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/L94M", () => { + // Tags in Explicit Mapping + const input: string = `? !!str a +: !!int 47 +? c +: !!str d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 47, c: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/L9U5", () => { + // Spec Example 7.11. Plain Implicit Keys + const input: string = `implicit block key : [ + implicit flow key : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LE5A", () => { + // Spec Example 7.24. Flow Nodes + const input: string = `- !!str "a" +- 'b' +- &anchor "c" +- *anchor +- !!str +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: anchor + + const expected: any = ["a", "b", "c", "c", ""]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LHL4", () => { + // Invalid tag + // Error test - expecting parse to fail + const input: string = `--- +!invalid{}tag scalar +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/LP6E", () => { + // Whitespace After Scalars in Flow + const input: string = `- [a, b , c ] +- { "a" : b + , c : 'd' , + e : "f" + } +- [ ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [["a", "b", "c"], { a: "b", c: "d", e: "f" }, []]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LQZ7", () => { + // Spec Example 7.4. Double Quoted Implicit Keys + const input: string = `"implicit block key" : [ + "implicit flow key" : value, + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "implicit block key": [{ "implicit flow key": "value" }], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/LX3P", () => { + // Implicit Flow Mapping Key on one line (using test.event for expected values) + const input: string = `[flow]: block +`; + + const parsed = YAML.parse(input); + + const expected: any = { flow: "block" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M29M", () => { + // Literal Block Scalar + const input: string = `a: | + ab + + cd + ef + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "ab\n\ncd\nef\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M2N8/00", () => { + // Question mark edge cases (using test.event for expected values) + const input: string = `- ? : x +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "[object Object]": null }]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M2N8/01", () => { + // Question mark edge cases (using test.event for expected values) + const input: string = `? []: x +`; + + const parsed = YAML.parse(input); + + const expected: any = { "{\n ? []\n : x\n}": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M5C3", () => { + // Spec Example 8.21. Block Scalar Nodes + const input: string = `literal: |2 + value +folded: + !foo + >1 + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "value\n", folded: "value\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/M5DY", () => { + // Spec Example 2.11. Mapping between Sequences (using test.event for expected values) + const input: string = `? - Detroit Tigers + - Chicago cubs +: + - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Detroit Tigers,Chicago cubs": ["2001-07-23"], + "New York Yankees,Atlanta Braves": ["2001-07-02", "2001-08-12", "2001-08-14"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M6YH", () => { + // Block sequence indentation + const input: string = `- | + x +- + foo: bar +- + - 42 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["x\n", { foo: "bar" }, [42]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M7A3", () => { + // Spec Example 9.3. Bare Documents + const input: string = `Bare +document +... +# No document +... +| +%!PS-Adobe-2.0 # Not the first line +`; + + const parsed = YAML.parse(input); + + const expected: any = ["Bare document", "%!PS-Adobe-2.0 # Not the first line\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M7NX", () => { + // Nested flow collections + const input: string = `--- +{ + a: [ + b, c, { + d: [e, f] + } + ] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: [ + "b", + "c", + { + d: ["e", "f"], + }, + ], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/M9B4", () => { + // Spec Example 8.7. Literal Scalar + const input: string = `| + literal + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "literal\n\ttext\n"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/MJS9", () => { + // Spec Example 6.7. Block Folding + const input: string = `> + foo + + bar + + baz +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo \n\n\t bar\n\nbaz\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/00", () => { + // Directive variants + // Error test - expecting parse to fail + const input: string = `%YAML 1.1#... +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/MUS6/01", () => { + // Directive variants + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +--- +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/MUS6/02", () => { + // Directive variants + const input: string = `%YAML 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/03", () => { + // Directive variants + const input: string = `%YAML 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/04", () => { + // Directive variants + const input: string = `%YAML 1.1 # comment +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/05", () => { + // Directive variants + const input: string = `%YAM 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MUS6/06", () => { + // Directive variants + const input: string = `%YAMLL 1.1 +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MXS3", () => { + // Flow Mapping in Block Sequence + const input: string = `- {a: b} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MYW6", () => { + // Block Scalar Strip + const input: string = `|- + ab + + +... +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/MZX3", () => { + // Non-Specific Tags on Scalars + const input: string = `- plain +- "double quoted" +- 'single quoted' +- > + block +- plain again +`; + + const parsed = YAML.parse(input); + + const expected: any = ["plain", "double quoted", "single quoted", "block\n", "plain again"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/N4JP", () => { + // Bad indentation in mapping + // Error test - expecting parse to fail + const input: string = `map: + key1: "quoted1" + key2: "bad indentation" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/N782", () => { + // Invalid document markers in flow style + // Error test - expecting parse to fail + const input: string = `[ +--- , +... +] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/NAT4", () => { + // Various empty or newline only quoted strings + const input: string = `--- +a: ' + ' +b: ' + ' +c: " + " +d: " + " +e: ' + + ' +f: " + + " +g: ' + + + ' +h: " + + + " +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: " ", + b: " ", + c: " ", + d: " ", + e: "\n", + f: "\n", + g: "\n\n", + h: "\n\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NB6Z", () => { + // Multiline plain value with tabs on empty lines + const input: string = `key: + value + with + + tabs +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value with\ntabs" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NHX8", () => { + // Empty Lines at End of Document (using test.event for expected values) + const input: string = `: + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NJ66", () => { + // Multiline plain flow mapping key + const input: string = `--- +- { single line: value} +- { multi + line: value} +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "single line": "value" }, { "multi line": "value" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NKF9", () => { + // Empty keys in block and flow mapping (using test.event for expected values) + const input: string = `--- +key: value +: empty key +--- +{ + key: value, : empty key +} +--- +# empty key and value +: +--- +# empty key and value +{ : } +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { key: "value", null: "empty key" }, + { key: "value", null: "empty key" }, + { null: null }, + { null: null }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/NP9H", () => { + // Spec Example 7.5. Double Quoted Line Breaks + const input: string = `"folded +to a space, + +to a line feed, or \\ + \\ non-content" +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded to a space,\nto a line feed, or \t \tnon-content"; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/P2AD", () => { + // Spec Example 8.1. Block Scalar Header + const input: string = `- | # Empty header↓ + literal +- >1 # Indentation indicator↓ + folded +- |+ # Chomping indicator↓ + keep + +- >1- # Both indicators↓ + strip +`; + + const parsed = YAML.parse(input); + + const expected: any = ["literal\n", " folded\n", "keep\n\n", " strip"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/P2EQ", () => { + // Invalid sequene item on same line as previous item + // Error test - expecting parse to fail + const input: string = `--- +- { y: z }- invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/P76L", () => { + // Spec Example 6.19. Secondary Tag Handle + const input: string = `%TAG !! tag:example.com,2000:app/ +--- +!!int 1 - 3 # Interval, not integer +`; + + const parsed = YAML.parse(input); + + const expected: any = "1 - 3"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/P94K", () => { + // Spec Example 6.11. Multi-Line Comments + const input: string = `key: # Comment + # lines + value + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PBJ2", () => { + // Spec Example 2.3. Mapping Scalars to Sequences + const input: string = `american: + - Boston Red Sox + - Detroit Tigers + - New York Yankees +national: + - New York Mets + - Chicago Cubs + - Atlanta Braves +`; + + const parsed = YAML.parse(input); + + const expected: any = { + american: ["Boston Red Sox", "Detroit Tigers", "New York Yankees"], + national: ["New York Mets", "Chicago Cubs", "Atlanta Braves"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PRH3", () => { + // Spec Example 7.9. Single Quoted Lines + const input: string = `' 1st non-empty + + 2nd non-empty + 3rd non-empty ' +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/PUW8", () => { + // Document start on last line + const input: string = `--- +a: b +--- +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ a: "b" }, null]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/PW8X", () => { + // Anchors on Empty Scalars (using test.event for expected values) + const input: string = `- &a +- a +- + &a : a + b: &b +- + &c : &a +- + ? &d +- + ? &e + : &a +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, "a", { null: "a", b: null }, { null: null }, { null: null }, { null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q4CL", () => { + // Trailing content after quoted value + // Error test - expecting parse to fail + const input: string = `key1: "quoted1" +key2: "quoted2" trailing content +key3: "quoted3" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Q5MG", () => { + // Tab at beginning of line followed by a flow mapping + const input: string = ` {} +`; + + const parsed = YAML.parse(input); + + const expected: any = {}; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q88A", () => { + // Spec Example 7.23. Flow Content + const input: string = `- [ a, b ] +- { a: b } +- "a" +- 'b' +- c +`; + + const parsed = YAML.parse(input); + + const expected: any = [["a", "b"], { a: "b" }, "a", "b", "c"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q8AD", () => { + // Spec Example 7.5. Double Quoted Line Breaks [1.3] + const input: string = `--- +"folded +to a space, + +to a line feed, or \\ + \\ non-content" +`; + + const parsed = YAML.parse(input); + + const expected: any = "folded to a space,\nto a line feed, or \t \tnon-content"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Q9WF", () => { + // Spec Example 6.12. Separation Spaces (using test.event for expected values) + const input: string = `{ first: Sammy, last: Sosa }: +# Statistics: + hr: # Home runs + 65 + avg: # Average + 0.278 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "[object Object]": { hr: 65, avg: 0.278 }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/QB6E", () => { + // Wrong indented multiline quoted scalar + // Error test - expecting parse to fail + const input: string = `--- +quoted: "a +b +c" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/QF4Y", () => { + // Spec Example 7.19. Single Pair Flow Mappings + const input: string = `[ +foo: bar +] +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ foo: "bar" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/QLJ7", () => { + // Tag shorthand used in documents but only defined in the first + // Error test - expecting parse to fail + const input: string = `%TAG !prefix! tag:example.com,2011: +--- !prefix!A +a: b +--- !prefix!B +c: d +--- !prefix!C +e: f +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/QT73", () => { + // Comment and document-end marker + const input: string = `# comment +... +`; + + const parsed = YAML.parse(input); + + const expected: any = null; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/R4YG", () => { + // Spec Example 8.2. Block Indentation Indicator + const input: string = `- | + detected +- > + + + # detected +- |1 + explicit +- > + + detected +`; + + const parsed = YAML.parse(input); + + const expected: any = ["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/R52L", () => { + // Nested flow mapping sequence and mappings + const input: string = `--- +{ top1: [item1, {key2: value2}, item3], top2: value2 } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: ["item1", { key2: "value2" }, "item3"], + top2: "value2", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RHX7", () => { + // YAML directive without document end marker + // Error test - expecting parse to fail + const input: string = `--- +key: value +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/RLU9", () => { + // Sequence Indent + const input: string = `foo: +- 42 +bar: + - 44 +`; + + const parsed = YAML.parse(input); + + const expected: any = { + foo: [42], + bar: [44], + }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/RR7F", () => { + // Mixed Block Mapping (implicit to explicit) + const input: string = `a: 4.2 +? d +: 23 +`; + + const parsed = YAML.parse(input); + + const expected: any = { d: 23, a: 4.2 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RTP8", () => { + // Spec Example 9.2. Document Markers + const input: string = `%YAML 1.2 +--- +Document +... # Suffix +`; + + const parsed = YAML.parse(input); + + const expected: any = "Document"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RXY3", () => { + // Invalid document-end marker in single quoted string + // Error test - expecting parse to fail + const input: string = `--- +' +... +' +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/RZP5", () => { + // Various Trailing Comments [1.3] (using test.event for expected values) + const input: string = `a: "double + quotes" # lala +b: plain + value # lala +c : #lala + d +? # lala + - seq1 +: # lala + - #lala + seq2 +e: &node # lala + - x: y +block: > # lala + abcde +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "double quotes", + b: "plain value", + c: "d", + seq1: ["seq2"], + e: [{ x: "y" }], + block: "abcde\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/RZT7", () => { + // Spec Example 2.28. Log File + const input: string = `--- +Time: 2001-11-23 15:01:42 -5 +User: ed +Warning: + This is an error message + for the log file +--- +Time: 2001-11-23 15:02:31 -5 +User: ed +Warning: + A slightly different error + message. +--- +Date: 2001-11-23 15:03:17 -5 +User: ed +Fatal: + Unknown variable "bar" +Stack: + - file: TopClass.py + line: 23 + code: | + x = MoreObject("345\\n") + - file: MoreClass.py + line: 58 + code: |- + foo = bar +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { Time: "2001-11-23 15:01:42 -5", User: "ed", Warning: "This is an error message for the log file" }, + { Time: "2001-11-23 15:02:31 -5", User: "ed", Warning: "A slightly different error message." }, + { + Date: "2001-11-23 15:03:17 -5", + User: "ed", + Fatal: 'Unknown variable "bar"', + Stack: [ + { file: "TopClass.py", line: 23, code: 'x = MoreObject("345\\n")\n' }, + { file: "MoreClass.py", line: 58, code: "foo = bar" }, + ], + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S3PD", () => { + // Spec Example 8.18. Implicit Block Mapping Entries (using test.event for expected values) + const input: string = `plain key: in-line value +: # Both empty +"quoted key": +- entry +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "plain key": "in-line value", + null: null, + "quoted key": ["entry"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S4GJ", () => { + // Invalid text after block scalar indicator + // Error test - expecting parse to fail + const input: string = `--- +folded: > first line + second line +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/S4JQ", () => { + // Spec Example 6.28. Non-Specific Tags + const input: string = `# Assuming conventional resolution: +- "12" +- 12 +- ! 12 +`; + + const parsed = YAML.parse(input); + + const expected: any = ["12", 12, "12"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S4T7", () => { + // Document with footer + const input: string = `aaa: bbb +... +`; + + const parsed = YAML.parse(input); + + const expected: any = { aaa: "bbb" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S7BG", () => { + // Colon followed by comma + const input: string = `--- +- :, +`; + + const parsed = YAML.parse(input); + + const expected: any = [":,"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/S98Z", () => { + // Block scalar with more spaces than first content line + // Error test - expecting parse to fail + const input: string = `empty block scalar: > + + + + # comment +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/S9E8", () => { + // Spec Example 5.3. Block Structure Indicators + const input: string = `sequence: +- one +- two +mapping: + ? sky + : blue + sea : green +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["one", "two"], + mapping: { sky: "blue", sea: "green" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SBG9", () => { + // Flow Sequence in Flow Mapping (using test.event for expected values) + const input: string = `{a: [b, c], [d, e]: f} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: ["b", "c"], + "d,e": "f", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SF5V", () => { + // Duplicate YAML directive + // Error test - expecting parse to fail + const input: string = `%YAML 1.2 +%YAML 1.2 +--- +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SKE5", () => { + // Anchor before zero indented sequence + const input: string = `--- +seq: + &anchor +- a +- b +`; + + const parsed = YAML.parse(input); + + const expected: any = { + seq: ["a", "b"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SM9W/00", () => { + // Single character streams + const input: string = "-"; + + const parsed = YAML.parse(input); + + const expected: any = [null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SM9W/01", () => { + // Single character streams (using test.event for expected values) + const input: string = ":"; + + const parsed = YAML.parse(input); + + const expected: any = { null: null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SR86", () => { + // Anchor plus Alias + // Error test - expecting parse to fail + const input: string = `key1: &a value +key2: &b *a +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SSW6", () => { + // Spec Example 7.7. Single Quoted Characters [1.3] + const input: string = `--- +'here''s to "quotes"' +`; + + const parsed = YAML.parse(input); + + const expected: any = 'here\'s to "quotes"'; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/SU5Z", () => { + // Comment without whitespace after doublequoted scalar + // Error test - expecting parse to fail + const input: string = `key: "value"# invalid comment +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SU74", () => { + // Anchor and alias as mapping key + // Error test - expecting parse to fail + const input: string = `key1: &alias value1 +&b *alias : value2 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SY6V", () => { + // Anchor before sequence entry on same line + // Error test - expecting parse to fail + const input: string = `&anchor - sequence entry +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/SYW4", () => { + // Spec Example 2.2. Mapping Scalars to Scalars + const input: string = `hr: 65 # Home runs +avg: 0.278 # Batting average +rbi: 147 # Runs Batted In +`; + + const parsed = YAML.parse(input); + + const expected: any = { hr: 65, avg: 0.278, rbi: 147 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T26H", () => { + // Spec Example 8.8. Literal Content [1.3] + const input: string = `--- | + + + literal + + + text + + # Comment +`; + + const parsed = YAML.parse(input); + + const expected: any = "\n\nliteral\n \n\ntext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T4YY", () => { + // Spec Example 7.9. Single Quoted Lines [1.3] + const input: string = `--- +' 1st non-empty + + 2nd non-empty + 3rd non-empty ' +`; + + const parsed = YAML.parse(input); + + const expected: any = " 1st non-empty\n2nd non-empty 3rd non-empty "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T5N4", () => { + // Spec Example 8.7. Literal Scalar [1.3] + const input: string = `--- | + literal + text + + +`; + + const parsed = YAML.parse(input); + + const expected: any = "literal\n\ttext\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/T833", () => { + // Flow mapping missing a separating comma + // Error test - expecting parse to fail + const input: string = `--- +{ + foo: 1 + bar: 2 } +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/TD5N", () => { + // Invalid scalar after sequence + // Error test - expecting parse to fail + const input: string = `- item1 +- item2 +invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/TE2A", () => { + // Spec Example 8.16. Block Mappings + const input: string = `block mapping: + key: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "block mapping": { key: "value" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/TL85", () => { + // Spec Example 6.8. Flow Folding + const input: string = `" + foo + + bar + + baz +" +`; + + const parsed = YAML.parse(input); + + const expected: any = " foo\nbar\nbaz "; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/TS54", () => { + // Folded Block Scalar + const input: string = `> + ab + cd + + ef + + + gh +`; + + const parsed = YAML.parse(input); + + const expected: any = "ab cd\nef\n\ngh\n"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U3C3", () => { + // Spec Example 6.16. “TAG” directive + const input: string = `%TAG !yaml! tag:yaml.org,2002: +--- +!yaml!str "foo" +`; + + const parsed = YAML.parse(input); + + const expected: any = "foo"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U3XV", () => { + // Node and Mapping Key Anchors + const input: string = `--- +top1: &node1 + &k1 key1: one +top2: &node2 # comment + key2: two +top3: + &k3 key3: three +top4: + &node4 + &k4 key4: four +top5: + &node5 + key5: five +top6: &val6 + six +top7: + &val7 seven +`; + + const parsed = YAML.parse(input); + + const expected: any = { + top1: { key1: "one" }, + top2: { key2: "two" }, + top3: { key3: "three" }, + top4: { key4: "four" }, + top5: { key5: "five" }, + top6: "six", + top7: "seven", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/U44R", () => { + // Bad indentation in mapping (2) + // Error test - expecting parse to fail + const input: string = `map: + key1: "quoted1" + key2: "bad indentation" +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/U99R", () => { + // Invalid comma in tag + // Error test - expecting parse to fail + const input: string = `- !!str, xxx +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/U9NS", () => { + // Spec Example 2.8. Play by Play Feed from a Game + const input: string = `--- +time: 20:03:20 +player: Sammy Sosa +action: strike (miss) +... +--- +time: 20:03:47 +player: Sammy Sosa +action: grand slam +... +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { time: "20:03:20", player: "Sammy Sosa", action: "strike (miss)" }, + { time: "20:03:47", player: "Sammy Sosa", action: "grand slam" }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UDM2", () => { + // Plain URL in flow mapping + const input: string = `- { url: http://example.org } +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ url: "http://example.org" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UDR7", () => { + // Spec Example 5.4. Flow Collection Indicators + const input: string = `sequence: [ one, two, ] +mapping: { sky: blue, sea: green } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + sequence: ["one", "two"], + mapping: { sky: "blue", sea: "green" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UGM3", () => { + // Spec Example 2.27. Invoice + const input: string = `--- ! +invoice: 34843 +date : 2001-01-23 +bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +ship-to: *id001 +product: + - sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 + - sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const sharedAddress: any = { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, + }; + const expected = { + "invoice": 34843, + "date": "2001-01-23", + "bill-to": sharedAddress, + "ship-to": sharedAddress, + "product": [ + { + sku: "BL394D", + quantity: 4, + description: "Basketball", + price: 450, + }, + { + sku: "BL4438H", + quantity: 1, + description: "Super Hoop", + price: 2392, + }, + ], + "tax": 251.42, + "total": 4443.52, + "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/00", () => { + // Syntax character edge cases (using test.event for expected values) + const input: string = `- : +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ null: null }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/01", () => { + // Syntax character edge cases + const input: string = `:: +`; + + const parsed = YAML.parse(input); + + const expected: any = { ":": null }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UKK6/02", () => { + // Syntax character edge cases (using test.event for expected values) + const input: string = `! +`; + + const parsed = YAML.parse(input); + + const expected: any = ""; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UT92", () => { + // Spec Example 9.4. Explicit Documents + const input: string = `--- +{ matches +% : 20 } +... +--- +# Empty +... +`; + + const parsed = YAML.parse(input); + + const expected: any = [{ "matches %": 20 }, null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/UV7Q", () => { + // Legal tab after indentation + const input: string = `x: + - x + x +`; + + const parsed = YAML.parse(input); + + const expected: any = { + x: ["x x"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/V55R", () => { + // Aliases in Block Sequence + const input: string = `- &a a +- &b b +- *a +- *b +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + // Detected anchors that are referenced: a, b + + const expected: any = ["a", "b", "a", "b"]; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/V9D5", () => { + // Spec Example 8.19. Compact Block Mappings (using test.event for expected values) + const input: string = `- sun: yellow +- ? earth: blue + : moon: white +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + { sun: "yellow" }, + { + "[object Object]": { moon: "white" }, + }, + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/VJP3/00", () => { + // Flow collections over many lines + // Error test - expecting parse to fail + const input: string = `k: { +k +: +v +} +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/VJP3/01", () => { + // Flow collections over many lines + const input: string = `k: { + k + : + v + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + k: { k: "v" }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W42U", () => { + // Spec Example 8.15. Block Sequence Entry Types + const input: string = `- # Empty +- | + block node +- - one # Compact + - two # sequence +- one: two # Compact mapping +`; + + const parsed = YAML.parse(input); + + const expected: any = [null, "block node\n", ["one", "two"], { one: "two" }]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W4TN", () => { + // Spec Example 9.5. Directives Documents + const input: string = `%YAML 1.2 +--- | +%!PS-Adobe-2.0 +... +%YAML 1.2 +--- +# Empty +... +`; + + const parsed = YAML.parse(input); + + const expected: any = ["%!PS-Adobe-2.0\n", null]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W5VH", () => { + // Allowed characters in alias + const input: string = `a: &:@*!$": scalar a +b: *:@*!$": +`; + + const parsed = YAML.parse(input); + + // This YAML has anchors and aliases - creating shared references + + const expected: any = { a: "scalar a", b: "scalar a" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/W9L4", () => { + // Literal block scalar with more spaces in first line + // Error test - expecting parse to fail + const input: string = `--- +block scalar: | + + more spaces at the beginning + are invalid +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/WZ62", () => { + // Spec Example 7.2. Empty Content + const input: string = `{ + foo : !!str, + !!str : bar, +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "", "": "bar" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/X38W", () => { + // Aliases in Flow Objects + // Special case: *a references the same array as first key, creating duplicate key + const input: string = `{ &a [a, &b b]: *b, *a : [c, *b, d]} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "a,b": ["c", "b", "d"], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/X4QW", () => { + // Comment without whitespace after block scalar indicator + // Error test - expecting parse to fail + const input: string = `block: ># comment + scalar +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/X8DW", () => { + // Explicit key and value seperated by comment + const input: string = `--- +? key +# comment +: value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/XLQ9", () => { + // Multiline scalar that looks like a YAML directive + const input: string = `--- +scalar +%YAML 1.2 +`; + + const parsed = YAML.parse(input); + + const expected: any = "scalar %YAML 1.2"; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/XV9V", () => { + // Spec Example 6.5. Empty Lines [1.3] + const input: string = `Folding: + "Empty line + + as a line feed" +Chomping: | + Clipped empty lines + + +`; + + const parsed = YAML.parse(input); + + const expected: any = { Folding: "Empty line\nas a line feed", Chomping: "Clipped empty lines\n" }; + + expect(parsed).toEqual(expected); +}); + +test.todo("yaml-test-suite/XW4D", () => { + // Various Trailing Comments (using test.event for expected values) + const input: string = `a: "double + quotes" # lala +b: plain + value # lala +c : #lala + d +? # lala + - seq1 +: # lala + - #lala + seq2 +e: + &node # lala + - x: y +block: > # lala + abcde +`; + + const parsed = YAML.parse(input); + + const expected: any = { + a: "double quotes", + b: "plain value", + c: "d", + seq1: ["seq2"], + e: [{ x: "y" }], + block: "abcde\n", + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y2GN", () => { + // Anchor with colon in the middle + const input: string = `--- +key: &an:chor value +`; + + const parsed = YAML.parse(input); + + const expected: any = { key: "value" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/000", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `foo: | + +bar: 1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/001", () => { + // Tabs in various contexts + const input: string = `foo: | + +bar: 1 +`; + + const parsed = YAML.parse(input); + + const expected: any = { foo: "\t\n", bar: 1 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/002", () => { + // Tabs in various contexts + const input: string = `- [ + + foo + ] +`; + + const parsed = YAML.parse(input); + + const expected: any = [["foo"]]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Y79Y/003", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- [ + foo, + foo + ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/004", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/005", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `- - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/006", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/007", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? - +: - +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/Y79Y/008", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? key: +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/009", () => { + // Tabs in various contexts + // Error test - expecting parse to fail + const input: string = `? key: +: key: +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Y79Y/010", () => { + // Tabs in various contexts + const input: string = `- -1 +`; + + const parsed = YAML.parse(input); + + const expected: any = [-1]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/YD5X", () => { + // Spec Example 2.5. Sequence of Sequences + const input: string = `- [name , hr, avg ] +- [Mark McGwire, 65, 0.278] +- [Sammy Sosa , 63, 0.288] +`; + + const parsed = YAML.parse(input); + + const expected: any = [ + ["name", "hr", "avg"], + ["Mark McGwire", 65, 0.278], + ["Sammy Sosa", 63, 0.288], + ]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/YJV2", () => { + // Dash in flow sequence + // Error test - expecting parse to fail + const input: string = `[-] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/Z67P", () => { + // Spec Example 8.21. Block Scalar Nodes [1.3] + const input: string = `literal: |2 + value +folded: !foo >1 + value +`; + + const parsed = YAML.parse(input); + + const expected: any = { literal: "value\n", folded: "value\n" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/Z9M4", () => { + // Spec Example 6.22. Global Tag Prefix + const input: string = `%TAG !e! tag:example.com,2000:app/ +--- +- !e!foo "bar" +`; + + const parsed = YAML.parse(input); + + const expected: any = ["bar"]; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZCZ6", () => { + // Invalid mapping in plain single line value + // Error test - expecting parse to fail + const input: string = `a: b: c: d +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/ZF4X", () => { + // Spec Example 2.6. Mapping of Mappings + const input: string = `Mark McGwire: {hr: 65, avg: 0.278} +Sammy Sosa: { + hr: 63, + avg: 0.288 + } +`; + + const parsed = YAML.parse(input); + + const expected: any = { + "Mark McGwire": { hr: 65, avg: 0.278 }, + "Sammy Sosa": { hr: 63, avg: 0.288 }, + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZH7C", () => { + // Anchors in Mapping + const input: string = `&a a: b +c: &d d +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: "b", c: "d" }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZK9H", () => { + // Nested top level flow mapping + const input: string = `{ key: [[[ + value + ]]] +} +`; + + const parsed = YAML.parse(input); + + const expected: any = { + key: [[["value"]]], + }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZL4Z", () => { + // Invalid nested mapping + // Error test - expecting parse to fail + const input: string = `--- +a: 'b': c +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test("yaml-test-suite/ZVH3", () => { + // Wrong indented sequence item + // Error test - expecting parse to fail + const input: string = `- key: value + - item1 +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); + +test.todo("yaml-test-suite/ZWK4", () => { + // Key with anchor after missing explicit mapping value + const input: string = `--- +a: 1 +? b +&anchor c: 3 +`; + + const parsed = YAML.parse(input); + + const expected: any = { a: 1, b: null, c: 3 }; + + expect(parsed).toEqual(expected); +}); + +test("yaml-test-suite/ZXT5", () => { + // Implicit key followed by newline and adjacent value + // Error test - expecting parse to fail + const input: string = `[ "key" + :value ] +`; + + expect(() => { + return YAML.parse(input); + }).toThrow(); +}); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index df904d64e9..f3e0d949ba 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -1,5 +1,6 @@ -import { YAML } from "bun"; +import { YAML, file } from "bun"; import { describe, expect, test } from "bun:test"; +import { join } from "path"; describe("Bun.YAML", () => { describe("parse", () => { @@ -702,6 +703,64 @@ production: }, }); }); + + test("issue 22659", () => { + const input1 = `- test2: next + test1: +`; + expect(YAML.parse(input1)).toMatchInlineSnapshot(` + [ + { + "test1": "+", + "test2": "next", + }, + ] + `); + const input2 = `- test1: + + test2: next`; + expect(YAML.parse(input2)).toMatchInlineSnapshot(` + [ + { + "test1": "+", + "test2": "next", + }, + ] + `); + }); + + test("issue 22392", () => { + const input = ` +foo: "some + ... + string" +`; + expect(YAML.parse(input)).toMatchInlineSnapshot(` + { + "foo": "some ... string", + } + `); + }); + + test("issue 22286", async () => { + const input1 = ` +my_anchor: &MyAnchor "MyAnchor" + +my_config: + *MyAnchor : + some_key: "some_value" +`; + expect(YAML.parse(input1)).toMatchInlineSnapshot(` + { + "my_anchor": "MyAnchor", + "my_config": { + "MyAnchor": { + "some_key": "some_value", + }, + }, + } + `); + const input2 = await file(join(import.meta.dir, "fixtures", "AHatInTime.yaml")).text(); + expect(YAML.parse(input2)).toMatchSnapshot(); + }); }); describe("stringify", () => { From bf26d725abaa93445240b7bd6152af0677ce6a8f Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sun, 5 Oct 2025 17:22:55 -0800 Subject: [PATCH 036/391] scripts/runner: pass TEST_SERIAL_ID for proper parallelism handling (#23031) adds environment variable for proper tmpdir setup actual fix for https://github.com/oven-sh/bun/commit/d2a4fb8124163b26cd9df65d838a73e6887d54c0 (which was reverted) this fixes flakyness in node:fs and node:cluster when using scripts/runner.node.mjs locally with the --parallel flag --- scripts/runner.node.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index fd9aee180b..cdf824ddfb 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -80,6 +80,7 @@ function getNodeParallelTestTimeout(testPath) { if (testPath.includes("test-dns")) { return 90_000; } + if (!isCI) return 60_000; // everything slower in debug mode return 20_000; } @@ -449,7 +450,7 @@ async function runTests() { if (parallelism > 1) { console.log(grouptitle); - result = await fn(); + result = await fn(index); } else { result = await startGroup(grouptitle, fn); } @@ -469,6 +470,7 @@ async function runTests() { const label = `${getAnsi(color)}[${index}/${total}] ${title} - ${error}${getAnsi("reset")}`; startGroup(label, () => { if (parallelism > 1) return; + if (!isCI) return; process.stderr.write(stdoutPreview); }); @@ -671,7 +673,9 @@ async function runTests() { const title = join(relative(cwd, vendorPath), testPath).replace(/\\/g, "/"); if (testRunner === "bun") { - await runTest(title, () => spawnBunTest(execPath, testPath, { cwd: vendorPath })); + await runTest(title, index => + spawnBunTest(execPath, testPath, { cwd: vendorPath, env: { TEST_SERIAL_ID: index } }), + ); } else { const testRunnerPath = join(cwd, "test", "runners", `${testRunner}.ts`); if (!existsSync(testRunnerPath)) { @@ -1298,6 +1302,7 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { * @param {object} [opts] * @param {string} [opts.cwd] * @param {string[]} [opts.args] + * @param {object} [opts.env] * @returns {Promise} */ async function spawnBunTest(execPath, testPath, opts = { cwd }) { @@ -1331,6 +1336,7 @@ async function spawnBunTest(execPath, testPath, opts = { cwd }) { const env = { GITHUB_ACTIONS: "true", // always true so annotations are parsed + ...opts["env"], }; if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(relative(cwd, absPath))) { env.BUN_JSC_validateExceptionChecks = "1"; From dd08a707e285bb0f7a5d965dad2dbeb69fe45366 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sun, 5 Oct 2025 18:58:26 -0700 Subject: [PATCH 037/391] update `yaml-test-suite` test generator script (#23277) ### What does this PR do? Adds `expect().toBe()` checks for anchors/aliases. Also adds git commit the tests were translated from. ### How did you verify your code works? Manually --- .../yaml/translate_yaml_test_suite_to_bun.py | 164 +++++++++++++----- test/js/bun/yaml/yaml-test-suite.test.ts | 69 +++++--- 2 files changed, 168 insertions(+), 65 deletions(-) diff --git a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py index 002af37c51..7bf930b3d6 100644 --- a/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py +++ b/test/js/bun/yaml/translate_yaml_test_suite_to_bun.py @@ -323,6 +323,40 @@ def detect_shared_references(yaml_content): return shared_refs +def generate_shared_reference_tests(yaml_content, parsed_path="parsed"): + """Generate toBe() tests for shared references based on anchors/aliases in YAML.""" + tests = [] + + # Common patterns for shared references + patterns = [ + # bill-to/ship-to pattern + (r'bill-to:\s*&(\w+)', r'ship-to:\s*\*\1', 'bill-to', 'ship-to'), + # Array items with anchors + (r'-\s*&(\w+)\s+', r'-\s*\*\1', None, None), + # Map values with anchors + (r':\s*&(\w+)\s+', r':\s*\*\1', None, None), + ] + + # Check for bill-to/ship-to pattern specifically + if 'bill-to:' in yaml_content and 'ship-to:' in yaml_content: + if re.search(r'bill-to:\s*&\w+', yaml_content) and re.search(r'ship-to:\s*\*\w+', yaml_content): + tests.append(f' // Shared reference check: bill-to and ship-to should be the same object') + tests.append(f' expect({parsed_path}["bill-to"]).toBe({parsed_path}["ship-to"]);') + + # Check for x-foo pattern (common in OpenAPI specs) + if re.search(r'x-\w+:\s*&\w+', yaml_content): + anchor_match = re.search(r'x-(\w+):\s*&(\w+)', yaml_content) + if anchor_match: + field_name = f'x-{anchor_match.group(1)}' + anchor_name = anchor_match.group(2) + # Find aliases to this anchor + alias_pattern = rf'\*{anchor_name}\b' + if re.search(alias_pattern, yaml_content): + tests.append(f' // Shared reference check: anchor {anchor_name}') + # This is generic - would need more context to generate specific tests + + return tests + def generate_expected_with_shared_refs(json_data, yaml_content): """Generate expected object with shared references for anchors/aliases.""" shared_refs = detect_shared_references(yaml_content) @@ -498,6 +532,23 @@ test("{test_name}", () => {{ ''' # Special handling for known problematic tests + if test_name == "yaml-test-suite/2SXE": + # 2SXE has complex anchor on key itself, not a shared reference case + test = f''' +test("{test_name}", () => {{ + // {description} + // Note: &a anchors the key "key" itself, *a references that string + const input: string = {format_js_string(yaml_content)}; + + const parsed = YAML.parse(input); + + const expected: any = {{ key: "value", foo: "key" }}; + + expect(parsed).toEqual(expected); +}}); +''' + return test + if test_name == "yaml-test-suite/X38W": # X38W has alias key that creates duplicate - yaml package doesn't handle this correctly # The correct output is just one key "a,b" with value ["c", "b", "d"] @@ -738,33 +789,20 @@ test("{test_name}", () => {{ # Try to identify simple cases if 'bill-to: &' in yaml_content and 'ship-to: *' in yaml_content: # Common pattern: bill-to/ship-to sharing - test += ''' - const sharedAddress: any = ''' - # Find the shared object from expected data stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) - if isinstance(stringified_value, dict): - if 'bill-to' in stringified_value: - shared_obj = stringified_value.get('bill-to') - test += json_to_js_literal(shared_obj) + ';' - # Now create expected with shared ref - test += ''' - const expected = ''' - # Build object with shared reference - test += '{\n' - for key, value in stringified_value.items(): - if key == 'bill-to': - test += f' "bill-to": sharedAddress,\n' - elif key == 'ship-to' and value == shared_obj: - test += f' "ship-to": sharedAddress,\n' - else: - test += f' "{escape_js_string(key)}": {json_to_js_literal(value)},\n' - test = test.rstrip(',\n') + '\n };' - else: - # Fallback to regular generation - stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) - expected_str = json_to_js_literal(stringified_value) - test += f''' - const expected: any = {expected_str};''' + if isinstance(stringified_value, dict) and 'bill-to' in stringified_value: + # Generate expected value normally with toBe check + expected_str = json_to_js_literal(stringified_value) + test += f''' + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected); + + // Verify shared references - bill-to and ship-to should be the same object + expect((parsed as any)["bill-to"]).toBe((parsed as any)["ship-to"]); +}}); +''' + return test else: # Fallback to regular generation stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) @@ -774,28 +812,60 @@ test("{test_name}", () => {{ else: # Generic anchor/alias case # Look for patterns like "- &anchor value" and "- *anchor" - anchor_matches = re.findall(r'&(\w+)\s+(.+?)(?:\n|$)', yaml_content) + anchor_matches = re.findall(r'&(\w+)', yaml_content) alias_matches = re.findall(r'\*(\w+)', yaml_content) if anchor_matches and alias_matches: # Build shared values based on anchors - anchor_vars = {} - for anchor_name, _ in anchor_matches: - if anchor_name in [a for a in alias_matches]: - # This anchor is referenced - anchor_vars[anchor_name] = f'shared_{anchor_name}' + shared_anchors = [] + for anchor_name in set(anchor_matches): + if anchor_name in alias_matches: + # This anchor is referenced by an alias + shared_anchors.append(anchor_name) - if anchor_vars and isinstance(expected_value, (list, dict)): - # Try to detect which values are shared - test += f''' - // Detected anchors that are referenced: {', '.join(anchor_vars.keys())} -''' - # For now, just generate the expected normally with a note + if shared_anchors and isinstance(expected_value, (list, dict)): + # Generate the expected value stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) expected_str = json_to_js_literal(stringified_value) + + # Build toBe checks based on detected patterns + toBe_checks = [] + + # Try to detect specific patterns for toBe checks + # Pattern 1: Array with repeated elements (- &anchor value, - *anchor) + for anchor in shared_anchors: + # Check if it's in an array context + if re.search(rf'-\s+&{anchor}\s+', yaml_content) and re.search(rf'-\s+\*{anchor}', yaml_content): + # This might be array elements - but hard to know indices without parsing + pass + # Check if it's in mapping values (not keys) + # Pattern: "key: &anchor" not "&anchor:" (which anchors the key) + # Use [\w-]+ to match keys with hyphens like "bill-to" + anchor_key_match = re.search(rf'([\w-]+):\s*&{anchor}\s', yaml_content) + alias_key_matches = re.findall(rf'([\w-]+):\s*\*{anchor}(?:\s|$)', yaml_content) + if anchor_key_match and alias_key_matches: + anchor_key = anchor_key_match.group(1) + for alias_key in alias_key_matches: + if anchor_key != alias_key: + # Additional check: make sure the anchor is not on a key itself + if not re.search(rf'&{anchor}:', yaml_content): + toBe_checks.append(f' expect((parsed as any)["{anchor_key}"]).toBe((parsed as any)["{alias_key}"]);') + test += f''' - const expected: any = {expected_str};''' + // Detected anchors that are referenced: {', '.join(shared_anchors)} + + const expected: any = {expected_str}; + + expect(parsed).toEqual(expected);''' + + if toBe_checks: + test += '\n\n // Verify shared references\n' + test += '\n'.join(toBe_checks) + + test += '\n});' + return test else: + # No shared anchors or not a dict/list stringified_value = stringify_map_keys(expected_value, from_yaml_package=not has_json) expected_str = json_to_js_literal(stringified_value) test += f''' @@ -911,7 +981,21 @@ def main(): mode_comment = "// AST validation disabled - only checking parse success/failure" if not check_ast else "// Using YAML.parse() with eemeli/yaml package as reference" + # Get yaml-test-suite commit hash + yaml_test_suite_commit = None + try: + result = subprocess.run(['git', 'rev-parse', 'HEAD'], + capture_output=True, text=True, + cwd=yaml_test_suite_path) + if result.returncode == 0: + yaml_test_suite_commit = result.stdout.strip() + except: + pass + + commit_comment = f"// Generated from yaml-test-suite commit: {yaml_test_suite_commit}" if yaml_test_suite_commit else "" + output = f'''// Tests translated from official yaml-test-suite +{commit_comment} {mode_comment} // Total: {len(test_dirs)} test directories @@ -933,7 +1017,7 @@ import {{ YAML }} from "bun"; try: test_case = generate_test(test_dir, test_name, check_ast) if test_case: - output += test_case + output += test_case + '\n' # Add newline between tests successful += 1 else: print(f" Skipped {test_name}: returned None") diff --git a/test/js/bun/yaml/yaml-test-suite.test.ts b/test/js/bun/yaml/yaml-test-suite.test.ts index ce10104203..e526e035aa 100644 --- a/test/js/bun/yaml/yaml-test-suite.test.ts +++ b/test/js/bun/yaml/yaml-test-suite.test.ts @@ -1,4 +1,5 @@ -// Tests translated from official yaml-test-suite (6e6c296) +// Tests translated from official yaml-test-suite +// Generated from yaml-test-suite commit: 6e6c296ae9c9d2d5c4134b4b64d01b29ac19ff6f // Using YAML.parse() with eemeli/yaml package as reference // Total: 402 test directories @@ -60,7 +61,7 @@ top6: // This YAML has anchors and aliases - creating shared references - // Detected anchors that are referenced: alias1, alias2 + // Detected anchors that are referenced: alias2, alias1 const expected: any = { top1: { key1: "scalar1" }, @@ -209,6 +210,7 @@ test("yaml-test-suite/2LFX", () => { test("yaml-test-suite/2SXE", () => { // Anchors With Colon in Name + // Note: &a anchors the key "key" itself, *a references that string const input: string = `&a: key: &a value foo: *a: @@ -216,10 +218,6 @@ foo: const parsed = YAML.parse(input); - // This YAML has anchors and aliases - creating shared references - - // Detected anchors that are referenced: a - const expected: any = { key: "value", foo: "key" }; expect(parsed).toEqual(expected); @@ -329,6 +327,9 @@ Reuse anchor: *anchor }; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["occurrence"]).toBe((parsed as any)["anchor"]); }); test("yaml-test-suite/3HFZ", () => { @@ -1221,6 +1222,9 @@ b: *anchor const expected: any = { a: null, b: null }; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["a"]).toBe((parsed as any)["b"]); }); test("yaml-test-suite/6LVF", () => { @@ -2514,6 +2518,10 @@ test("yaml-test-suite/C4HZ", () => { ]; expect(parsed).toEqual(expected); + + // Verify shared references + expect((parsed as any)["center"]).toBe((parsed as any)["start"]); + expect((parsed as any)["center"]).toBe((parsed as any)["start"]); }); test("yaml-test-suite/CC74", () => { @@ -3065,7 +3073,7 @@ test("yaml-test-suite/E76Z", () => { // This YAML has anchors and aliases - creating shared references - // Detected anchors that are referenced: a + // Detected anchors that are referenced: a, b const expected: any = { a: "b", b: "a" }; @@ -5718,22 +5726,30 @@ comments: // This YAML has anchors and aliases - creating shared references - const sharedAddress: any = { - given: "Chris", - family: "Dumars", - address: { - lines: "458 Walkman Dr.\nSuite #292\n", - city: "Royal Oak", - state: "MI", - postal: 48046, + const expected: any = { + invoice: 34843, + date: "2001-01-23", + "bill-to": { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, }, - }; - const expected = { - "invoice": 34843, - "date": "2001-01-23", - "bill-to": sharedAddress, - "ship-to": sharedAddress, - "product": [ + "ship-to": { + given: "Chris", + family: "Dumars", + address: { + lines: "458 Walkman Dr.\nSuite #292\n", + city: "Royal Oak", + state: "MI", + postal: 48046, + }, + }, + product: [ { sku: "BL394D", quantity: 4, @@ -5747,12 +5763,15 @@ comments: price: 2392, }, ], - "tax": 251.42, - "total": 4443.52, - "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", + tax: 251.42, + total: 4443.52, + comments: "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", }; expect(parsed).toEqual(expected); + + // Verify shared references - bill-to and ship-to should be the same object + expect((parsed as any)["bill-to"]).toBe((parsed as any)["ship-to"]); }); test("yaml-test-suite/UKK6/00", () => { From a9c0ec63e84bb9234bec1a21d7f1d293e269fe60 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sun, 5 Oct 2025 19:28:32 -0800 Subject: [PATCH 038/391] node:net: removed explicit ebaf from writing to detached socket (#23278) supersedes https://github.com/oven-sh/bun/pull/23030 partial revert of https://github.com/oven-sh/bun/commit/354391a26331b90f9c64ee8eb629704167cc96a5 likely fixes https://github.com/oven-sh/bun/issues/21982 --- src/bun.js/api/bun/socket.zig | 9 +--- .../test-net-socket-write-after-close.js | 42 ------------------- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 test/js/node/test/parallel/test-net-socket-write-after-close.js diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 8fa141fb59..3da90d798b 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -884,14 +884,7 @@ pub fn NewSocket(comptime ssl: bool) type { pub fn writeBuffered(this: *This, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { if (this.socket.isDetached()) { this.buffered_data_for_node_net.clearAndFree(bun.default_allocator); - // TODO: should we separate unattached and detached? unattached shouldn't throw here - const err: jsc.SystemError = .{ - .errno = @intFromEnum(bun.sys.SystemErrno.EBADF), - .code = .static("EBADF"), - .message = .static("write EBADF"), - .syscall = .static("write"), - }; - return globalObject.throwValue(err.toErrorInstance(globalObject)); + return .false; } const args = callframe.argumentsUndef(2); diff --git a/test/js/node/test/parallel/test-net-socket-write-after-close.js b/test/js/node/test/parallel/test-net-socket-write-after-close.js deleted file mode 100644 index 207f735fff..0000000000 --- a/test/js/node/test/parallel/test-net-socket-write-after-close.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const net = require('net'); - -{ - const server = net.createServer(); - - server.listen(common.mustCall(() => { - const port = server.address().port; - const client = net.connect({ port }, common.mustCall(() => { - client.on('error', common.mustCall((err) => { - server.close(); - assert.strictEqual(err.constructor, Error); - assert.strictEqual(err.message, 'write EBADF'); - })); - client._handle.close(); - client.write('foo'); - })); - })); -} - -{ - const server = net.createServer(); - - server.listen(common.mustCall(() => { - const port = server.address().port; - const client = net.connect({ port }, common.mustCall(() => { - client.on('error', common.expectsError({ - code: 'ERR_SOCKET_CLOSED', - message: 'Socket is closed', - name: 'Error' - })); - - server.close(); - - client._handle.close(); - client._handle = null; - client.write('foo'); - })); - })); -} From d292dcad2622829835db25d2125847cab77e17f1 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 6 Oct 2025 00:37:29 -0700 Subject: [PATCH 039/391] fix(parser): typescript `module` parsing bug (#23284) ### What does this PR do? A bug in our typescript parser was causing `module.foo = foo` to parse as a typescript namespace. If it didn't end with a semicolon and there's a statement on the next line it would cause a syntax error. Example: ```ts module.foo = foo foo.foo = foo ``` fixes #22929 fixes #22883 ### How did you verify your code works? Added a regression test --- src/ast/parseStmt.zig | 5 +- .../issue/22929-module-extensions-asi.test.ts | 115 ++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/22929-module-extensions-asi.test.ts diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index b8bce67462..8f946e079b 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -1223,8 +1223,9 @@ pub fn ParseStmt( // "module Foo {}" // "declare module 'fs' {}" // "declare module 'fs';" - if (((opts.is_module_scope or opts.is_namespace_scope) and (p.lexer.token == .t_identifier or - (p.lexer.token == .t_string_literal and opts.is_typescript_declare)))) + if (!p.lexer.has_newline_before and + (opts.is_module_scope or opts.is_namespace_scope) and + (p.lexer.token == .t_identifier or (p.lexer.token == .t_string_literal and opts.is_typescript_declare))) { return p.parseTypeScriptNamespaceStmt(loc, opts); } diff --git a/test/regression/issue/22929-module-extensions-asi.test.ts b/test/regression/issue/22929-module-extensions-asi.test.ts new file mode 100644 index 0000000000..7c5ecf31d8 --- /dev/null +++ b/test/regression/issue/22929-module-extensions-asi.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync, writeFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; +import { tmpdir } from "os"; +import { join } from "path"; + +test("Module._extensions should not break ASI (automatic semicolon insertion)", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-asi-")); + + // Create a module without semicolons that relies on ASI + const moduleWithoutSemi = join(dir, "module-no-semi.js"); + writeFileSync( + moduleWithoutSemi, + `function f() {} +module.exports = f +f.f = f`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +// Hook Module._extensions[".js"] - commonly done by transpiler libraries +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should work without parse errors +const result = require("./module-no-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result.f !== result) { + throw new Error('Expected result.f === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should not have parse errors + expect(stderr).not.toContain("Expected '{'"); + expect(stderr).not.toContain("Unexpected end of file"); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +}); + +test("Module._extensions works with modules that have semicolons", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-semi-")); + + // Create a module with semicolons + const moduleWithSemi = join(dir, "module-with-semi.js"); + writeFileSync( + moduleWithSemi, + `function g() { return 42; } +module.exports = g; +g.g = g;`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should also work with semicolons +const result = require("./module-with-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result() !== 42) { + throw new Error('Expected result() === 42'); +} +if (result.g !== result) { + throw new Error('Expected result.g === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should work correctly + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +}); From 1c363f0ad0e725edabaedd0cf60a489914c18d4b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 6 Oct 2025 00:39:08 -0700 Subject: [PATCH 040/391] fix(parser): `typeof` minification regression (#23280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What does this PR do? In Bun v1.2.22 a minification for `typeof x === "undefined"` → `typeof x > "u"` was added. This introduced a regression causing `return (typeof x !== "undefined", false)` to minify to invalid syntax when `--minify-syntax` is enabled (this is also enabled for transpilation at runtime). This pr fixes the regression making sure `return (typeof x !== "undefined", false);` minifies correctly to `return !1;`. fixes #21137 ### How did you verify your code works? Added a regression test. --- src/ast/SideEffects.zig | 13 +- .../issue/21137-minify-typeof-comma.test.ts | 168 ++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/21137-minify-typeof-comma.test.ts diff --git a/src/ast/SideEffects.zig b/src/ast/SideEffects.zig index 1b0f023c3a..d7ce926198 100644 --- a/src/ast/SideEffects.zig +++ b/src/ast/SideEffects.zig @@ -205,9 +205,18 @@ pub const SideEffects = enum(u1) { .bin_ge, => { if (isPrimitiveWithSideEffects(bin.left.data) and isPrimitiveWithSideEffects(bin.right.data)) { + const left_simplified = simplifyUnusedExpr(p, bin.left); + const right_simplified = simplifyUnusedExpr(p, bin.right); + + // If both sides would be removed entirely, we can return null to remove the whole expression + if (left_simplified == null and right_simplified == null) { + return null; + } + + // Otherwise, preserve at least the structure return Expr.joinWithComma( - simplifyUnusedExpr(p, bin.left) orelse bin.left.toEmpty(), - simplifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty(), + left_simplified orelse bin.left.toEmpty(), + right_simplified orelse bin.right.toEmpty(), p.allocator, ); } diff --git a/test/regression/issue/21137-minify-typeof-comma.test.ts b/test/regression/issue/21137-minify-typeof-comma.test.ts new file mode 100644 index 0000000000..1d5fe16727 --- /dev/null +++ b/test/regression/issue/21137-minify-typeof-comma.test.ts @@ -0,0 +1,168 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import path from "path"; + +// Regression test for minification bug where typeof comparison in comma operator +// produces invalid JavaScript output +test("issue #21137: minify typeof undefined in comma operator", async () => { + using dir = tempDir("issue-21137", {}); + + // This code pattern was producing invalid JavaScript: ", !1" instead of "!1" + const testCode = ` +function testFunc() { + return (typeof undefinedVar !== "undefined", false); +} + +// Test with other variations +function testFunc2() { + return (typeof someVar === "undefined", true); +} + +function testFunc3() { + // Nested comma operators + return ((typeof a !== "undefined", 1), (typeof b === "undefined", 2)); +} + +// Test in conditional +const result = typeof window !== "undefined" ? (typeof document !== "undefined", true) : false; + +console.log(testFunc()); +console.log(testFunc2()); +console.log(testFunc3()); +console.log(result); +`; + + const testFile = path.join(String(dir), "test.js"); + await Bun.write(testFile, testCode); + + // Build with minify-syntax flag + await using buildProc = Bun.spawn({ + cmd: [bunExe(), "build", "--minify-syntax", testFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [buildOutput, buildStderr, buildExitCode] = await Promise.all([ + buildProc.stdout.text(), + buildProc.stderr.text(), + buildProc.exited, + ]); + + // Build should succeed + expect(buildExitCode).toBe(0); + + // The output should NOT contain invalid syntax like ", !" or ", false" or ", true" + // These patterns indicate the bug where the left side of comma was incorrectly removed + expect(buildOutput).not.toContain(", !"); + expect(buildOutput).not.toContain(", false"); + expect(buildOutput).not.toContain(", true"); + expect(buildOutput).not.toContain(", 1"); + expect(buildOutput).not.toContain(", 2"); + + // Verify the minified code runs without syntax errors + const minifiedFile = path.join(String(dir), "minified.js"); + await Bun.write(minifiedFile, buildOutput); + + await using runProc = Bun.spawn({ + cmd: [bunExe(), minifiedFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [runOutput, runStderr, runExitCode] = await Promise.all([ + runProc.stdout.text(), + runProc.stderr.text(), + runProc.exited, + ]); + + // Should run without errors + expect(runExitCode).toBe(0); + + // Verify the output is correct + const lines = runOutput.trim().split("\n"); + expect(lines[0]).toBe("false"); // testFunc() returns false + expect(lines[1]).toBe("true"); // testFunc2() returns true + expect(lines[2]).toBe("2"); // testFunc3() returns 2 + expect(lines[3]).toBe("false"); // result is false (no window in Node/Bun) +}); + +// Additional test for the specific optimization that was causing the bug +test("issue #21137: typeof undefined optimization preserves valid syntax", async () => { + using dir = tempDir("issue-21137-opt", {}); + + // Test the specific optimization: typeof x !== "undefined" -> typeof x < "u" + const testCode = ` +// These should be optimized but remain valid +const a = typeof x !== "undefined"; +const b = typeof y === "undefined"; +const c = typeof z != "undefined"; +const d = typeof w == "undefined"; + +// In comma expressions +const e = (typeof foo !== "undefined", 42); +const f = (typeof bar === "undefined", "test"); + +// Should not break when left side is removed +function check() { + return (typeof missing !== "undefined", null); +} + +console.log(JSON.stringify({a, b, c, d, e, f, check: check()})); +`; + + const testFile = path.join(String(dir), "optimize.js"); + await Bun.write(testFile, testCode); + + await using buildProc = Bun.spawn({ + cmd: [bunExe(), "build", "--minify-syntax", testFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [buildOutput, buildStderr, buildExitCode] = await Promise.all([ + buildProc.stdout.text(), + buildProc.stderr.text(), + buildProc.exited, + ]); + + expect(buildExitCode).toBe(0); + + // Check that the optimization is applied (should contain < or > comparisons with "u") + expect(buildOutput).toContain('"u"'); + + // But should not have invalid comma syntax + expect(buildOutput).not.toMatch(/,\s*[!<>]/); // No comma followed by operator + expect(buildOutput).not.toMatch(/,\s*"u"/); // No comma followed by "u" + + // Run the minified code to ensure it's valid + const minifiedFile = path.join(String(dir), "minified.js"); + await Bun.write(minifiedFile, buildOutput); + + await using runProc = Bun.spawn({ + cmd: [bunExe(), minifiedFile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [runOutput, runStderr, runExitCode] = await Promise.all([ + runProc.stdout.text(), + runProc.stderr.text(), + runProc.exited, + ]); + + expect(runExitCode).toBe(0); + + // Parse and verify the output + const result = JSON.parse(runOutput.trim()); + expect(result.a).toBe(false); + expect(result.b).toBe(true); + expect(result.c).toBe(false); + expect(result.d).toBe(true); + expect(result.e).toBe(42); + expect(result.f).toBe("test"); + expect(result.check).toBe(null); +}); From f7da0ac6fd24886d029dd6a9890ab119f2c5b946 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 6 Oct 2025 20:58:04 +1100 Subject: [PATCH 041/391] bun install: support for `minimumReleaseAge` (#22801) ### What does this PR do? fixes #22679 * includes a better error if a package cant be met because of the age (but would normally) * logs the resolved one in --verbose (which can be helpful in debugging to show it does know latest but couldn't use) * makes bun outdated show in the table when the package isn't true latest * includes a rudimentary "stability" check if a later version is in blacked out time (but only up to 7 days as it goes back to latest with min age) For extended security we could also Last-Modified header of the tgz download and then abort if too new (just like the hash) | install error with no recent version | bun outdated respecting the rule | | --- | --- | image | image | For stable release we will make it use `3d` type syntax instead of magic second numbers. ### How did you verify your code works? tests & manual --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway --- bunfig.toml | 1 + docs/cli/install.md | 36 + docs/runtime/bunfig.md | 14 + src/api/schema.zig | 169 +- src/bun.js/bindings/WTF.zig | 18 + src/bun.js/bindings/wtf-bindings.cpp | 6 + src/bunfig.zig | 34 + src/cli/outdated_command.zig | 79 +- src/cli/pm_view_command.zig | 1 + src/cli/update_interactive_command.zig | 7 +- src/install/NetworkTask.zig | 21 +- .../PackageManager/CommandLineArguments.zig | 15 + .../PackageManager/PackageManagerEnqueue.zig | 104 +- .../PackageManager/PackageManagerOptions.zig | 19 +- .../PackageManagerResolution.zig | 3 +- .../PackageManager/PopulateManifestCache.zig | 12 +- src/install/PackageManager/runTasks.zig | 3 + src/install/PackageManagerTask.zig | 1 + src/install/PackageManifestMap.zig | 28 +- src/install/lockfile.zig | 2 + src/install/npm.zig | 329 ++- test/cli/install/minimum-release-age.test.ts | 2306 +++++++++++++++++ test/no-validate-leaksan.txt | 1 + 23 files changed, 2995 insertions(+), 214 deletions(-) create mode 100644 test/cli/install/minimum-release-age.test.ts diff --git a/bunfig.toml b/bunfig.toml index 3eae059d7c..f1bba3259c 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -10,3 +10,4 @@ preload = "./test/preload.ts" [install] linker = "isolated" +minimumReleaseAge = 1 diff --git a/docs/cli/install.md b/docs/cli/install.md index 0ad692ac62..68578a1c22 100644 --- a/docs/cli/install.md +++ b/docs/cli/install.md @@ -221,6 +221,38 @@ Bun uses a global cache at `~/.bun/install/cache/` to minimize disk usage. Packa For complete documentation refer to [Package manager > Global cache](https://bun.com/docs/install/cache). +## Minimum release age + +To protect against supply chain attacks where malicious packages are quickly published, you can configure a minimum age requirement for npm packages. Package versions published more recently than the specified threshold (in seconds) will be filtered out during installation. + +```bash +# Only install package versions published at least 3 days ago +$ bun add @types/bun --minimum-release-age 259200 # seconds +``` + +You can also configure this in `bunfig.toml`: + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 # seconds + +# Exclude trusted packages from the age gate +minimumReleaseAgeExcludes = ["@types/node", "typescript"] +``` + +When the minimum age filter is active: + +- Only affects new package resolution - existing packages in `bun.lock` remain unchanged +- All dependencies (direct and transitive) are filtered to meet the age requirement when being resolved +- When versions are blocked by the age gate, a stability check detects rapid bugfix patterns + - If multiple versions were published close together just outside your age gate, it extends the filter to skip those potentially unstable versions and selects an older, more mature version + - Searches up to 7 days after the age gate, however if still finding rapid releases it ignores stability check + - Exact version requests (like `package@1.1.1`) still respect the age gate but bypass the stability check +- Versions without a `time` field are treated as passing the age check (npm registry should always provide timestamps) + +For more advanced security scanning, including integration with services & custom filtering, see [Package manager > Security Scanner API](https://bun.com/docs/install/security-scanner-api). + ## Configuration The default behavior of `bun install` can be configured in `bunfig.toml`. The default values are shown below. @@ -255,6 +287,10 @@ concurrentScripts = 16 # (cpu count or GOMAXPROCS) x2 # installation strategy: "hoisted" or "isolated" # default: "hoisted" linker = "hoisted" + +# minimum age config +minimumReleaseAge = 259200 # seconds +minimumReleaseAgeExcludes = ["@types/node", "typescript"] ``` ## CI/CD diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 11c47d814c..532908f9bc 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -570,6 +570,20 @@ Valid values are: {% /table %} +### `install.minimumReleaseAge` + +Configure a minimum age (in seconds) for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled). + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 +# These packages will bypass the 3-day minimum age requirement +minimumReleaseAgeExcludes = ["@types/bun", "typescript"] +``` + +For more details see [Minimum release age](https://bun.com/docs/cli/install#minimum-release-age) in the install documentation. + ` in the issue comments - ignore other bot comments). If so, do not proceed. +2. Use an agent to view a GitHub issue, and ask the agent to return a summary of the issue +3. Then, launch 5 parallel agents to search GitHub for duplicates of this issue, using diverse keywords and search approaches, using the summary from Step 2. **IMPORTANT**: Always scope searches with `repo:owner/repo` to constrain results to the current repository only. +4. Next, feed the results from Steps 2 and 3 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed. +5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates) + +Notes (be sure to tell this to your agents, too): + +- Use `gh` to interact with GitHub, rather than web fetch +- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.) +- Make a todo list first +- Always scope searches with `repo:owner/repo` to prevent cross-repo false positives +- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates): + +--- + +Found 3 possible duplicate issues: + +1. +2. +3. + +This issue will be automatically closed as a duplicate in 3 days. + +- If your issue is a duplicate, please close it and 👍 the existing issue instead +- To prevent auto-closure, add a comment or 👎 this comment + +🤖 Generated with [Claude Code](https://claude.ai/code) + + + +--- diff --git a/.github/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml new file mode 100644 index 0000000000..886976bf6a --- /dev/null +++ b/.github/workflows/auto-close-duplicates.yml @@ -0,0 +1,29 @@ +name: Auto-close duplicate issues +on: + schedule: + - cron: "0 9 * * *" + workflow_dispatch: + +jobs: + auto-close-duplicates: + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: auto-close-duplicates-${{ github.repository }} + cancel-in-progress: true + permissions: + contents: read + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Auto-close duplicate issues + run: bun run scripts/auto-close-duplicates.ts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/.github/workflows/claude-dedupe-issues.yml b/.github/workflows/claude-dedupe-issues.yml new file mode 100644 index 0000000000..3677f61352 --- /dev/null +++ b/.github/workflows/claude-dedupe-issues.yml @@ -0,0 +1,34 @@ +name: Claude Issue Dedupe +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to process for duplicate detection' + required: true + type: string + +jobs: + claude-dedupe-issues: + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: claude-dedupe-issues-${{ github.event.issue.number || inputs.issue_number }} + cancel-in-progress: true + permissions: + contents: read + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Claude Code slash command + uses: anthropics/claude-code-base-action@beta + with: + prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_args: "--model claude-sonnet-4-5-20250929" + claude_env: | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/auto-close-duplicates.ts b/scripts/auto-close-duplicates.ts new file mode 100644 index 0000000000..d0c33575d7 --- /dev/null +++ b/scripts/auto-close-duplicates.ts @@ -0,0 +1,347 @@ +#!/usr/bin/env bun + +declare global { + var process: { + env: Record; + }; +} + +interface GitHubIssue { + number: number; + title: string; + user: { id: number }; + created_at: string; + pull_request?: object; +} + +interface GitHubComment { + id: number; + body: string; + created_at: string; + user: { type?: string; id: number }; +} + +interface GitHubReaction { + user: { id: number }; + content: string; +} + +async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function githubRequest( + endpoint: string, + token: string, + method: string = "GET", + body?: any, + retryCount: number = 0, +): Promise { + const maxRetries = 3; + + const response = await fetch(`https://api.github.com${endpoint}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + "User-Agent": "auto-close-duplicates-script", + ...(body && { "Content-Type": "application/json" }), + }, + ...(body && { body: JSON.stringify(body) }), + }); + + // Check rate limit headers + const rateLimitRemaining = response.headers.get("x-ratelimit-remaining"); + const rateLimitReset = response.headers.get("x-ratelimit-reset"); + + if (rateLimitRemaining && parseInt(rateLimitRemaining) < 100) { + console.warn(`[WARNING] GitHub API rate limit low: ${rateLimitRemaining} requests remaining`); + + if (parseInt(rateLimitRemaining) < 10) { + const resetTime = rateLimitReset ? parseInt(rateLimitReset) * 1000 : Date.now() + 60000; + const waitTime = Math.max(0, resetTime - Date.now()); + console.warn(`[WARNING] Rate limit critically low, waiting ${Math.ceil(waitTime / 1000)}s until reset`); + await sleep(waitTime + 1000); // Add 1s buffer + } + } + + // Handle rate limit errors with retry + if (response.status === 429 || response.status === 403) { + if (retryCount >= maxRetries) { + throw new Error(`GitHub API rate limit exceeded after ${maxRetries} retries`); + } + + const retryAfter = response.headers.get("retry-after"); + const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.min(1000 * Math.pow(2, retryCount), 32000); + + console.warn( + `[WARNING] Rate limited (${response.status}), retry ${retryCount + 1}/${maxRetries} after ${waitTime}ms`, + ); + await sleep(waitTime); + + return githubRequest(endpoint, token, method, body, retryCount + 1); + } + + if (!response.ok) { + throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +async function fetchAllComments( + owner: string, + repo: string, + issueNumber: number, + token: string, +): Promise { + const allComments: GitHubComment[] = []; + let page = 1; + const perPage = 100; + + while (true) { + const comments: GitHubComment[] = await githubRequest( + `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`, + token, + ); + + if (comments.length === 0) break; + + allComments.push(...comments); + page++; + + // Safety limit + if (page > 20) break; + } + + return allComments; +} + +async function fetchAllReactions( + owner: string, + repo: string, + commentId: number, + token: string, + authorId?: number, +): Promise { + const allReactions: GitHubReaction[] = []; + let page = 1; + const perPage = 100; + + while (true) { + const reactions: GitHubReaction[] = await githubRequest( + `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions?per_page=${perPage}&page=${page}`, + token, + ); + + if (reactions.length === 0) break; + + allReactions.push(...reactions); + + // Early exit if we're looking for a specific author and found their -1 reaction + if (authorId && reactions.some(r => r.user.id === authorId && r.content === "-1")) { + console.log(`[DEBUG] Found author thumbs down reaction, short-circuiting pagination`); + break; + } + + page++; + + // Safety limit + if (page > 20) break; + } + + return allReactions; +} + +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function extractDuplicateIssueNumber(commentBody: string, owner: string, repo: string): number | null { + // Escape owner and repo to prevent ReDoS attacks + const escapedOwner = escapeRegExp(owner); + const escapedRepo = escapeRegExp(repo); + + // Try to match same-repo GitHub issue URL format first: https://github.com/owner/repo/issues/123 + const repoUrlPattern = new RegExp(`github\\.com/${escapedOwner}/${escapedRepo}/issues/(\\d+)`); + let match = commentBody.match(repoUrlPattern); + if (match) { + return parseInt(match[1], 10); + } + + // Fallback to #123 format (assumes same repo) + match = commentBody.match(/#(\d+)/); + if (match) { + return parseInt(match[1], 10); + } + + return null; +} + +async function closeIssueAsDuplicate( + owner: string, + repo: string, + issueNumber: number, + duplicateOfNumber: number, + token: string, +): Promise { + // Close the issue as duplicate and add the duplicate label + await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}`, token, "PATCH", { + state: "closed", + state_reason: "duplicate", + labels: ["duplicate"], + }); + + await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, token, "POST", { + body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}. + +If this is incorrect, please re-open this issue or create a new one. + +🤖 Generated with [Claude Code](https://claude.ai/code)`, + }); +} + +async function autoCloseDuplicates(): Promise { + console.log("[DEBUG] Starting auto-close duplicates script"); + + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error("GITHUB_TOKEN environment variable is required"); + } + console.log("[DEBUG] GitHub token found"); + + // Parse GITHUB_REPOSITORY (format: "owner/repo") + const repository = process.env.GITHUB_REPOSITORY || "oven-sh/bun"; + const [owner, repo] = repository.split("/"); + if (!owner || !repo) { + throw new Error(`Invalid GITHUB_REPOSITORY format: ${repository}`); + } + console.log(`[DEBUG] Repository: ${owner}/${repo}`); + + const threeDaysAgo = new Date(); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + console.log(`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`); + + console.log("[DEBUG] Fetching open issues created more than 3 days ago..."); + const allIssues: GitHubIssue[] = []; + let page = 1; + const perPage = 100; + + while (true) { + const pageIssues: GitHubIssue[] = await githubRequest( + `/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`, + token, + ); + + if (pageIssues.length === 0) break; + + // Filter for issues created more than 3 days ago and exclude pull requests + const oldEnoughIssues = pageIssues.filter( + issue => !issue.pull_request && new Date(issue.created_at) <= threeDaysAgo, + ); + + allIssues.push(...oldEnoughIssues); + page++; + + // Safety limit to avoid infinite loops + if (page > 20) break; + } + + const issues = allIssues; + console.log(`[DEBUG] Found ${issues.length} open issues`); + + let processedCount = 0; + let candidateCount = 0; + + for (const issue of issues) { + processedCount++; + console.log(`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`); + + console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`); + const comments = await fetchAllComments(owner, repo, issue.number, token); + console.log(`[DEBUG] Issue #${issue.number} has ${comments.length} comments`); + + const dupeComments = comments.filter( + comment => + comment.body.includes("Found") && + comment.body.includes("possible duplicate") && + comment.user?.type === "Bot" && + comment.body.includes(""), + ); + console.log(`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`); + + if (dupeComments.length === 0) { + console.log(`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`); + continue; + } + + const lastDupeComment = dupeComments[dupeComments.length - 1]; + const dupeCommentDate = new Date(lastDupeComment.created_at); + console.log( + `[DEBUG] Issue #${issue.number} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`, + ); + + if (dupeCommentDate > threeDaysAgo) { + console.log(`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`); + continue; + } + console.log( + `[DEBUG] Issue #${issue.number} - duplicate comment is old enough (${Math.floor( + (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24), + )} days)`, + ); + + // Filter for human comments (not bot comments) after the duplicate comment + const commentsAfterDupe = comments.filter( + comment => new Date(comment.created_at) > dupeCommentDate && comment.user?.type !== "Bot", + ); + console.log( + `[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} human comments after duplicate detection`, + ); + + if (commentsAfterDupe.length > 0) { + console.log(`[DEBUG] Issue #${issue.number} - has human activity after duplicate comment, skipping`); + continue; + } + + console.log(`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`); + const reactions = await fetchAllReactions(owner, repo, lastDupeComment.id, token, issue.user.id); + console.log(`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`); + + const authorThumbsDown = reactions.some( + reaction => reaction.user.id === issue.user.id && reaction.content === "-1", + ); + console.log(`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`); + + if (authorThumbsDown) { + console.log(`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`); + continue; + } + + const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body, owner, repo); + if (!duplicateIssueNumber) { + console.log(`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`); + continue; + } + + candidateCount++; + const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`; + + try { + console.log(`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`); + await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token); + console.log(`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`); + } catch (error) { + console.error(`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`); + } + } + + console.log( + `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`, + ); +} + +autoCloseDuplicates().catch(console.error); + +// Make it a module +export {}; From 12e22af382bb80a0ae18f97daf92e3a60d6122eb Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 21 Oct 2025 16:25:29 -0700 Subject: [PATCH 214/391] set C_STANDARD to 17 (#23928) ### What does this PR do? msvc doesn't support c23 yet ### How did you verify your code works? --------- Co-authored-by: Marko Vejnovic --- cmake/targets/BuildBun.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index c31c8a4de5..113c61fbff 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -819,7 +819,7 @@ set_target_properties(${bun} PROPERTIES CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS YES CXX_VISIBILITY_PRESET hidden - C_STANDARD 23 + C_STANDARD 17 # Cannot uprev to C23 because MSVC doesn't have support. C_STANDARD_REQUIRED YES VISIBILITY_INLINES_HIDDEN YES ) From 3bc78598c6227e88ca9959eb3547144e12afa334 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Tue, 21 Oct 2025 17:54:44 -0700 Subject: [PATCH 215/391] bug(SlicedString.zig): Fix incorrect assertion in SlicedString.sub (#23934) ### What does this PR do? Fixes a small bug I found in https://github.com/oven-sh/bun/pull/23107 which caused `SlicedString` not to correctly provide us with subslices. This would have been a **killer** use-case for the interval utility we decided to reject in https://github.com/oven-sh/bun/pull/23882. Consider how nice the code could've been: ```zig pub inline fn sub(this: SlicedString, input: string) SlicedString { const buf_r = bun.math.interval.fromSlice(this.buf); const inp_r = bun.math.interval.fromSlice(this.input); if (Environment.allow_assert) { if (!buf_r.superset(inp_r)) { bun.Output.panic("SlicedString.sub input [{}, {}) is not a substring of the " ++ "slice [{}, {})", .{ start_i, end_i, start_buf, end_buf }); } } return SlicedString{ .buf = this.buf, .slice = input }; } ``` That's a lot more readable than the middle-school algebra we have here, but here we are. ### How did you verify your code works? CI Co-authored-by: Jarred Sumner --- src/semver/SlicedString.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/semver/SlicedString.zig b/src/semver/SlicedString.zig index 0f7ef55909..1e8e1bbe91 100644 --- a/src/semver/SlicedString.zig +++ b/src/semver/SlicedString.zig @@ -30,8 +30,14 @@ pub inline fn value(this: SlicedString) String { pub inline fn sub(this: SlicedString, input: string) SlicedString { if (Environment.allow_assert) { - if (!(@intFromPtr(this.buf.ptr) <= @intFromPtr(this.buf.ptr) and ((@intFromPtr(input.ptr) + input.len) <= (@intFromPtr(this.buf.ptr) + this.buf.len)))) { - @panic("SlicedString.sub input is not a substring of the slice"); + if (!bun.isSliceInBuffer(input, this.buf)) { + const start_buf = @intFromPtr(this.buf.ptr); + const end_buf = @intFromPtr(this.buf.ptr) + this.buf.len; + const start_i = @intFromPtr(input.ptr); + const end_i = @intFromPtr(input.ptr) + input.len; + + bun.Output.panic("SlicedString.sub input [{}, {}) is not a substring of the " ++ + "slice [{}, {})", .{ start_i, end_i, start_buf, end_buf }); } } return SlicedString{ .buf = this.buf, .slice = input }; From 1aaabcf4de3adcc7ce4582797a7383a25216989b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 21 Oct 2025 18:18:37 -0700 Subject: [PATCH 216/391] Add missing error handling in ShellWriter's start() method & delete assert() footgun (#23935) ### What does this PR do? ### How did you verify your code works? --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/node.zig | 9 --------- src/css/css_internals.zig | 11 ++++++++--- src/css/error.zig | 15 ++++++--------- src/shell/IOWriter.zig | 8 +++++++- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/bun.js/node.zig b/src/bun.js/node.zig index 176f09f7d7..6a593f717c 100644 --- a/src/bun.js/node.zig +++ b/src/bun.js/node.zig @@ -85,15 +85,6 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { .syscall = .access, } }; - pub fn assert(this: @This()) ReturnType { - switch (this) { - .err => |err| { - bun.Output.panic("Unexpected error\n{}", .{err}); - }, - .result => |result| return result, - } - } - pub inline fn todo() @This() { if (Environment.allow_assert) { if (comptime ReturnType == void) { diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index 74f4db425f..700756177b 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -110,7 +110,12 @@ pub fn testingImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, c var stylesheet, var extra = ret; var minify_options: bun.css.MinifyOptions = bun.css.MinifyOptions.default(); minify_options.targets.browsers = browsers; - _ = stylesheet.minify(alloc, minify_options, &extra).assert(); + switch (stylesheet.minify(alloc, minify_options, &extra)) { + .result => |_| {}, + .err => |*err| { + return globalThis.throwValue(try err.toErrorInstance(globalThis)); + }, + } const symbols = bun.ast.Symbol.Map{}; var local_names = bun.css.LocalsResultsMap{}; @@ -131,8 +136,8 @@ pub fn testingImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, c &symbols, )) { .result => |result| result, - .err => |err| { - return err.toJSString(alloc, globalThis); + .err => |*err| { + return globalThis.throwValue(try err.toErrorInstance(globalThis)); }, }; diff --git a/src/css/error.zig b/src/css/error.zig index 1b7a806783..cdc54adda4 100644 --- a/src/css/error.zig +++ b/src/css/error.zig @@ -34,11 +34,10 @@ pub fn Err(comptime T: type) type { @compileError("fmt not implemented for " ++ @typeName(T)); } - pub fn toJSString(this: @This(), allocator: Allocator, globalThis: *bun.jsc.JSGlobalObject) bun.jsc.JSValue { - var error_string = ArrayList(u8){}; - defer error_string.deinit(allocator); - error_string.writer(allocator).print("{}", .{this.kind}) catch unreachable; - return bun.String.fromBytes(error_string.items).toJS(globalThis); + pub fn toErrorInstance(this: *const @This(), globalThis: *bun.jsc.JSGlobalObject) !bun.jsc.JSValue { + var str = try bun.String.createFormat("{}", .{this.kind}); + defer str.deref(); + return str.toErrorInstance(globalThis); } pub fn fromParseError(err: ParseError(ParserError), filename: []const u8) Err(ParserError) { @@ -420,10 +419,8 @@ pub const MinifyErrorKind = union(enum) { }; const bun = @import("bun"); +const std = @import("std"); +const Allocator = std.mem.Allocator; const logger = bun.logger; const Log = logger.Log; - -const std = @import("std"); -const ArrayList = std.ArrayListUnmanaged; -const Allocator = std.mem.Allocator; diff --git a/src/shell/IOWriter.zig b/src/shell/IOWriter.zig index debc800af8..451be65d59 100644 --- a/src/shell/IOWriter.zig +++ b/src/shell/IOWriter.zig @@ -227,7 +227,13 @@ fn write(this: *IOWriter) enum { bun.assert(this.writer.handle == .poll); if (this.writer.handle.poll.isWatching()) return .suspended; - this.writer.start(this.fd, this.flags.pollable).assert(); + switch (this.writer.start(this.fd, this.flags.pollable)) { + .result => |_| {}, + .err => |err| { + this.onError(err); + return .failed; + }, + } return .suspended; } From 06eea5213a6682b645e5dfb8eb0423d227ce1831 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Wed, 22 Oct 2025 10:19:34 +0900 Subject: [PATCH 217/391] Add missing exception check for ReadableStream (#23932) ### What does this PR do? Adds missing exception check for ReadableStream. ### How did you verify your code works? Tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../bindings/webcore/ReadableStream.cpp | 2 +- test/js/web/streams/streams.test.js | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/webcore/ReadableStream.cpp b/src/bun.js/bindings/webcore/ReadableStream.cpp index 4e56927610..f7efa38ace 100644 --- a/src/bun.js/bindings/webcore/ReadableStream.cpp +++ b/src/bun.js/bindings/webcore/ReadableStream.cpp @@ -465,7 +465,7 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig:: auto callData = JSC::getCallData(function); auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - EXCEPTION_ASSERT(!!scope.exception() == !result); + RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(result); } diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index ca15d245c6..2d03f58f87 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 { bunEnv, isMacOS, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isMacOS, isWindows, tmpdirSync } from "harness"; import { mkfifo } from "mkfifo"; import { createReadStream, realpathSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -1142,3 +1142,44 @@ it("pipeThrough doesn't cause unhandled rejections on readable errors", async () expect(unhandledRejectionCaught).toBe(false); }); + +it("Handles exception during ReadableStream creation from Response.body", async () => { + const dir = tmpdirSync(); + const testFile = join(dir, "test-fixture.js"); + writeFileSync( + testFile, + ` +function recursiveFunction() { + const url = new URL("https://example.com/path"); + const response = new Response("test"); + + // Access Response.body which creates a ReadableStream + const body = response.body; + + // Set up infinite recursion via URL.pathname setter + url[Symbol.toPrimitive] = recursiveFunction; + try { + url.pathname = url; // Triggers toString() → toPrimitive → recursiveFunction() + } catch (e) { + // Stack overflow expected + if (e instanceof RangeError || e.message?.includes("stack")) { + process.exit(0); + } + throw e; + } +} +recursiveFunction(); +`, + ); + + await using proc = Bun.spawn({ + cmd: [bunExe(), testFile], + env: bunEnv, + cwd: dir, + stderr: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); +}); From d846e9a1e79953c00ac143b4fb19fa548f47dd61 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Tue, 21 Oct 2025 18:42:39 -0700 Subject: [PATCH 218/391] Fix `bun.String.toOwnedSliceReturningAllASCII` (#23925) `bun.String.toOwnedSliceReturningAllASCII` is supposed to return a boolean indicating whether or not the string is entirely composed of ASCII characters. However, the current implementation frequently produces incorrect results: * If the string is a `ZigString`, it always returns true, even though `ZigString`s can be UTF-16 or Latin-1. * If the string is a `StaticZigString`, it always returns false, even though `StaticZigStrings` can be all ASCII. * If the string is a 16-bit `WTFStringImpl`, it always returns false, even though 16-bit `WTFString`s can be all ASCII. * If the string is empty, it always returns false, even though empty strings are valid ASCII strings. `toOwnedSliceReturningAllASCII` is currently used in two places, both of which assume its answer is accurate: * `bun.webcore.Blob.fromJSWithoutDeferGC` * `bun.api.ServerConfig.fromJS` (For internal tracking: fixes ENG-21249) --- src/bun.js/bindings/ZigString.zig | 3 +- src/bun.js/webcore/Blob.zig | 20 ++---------- src/string.zig | 52 ++++++++++++++++++++++--------- src/string/immutable.zig | 13 ++++++++ 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/src/bun.js/bindings/ZigString.zig b/src/bun.js/bindings/ZigString.zig index abf9f61111..ee91221403 100644 --- a/src/bun.js/bindings/ZigString.zig +++ b/src/bun.js/bindings/ZigString.zig @@ -412,7 +412,8 @@ pub const ZigString = extern struct { } pub fn mut(this: Slice) []u8 { - return @as([*]u8, @ptrFromInt(@intFromPtr(this.ptr)))[0..this.len]; + bun.assertf(!this.allocator.isNull(), "cannot mutate a borrowed ZigString.Slice", .{}); + return @constCast(this.ptr)[0..this.len]; } /// Does nothing if the slice is not allocated diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index d5d5ab337e..68c85a3138 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -34,7 +34,7 @@ content_type_was_set: bool = false, /// JavaScriptCore strings are either latin1 or UTF-16 /// When UTF-16, they're nearly always due to non-ascii characters -charset: Charset = .unknown, +charset: strings.AsciiStatus = .unknown, /// Was it created via file constructor? is_jsdom_file: bool = false, @@ -3244,7 +3244,7 @@ pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: * .store = store, .content_type = "", .globalThis = globalThis, - .charset = .fromIsAllASCII(is_all_ascii), + .charset = .fromBool(is_all_ascii), }; } @@ -3423,7 +3423,7 @@ pub fn sharedView(this: *const Blob) []const u8 { pub const Lifetime = jsc.WebCore.Lifetime; pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void { - this.charset = .fromIsAllASCII(is_all_ascii); + this.charset = .fromBool(is_all_ascii); // if this Blob represents the entire binary data // which will be pretty common // we can update the store's is_all_ascii flag @@ -4735,20 +4735,6 @@ pub fn FileCloser(comptime This: type) type { }; } -/// This takes up less space than a `?bool`. -pub const Charset = enum { - unknown, - all_ascii, - non_ascii, - - pub fn fromIsAllASCII(is_all_ascii: ?bool) Charset { - return if (is_all_ascii orelse return .unknown) - .all_ascii - else - .non_ascii; - } -}; - pub fn isAllASCII(self: *const Blob) ?bool { return switch (self.charset) { .unknown => null, diff --git a/src/string.zig b/src/string.zig index cb1ed9d85d..83e10a5a85 100644 --- a/src/string.zig +++ b/src/string.zig @@ -74,27 +74,48 @@ pub const String = extern struct { return BunString__transferToJS(this, globalThis); } - pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 { - const bytes, _ = try this.toOwnedSliceReturningAllASCII(allocator); + pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) OOM![]u8 { + const bytes, _ = try this.toOwnedSliceImpl(allocator); return bytes; } + /// Returns `.{ utf8_bytes, is_all_ascii }`. + /// + /// `false` means the string contains at least one non-ASCII character. pub fn toOwnedSliceReturningAllASCII(this: String, allocator: std.mem.Allocator) OOM!struct { []u8, bool } { - switch (this.tag) { - .ZigString => return .{ try this.value.ZigString.toOwnedSlice(allocator), true }, - .WTFStringImpl => { - var utf8_slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); - if (utf8_slice.allocator.get()) |alloc| { - if (!isWTFAllocator(alloc)) { - return .{ @constCast(utf8_slice.slice()), false }; - } - } + const bytes, const ascii_status = try this.toOwnedSliceImpl(allocator); + const is_ascii = switch (ascii_status) { + .all_ascii => true, + .non_ascii => false, + .unknown => bun.strings.isAllASCII(bytes), + }; + return .{ bytes, is_ascii }; + } - return .{ @constCast((try utf8_slice.cloneIfNeeded(allocator)).slice()), true }; + fn toOwnedSliceImpl(this: String, allocator: std.mem.Allocator) !struct { []u8, AsciiStatus } { + return switch (this.tag) { + .ZigString => .{ try this.value.ZigString.toOwnedSlice(allocator), .unknown }, + .WTFStringImpl => blk: { + const utf8_slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); + // `utf8_slice.allocator` is either null, or `allocator`. + errdefer utf8_slice.deinit(); + + const ascii_status: AsciiStatus = if (utf8_slice.allocator.isNull()) + .all_ascii // no allocation means the string was 8-bit and all ascii + else if (this.value.WTFStringImpl.is8Bit()) + .non_ascii // otherwise the allocator would be null for an 8-bit string + else + .unknown; // string was 16-bit; may or may not be all ascii + + const owned_slice = try utf8_slice.cloneIfNeeded(allocator); + // `owned_slice.allocator` is guaranteed to be `allocator`. + break :blk .{ owned_slice.mut(), ascii_status }; }, - .StaticZigString => return .{ try this.value.StaticZigString.toOwnedSlice(allocator), false }, - else => return .{ &[_]u8{}, false }, - } + .StaticZigString => .{ + try this.value.StaticZigString.toOwnedSlice(allocator), .unknown, + }, + else => return .{ &.{}, .all_ascii }, // trivially all ascii + }; } pub fn createIfDifferent(other: String, utf8_slice: []const u8) String { @@ -1237,6 +1258,7 @@ const std = @import("std"); const bun = @import("bun"); const JSError = bun.JSError; const OOM = bun.OOM; +const AsciiStatus = bun.strings.AsciiStatus; const jsc = bun.jsc; const JSValue = bun.jsc.JSValue; diff --git a/src/string/immutable.zig b/src/string/immutable.zig index 04bb476dce..ce36729315 100644 --- a/src/string/immutable.zig +++ b/src/string/immutable.zig @@ -10,6 +10,19 @@ pub const Encoding = enum { utf16, }; +pub const AsciiStatus = enum { + unknown, + all_ascii, + non_ascii, + + pub fn fromBool(is_all_ascii: ?bool) AsciiStatus { + return if (is_all_ascii orelse return .unknown) + .all_ascii + else + .non_ascii; + } +}; + /// Returned by classification functions that do not discriminate between utf8 and ascii. pub const EncodingNonAscii = enum { utf8, From 45841d663072f584668647a1ba4f77c053f1fb39 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 21 Oct 2025 19:22:55 -0700 Subject: [PATCH 219/391] Check if toSlice has a bug (#23889) ### What does this PR do? toSlice has a bug ### How did you verify your code works? --------- Co-authored-by: taylor.fish Co-authored-by: Dylan Conway --- src/bake/FrameworkRouter.zig | 10 +++---- src/bun.js/ConsoleObject.zig | 6 ++--- src/bun.js/api/YAMLObject.zig | 3 ++- src/bun.js/api/bun/dns.zig | 15 +++++------ src/bun.js/api/server.zig | 4 +-- src/bun.js/bindings/JSValue.zig | 6 ++--- src/bun.js/bindings/ZigString.zig | 38 +++++++-------------------- src/bun.js/bindings/bindings.cpp | 5 ++-- src/bun.js/node/path.zig | 2 +- src/bun.js/node/types.zig | 14 +++++----- src/bun.js/test/ScopeFunctions.zig | 42 +++++++++++++++--------------- src/bun.js/test/expect.zig | 8 +++--- src/bun.js/test/pretty_format.zig | 2 +- src/string.zig | 7 ++--- 14 files changed, 70 insertions(+), 92 deletions(-) diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index 76d737baa3..2975ac631d 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -1233,14 +1233,12 @@ pub const JSFrameworkRouter = struct { } pub fn match(jsfr: *JSFrameworkRouter, global: *JSGlobalObject, callframe: *jsc.CallFrame) !JSValue { - const path_js = callframe.argumentsAsArray(1)[0]; - const path_str = try path_js.toBunString(global); - defer path_str.deref(); - const path_slice = path_str.toSlice(bun.default_allocator); - defer path_slice.deinit(); + const path_value = callframe.argumentsAsArray(1)[0]; + const path = try path_value.toSlice(global, bun.default_allocator); + defer path.deinit(); var params_out: MatchedParams = undefined; - if (jsfr.router.matchSlow(path_slice.slice(), ¶ms_out)) |index| { + if (jsfr.router.matchSlow(path.slice(), ¶ms_out)) |index| { var sfb = std.heap.stackFallback(4096, bun.default_allocator); const alloc = sfb.get(); diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 166a0f027f..46ced4842c 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -2322,11 +2322,11 @@ pub const Formatter = struct { } }, .Function => { - var printable = value.getName(this.globalThis); + var printable = try value.getName(this.globalThis); defer printable.deref(); const proto = value.getPrototype(this.globalThis); - const func_name = proto.getName(this.globalThis); // "Function" | "AsyncFunction" | "GeneratorFunction" | "AsyncGeneratorFunction" + const func_name = try proto.getName(this.globalThis); // "Function" | "AsyncFunction" | "GeneratorFunction" | "AsyncGeneratorFunction" defer func_name.deref(); if (printable.isEmpty() or func_name.eql(printable)) { @@ -3312,7 +3312,7 @@ pub const Formatter = struct { this.resetLine(); } - var display_name = value.getName(this.globalThis); + var display_name = try value.getName(this.globalThis); if (display_name.isEmpty()) { display_name = String.static("Object"); } diff --git a/src/bun.js/api/YAMLObject.zig b/src/bun.js/api/YAMLObject.zig index 327ccea014..0c01e36feb 100644 --- a/src/bun.js/api/YAMLObject.zig +++ b/src/bun.js/api/YAMLObject.zig @@ -922,7 +922,8 @@ pub fn parse( const input_value = callFrame.argumentsAsArray(1)[0]; const input: jsc.Node.BlobOrStringOrBuffer = try jsc.Node.BlobOrStringOrBuffer.fromJS(global, arena.allocator(), input_value) orelse input: { - const str = try input_value.toBunString(global); + var str = try input_value.toBunString(global); + defer str.deref(); break :input .{ .string_or_buffer = .{ .string = str.toSlice(arena.allocator()) } }; }; defer input.deinit(); diff --git a/src/bun.js/api/bun/dns.zig b/src/bun.js/api/bun/dns.zig index 91e0934a4b..50256ff78f 100644 --- a/src/bun.js/api/bun/dns.zig +++ b/src/bun.js/api/bun/dns.zig @@ -3240,24 +3240,21 @@ pub const Resolver = struct { } fn setChannelLocalAddress(channel: *c_ares.Channel, globalThis: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!c_int { - const str = try value.toBunString(globalThis); - defer str.deref(); + var str = try value.toSlice(globalThis, bun.default_allocator); + defer str.deinit(); - const slice = str.toSlice(bun.default_allocator).slice(); - var buffer = bun.handleOom(bun.default_allocator.alloc(u8, slice.len + 1)); - defer bun.default_allocator.free(buffer); - _ = strings.copy(buffer[0..], slice); - buffer[slice.len] = 0; + const slice = try str.intoOwnedSliceZ(bun.default_allocator); + defer bun.default_allocator.free(slice); var addr: [16]u8 = undefined; - if (c_ares.ares_inet_pton(c_ares.AF.INET, buffer.ptr, &addr) == 1) { + if (c_ares.ares_inet_pton(c_ares.AF.INET, slice.ptr, &addr) == 1) { const ip = std.mem.readInt(u32, addr[0..4], .big); c_ares.ares_set_local_ip4(channel, ip); return c_ares.AF.INET; } - if (c_ares.ares_inet_pton(c_ares.AF.INET6, buffer.ptr, &addr) == 1) { + if (c_ares.ares_inet_pton(c_ares.AF.INET6, slice.ptr, &addr) == 1) { c_ares.ares_set_local_ip6(channel, &addr); return c_ares.AF.INET6; } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 7db6d46af2..cb53d50fbe 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -76,9 +76,9 @@ pub const AnyRoute = union(enum) { fn bundledHTMLManifestItemFromJS(argument: jsc.JSValue, index_path: []const u8, init_ctx: *ServerInitContext) bun.JSError!?AnyRoute { if (!argument.isObject()) return null; - const path_string = try bun.String.fromJS(try argument.get(init_ctx.global, "path") orelse return null, init_ctx.global); + var path_string = try bun.String.fromJS(try argument.get(init_ctx.global, "path") orelse return null, init_ctx.global); defer path_string.deref(); - var path = jsc.Node.PathOrFileDescriptor{ .path = try jsc.Node.PathLike.fromBunString(init_ctx.global, path_string, false, bun.default_allocator) }; + var path = jsc.Node.PathOrFileDescriptor{ .path = try jsc.Node.PathLike.fromBunString(init_ctx.global, &path_string, false, bun.default_allocator) }; defer path.deinit(); // Construct the route by stripping paths above the root. diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index f8d7666bc8..8dfff1c386 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -253,7 +253,7 @@ pub const JSValue = enum(i64) { loop.debug.js_call_count_outside_tick_queue += @as(usize, @intFromBool(!loop.debug.is_inside_tick_queue)); if (loop.debug.track_last_fn_name and !loop.debug.is_inside_tick_queue) { loop.debug.last_fn_name.deref(); - loop.debug.last_fn_name = function.getName(global); + loop.debug.last_fn_name = try function.getName(global); } // Do not assert that the function is callable here. // The Bun__JSValue__call function will already assert that, and @@ -1054,9 +1054,9 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__getName(jsc.JSValue, *jsc.JSGlobalObject, *bun.String) void; - pub fn getName(this: JSValue, global: *JSGlobalObject) bun.String { + pub fn getName(this: JSValue, global: *JSGlobalObject) JSError!bun.String { var ret = bun.String.empty; - JSC__JSValue__getName(this, global, &ret); + try bun.jsc.fromJSHostCallGeneric(global, @src(), JSC__JSValue__getName, .{ this, global, &ret }); return ret; } diff --git a/src/bun.js/bindings/ZigString.zig b/src/bun.js/bindings/ZigString.zig index ee91221403..9a954969b6 100644 --- a/src/bun.js/bindings/ZigString.zig +++ b/src/bun.js/bindings/ZigString.zig @@ -383,6 +383,16 @@ pub const ZigString = extern struct { return (try this.toOwned(allocator)).slice(); } + /// Same as `intoOwnedSlice`, but creates `[:0]const u8` + pub fn intoOwnedSliceZ(this: *Slice, allocator: std.mem.Allocator) OOM![:0]const u8 { + defer { + this.deinit(); + this.* = .{}; + } + // always clones + return allocator.dupeZ(u8, this.slice()); + } + /// Note that the returned slice is not guaranteed to be allocated by `allocator`. pub fn cloneIfNeeded(this: Slice, allocator: std.mem.Allocator) bun.OOM!Slice { if (this.isAllocated()) { @@ -398,15 +408,6 @@ pub const ZigString = extern struct { return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buf.ptr, .len = @as(u32, @truncate(buf.len)) }; } - pub fn cloneZ(this: Slice, allocator: std.mem.Allocator) !Slice { - if (this.isAllocated() or this.len == 0) { - return this; - } - - const duped = try allocator.dupeZ(u8, this.ptr[0..this.len]); - return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = duped.ptr, .len = this.len }; - } - pub fn slice(this: *const Slice) []const u8 { return this.ptr[0..this.len]; } @@ -695,25 +696,6 @@ pub const ZigString = extern struct { }; } - pub fn toSliceZ(this: ZigString, allocator: std.mem.Allocator) Slice { - if (this.len == 0) - return Slice.empty; - - if (is16Bit(&this)) { - const buffer = this.toOwnedSliceZ(allocator) catch unreachable; - return Slice{ - .ptr = buffer.ptr, - .len = @as(u32, @truncate(buffer.len)), - .allocator = NullableAllocator.init(allocator), - }; - } - - return Slice{ - .ptr = untagged(this._unsafe_ptr_do_not_use), - .len = @as(u32, @truncate(this.len)), - }; - } - pub fn sliceZBuf(this: ZigString, buf: *bun.PathBuffer) ![:0]const u8 { return try std.fmt.bufPrintZ(buf, "{}", .{this}); } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 985a46e4b9..db24591323 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4489,7 +4489,7 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb arg2->len = 0; } -extern "C" void JSC__JSValue__getName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, BunString* arg2) +[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__getName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, BunString* arg2) { JSC::JSValue value = JSC::JSValue::decode(JSValue0); if (!value.isObject()) { @@ -4497,7 +4497,7 @@ extern "C" void JSC__JSValue__getName(JSC::EncodedJSValue JSValue0, JSC::JSGloba return; } auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); JSObject* object = value.getObject(); auto displayName = JSC::getCalculatedDisplayName(vm, object); @@ -4511,7 +4511,6 @@ extern "C" void JSC__JSValue__getName(JSC::EncodedJSValue JSValue0, JSC::JSGloba } } } - CLEAR_IF_EXCEPTION(scope); *arg2 = Bun::toStringRef(displayName); } diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index b8bf942dc4..bf8a26b857 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -2793,7 +2793,7 @@ pub fn resolve(globalObject: *jsc.JSGlobalObject, isWindows: bool, args_ptr: [*] } paths_offset -= 1; - paths_buf[paths_offset] = path_str.toSlice(allocator).slice(); + paths_buf[paths_offset] = try path_str.toOwnedSlice(allocator); if (!isWindows) { if (path_str.charAt(0) == CHAR_FORWARD_SLASH) { diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index f1020fa026..cfdc1640a0 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -223,10 +223,9 @@ pub const StringOrBuffer = union(enum) { if (!allow_string_object and str_type != .String) { return null; } - const str = try bun.String.fromJS(value, global); - + var str = try bun.String.fromJS(value, global); + defer str.deref(); if (is_async) { - defer str.deref(); var possible_clone = str; var sliced = try possible_clone.toThreadSafeSlice(allocator); sliced.reportExtraMemory(global.vm()); @@ -672,7 +671,7 @@ pub const PathLike = union(enum) { arguments.eat(); - return try fromBunString(ctx, str, arguments.will_be_async, allocator); + return try fromBunString(ctx, &str, arguments.will_be_async, allocator); }, else => { if (arg.as(jsc.DOMURL)) |domurl| { @@ -693,7 +692,7 @@ pub const PathLike = union(enum) { } arguments.eat(); - return try fromBunString(ctx, str, arguments.will_be_async, allocator); + return try fromBunString(ctx, &str, arguments.will_be_async, allocator); } return null; @@ -701,7 +700,7 @@ pub const PathLike = union(enum) { } } - pub fn fromBunString(global: *jsc.JSGlobalObject, str: bun.String, will_be_async: bool, allocator: std.mem.Allocator) !PathLike { + pub fn fromBunString(global: *jsc.JSGlobalObject, str: *bun.String, will_be_async: bool, allocator: std.mem.Allocator) !PathLike { try Valid.pathStringLength(str.length(), global); if (will_be_async) { @@ -718,13 +717,12 @@ pub const PathLike = union(enum) { return .{ .threadsafe_string = sliced }; } else { var sliced = str.toSlice(allocator); - errdefer if (!sliced.isWTFAllocated()) sliced.deinit(); + errdefer sliced.deinit(); try Valid.pathNullBytes(sliced.slice(), global); // Costs nothing to keep both around. if (sliced.isWTFAllocated()) { - str.ref(); return .{ .slice_with_underlying_string = sliced }; } diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig index 4f856a6e45..4d7b1c9a4a 100644 --- a/src/bun.js/test/ScopeFunctions.zig +++ b/src/bun.js/test/ScopeFunctions.zig @@ -298,35 +298,35 @@ const ParseArgumentsResult = struct { pub const CallbackMode = enum { require, allow }; fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, description: jsc.JSValue, signature: Signature) bun.JSError![]const u8 { - const is_valid_description = - description.isClass(globalThis) or - (description.isFunction() and !description.getName(globalThis).isEmpty()) or - description.isNumber() or - description.isString(); - - if (!is_valid_description) { - return globalThis.throwPretty("{s}() expects first argument to be a named class, named function, number, or string", .{signature}); - } - if (description == .zero) { return ""; } if (description.isClass(globalThis)) { - const name_str = if ((try description.className(globalThis)).toSlice(gpa).length() == 0) - description.getName(globalThis).toSlice(gpa).slice() - else - (try description.className(globalThis)).toSlice(gpa).slice(); - return try gpa.dupe(u8, name_str); + var description_class_name = try description.className(globalThis); + + if (description_class_name.len > 0) { + return description_class_name.toOwnedSlice(gpa); + } + + var description_name = try description.getName(globalThis); + defer description_name.deref(); + return description_name.toOwnedSlice(gpa); } + if (description.isFunction()) { - var slice = description.getName(globalThis).toSlice(gpa); - defer slice.deinit(); - return try gpa.dupe(u8, slice.slice()); + const func_name = try description.getName(globalThis); + if (func_name.length() > 0) { + return func_name.toOwnedSlice(gpa); + } } - var slice = try description.toSlice(globalThis, gpa); - defer slice.deinit(); - return try gpa.dupe(u8, slice.slice()); + + if (description.isNumber() or description.isString()) { + var slice = try description.toSlice(globalThis, gpa); + return slice.intoOwnedSlice(gpa); + } + + return globalThis.throwPretty("{s}() expects first argument to be a named class, named function, number, or string", .{signature}); } pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode }) bun.JSError!ParseArgumentsResult { diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 4f0a1ae4ca..8a1326a11c 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -961,7 +961,7 @@ pub const Expect = struct { pub fn format(this: CustomMatcherParamsFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { // try to detect param names from matcher_fn (user function) source code if (jsc.JSFunction.getSourceCode(this.matcher_fn)) |source_str| { - var source_slice = source_str.toSlice(this.globalThis.allocator()); + const source_slice = source_str.toUTF8(this.globalThis.allocator()); defer source_slice.deinit(); var source: string = source_slice.slice(); @@ -1128,7 +1128,7 @@ pub const Expect = struct { // so now execute the symmetric matching // retrieve the matcher name - const matcher_name = matcher_fn.getName(globalThis); + const matcher_name = try matcher_fn.getName(globalThis); const matcher_params = CustomMatcherParamsFormatter{ .colors = Output.enable_ansi_colors, @@ -1688,7 +1688,9 @@ pub const ExpectCustomAsymmetricMatcher = struct { } // retrieve the matcher name - const matcher_name = matcher_fn.getName(globalThis); + const matcher_name = matcher_fn.getName(globalThis) catch { + return false; + }; // retrieve the asymmetric matcher args // if null, it means the function has not yet been called to capture the args, which is a misuse of the matcher diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index ef5fa5085d..1441c20749 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -2117,7 +2117,7 @@ pub const JestPrettyFormat = struct { const flags = instance.flags; const args_value = expect.ExpectCustomAsymmetricMatcher.js.capturedArgsGetCached(value) orelse return true; const matcher_fn = expect.ExpectCustomAsymmetricMatcher.js.matcherFnGetCached(value) orelse return true; - const matcher_name = matcher_fn.getName(this.globalThis); + const matcher_name = try matcher_fn.getName(this.globalThis); printAsymmetricMatcherPromisePrefix(flags, this, writer); if (flags.not) { diff --git a/src/string.zig b/src/string.zig index 83e10a5a85..6572b8e5cf 100644 --- a/src/string.zig +++ b/src/string.zig @@ -769,14 +769,15 @@ pub const String = extern struct { } /// use `byteSlice` to get a `[]const u8`. - pub fn toSlice(this: String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + pub fn toSlice(this: *String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + defer this.* = .empty; return SliceWithUnderlyingString{ .utf8 = this.toUTF8(allocator), - .underlying = this, + .underlying = this.*, }; } - pub fn toThreadSafeSlice(this: *const String, allocator: std.mem.Allocator) bun.OOM!SliceWithUnderlyingString { + pub fn toThreadSafeSlice(this: *String, allocator: std.mem.Allocator) bun.OOM!SliceWithUnderlyingString { if (this.tag == .WTFStringImpl) { if (!this.value.WTFStringImpl.isThreadSafe()) { const slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); From a3c43dc8b9f7ddfa00defa531497398afacfff1c Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 21 Oct 2025 19:42:01 -0700 Subject: [PATCH 220/391] Fix Windows bunx fast path index out of bounds panic (#23938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixed a bug in the Windows bunx fast path code where UTF-8 byte length was incorrectly used instead of UTF-16 code unit length when calculating buffer offsets. ## Details In `run_command.zig:1565`, the code was using `target_name.len` (UTF-8 byte length) instead of `encoded.len` (UTF-16 code unit length) when calculating the total path length. This caused an index out of bounds panic when package names contained multi-byte UTF-8 characters. **Example scenario:** - Package name contains character "中" (U+4E2D) - UTF-8: 3 bytes (0xE4 0xB8 0xAD) → `target_name.len` counts as 3 - UTF-16: 1 code unit (0x4E2D) → `encoded.len` counts as 1 - Using the wrong length led to: `panic: index out of bounds: index 62, len 60` ## Changes - Changed line 1565 from `target_name.len` to `encoded.len` ## Test plan - [x] Build compiles successfully - [x] Code review confirms the fix addresses the root cause - [ ] Windows-specific testing (if available) Fixes the panic reported in Sentry/crash reports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/cli/run_command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 34873d6819..7c5fbb0818 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1562,7 +1562,7 @@ pub const RunCommand = struct { @memcpy(ptr[0..ext.len], ext); ptr[ext.len] = 0; - const l = root.len + cwd_len + prefix.len + target_name.len + ext.len; + const l = root.len + cwd_len + prefix.len + encoded.len + ext.len; const path_to_use = BunXFastPath.direct_launch_buffer[0..l :0]; BunXFastPath.tryLaunch(ctx, path_to_use, this_transpiler.env, ctx.passthrough); } From bb5f0f5d69f9180f2054804b96269ce4e0993e91 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 21 Oct 2025 20:07:08 -0800 Subject: [PATCH 221/391] node:net: another memory leak fix (#23936) found with https://github.com/oven-sh/bun/pull/21663 again case found in `test/js/bun/net/socket.test.ts` test `"should throw when a socket from a file descriptor has a bad file descriptor"` --- src/bun.js/api/bun/socket/Listener.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 4f058cdb05..75252b70e2 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -784,10 +784,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock SocketType.js.dataSetCached(socket.getThisValue(globalObject), globalObject, default_data); socket.flags.allow_half_open = socket_config.allowHalfOpen; socket.doConnect(connection) catch { - socket.handleConnectError(@intFromEnum(if (port == null) - bun.sys.SystemErrno.ENOENT - else - bun.sys.SystemErrno.ECONNREFUSED)); + socket.handleConnectError(@intFromEnum(if (port == null) bun.sys.SystemErrno.ENOENT else bun.sys.SystemErrno.ECONNREFUSED)); + if (maybe_previous == null) socket.deref(); return promise_value; }; From 72f1ffdaf7bc43b6d2fb976c8c4bb66eb19824d9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 21 Oct 2025 22:56:36 -0700 Subject: [PATCH 222/391] Silence non-actionable worker_threads.Worker option warnings (#23941) ### What does this PR do? ### How did you verify your code works? --- src/js/node/worker_threads.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts index f062fd8814..ac5f6320d6 100644 --- a/src/js/node/worker_threads.ts +++ b/src/js/node/worker_threads.ts @@ -223,8 +223,6 @@ function moveMessagePortToContext() { throwNotImplemented("worker_threads.moveMessagePortToContext"); } -const unsupportedOptions = ["stdin", "stdout", "stderr", "trackedUnmanagedFds", "resourceLimits"]; - class Worker extends EventEmitter { #worker: WebWorker; #performance; @@ -236,11 +234,6 @@ class Worker extends EventEmitter { constructor(filename: string, options: NodeWorkerOptions = {}) { super(); - for (const key of unsupportedOptions) { - if (key in options && options[key] != null) { - warnNotImplementedOnce(`worker_threads.Worker option "${key}"`); - } - } const builtinsGeneratorHatesEval = "ev" + "a" + "l"[0]; if (options && builtinsGeneratorHatesEval in options) { From 89fa0f343945e61d5e4a0077cc7e93a802ed56e7 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 21 Oct 2025 22:58:46 -0700 Subject: [PATCH 223/391] Refactor napi_env to use Ref-counted NapiEnv (#23940) ### What does this PR do? Replaces raw napi_env pointers with WTF::Ref for improved memory management and safety. Updates related classes, function signatures, and finalizer handling to use reference counting. Adds ref/deref methods to NapiEnv and integrates them in Zig and C++ code paths, ensuring proper lifecycle management for N-API environments. ### How did you verify your code works? --- src/bun.js/api/FFI.h | 4 +- src/bun.js/bindings/BunProcess.cpp | 4 +- src/bun.js/bindings/NapiRef.cpp | 2 +- src/bun.js/bindings/ZigGlobalObject.cpp | 8 +- src/bun.js/bindings/ZigGlobalObject.h | 4 +- src/bun.js/bindings/napi.cpp | 32 +++-- src/bun.js/bindings/napi.h | 29 ++-- src/bun.js/bindings/napi_external.cpp | 6 +- src/bun.js/bindings/napi_external.h | 14 +- src/bun.js/bindings/napi_finalizer.cpp | 6 +- src/bun.js/bindings/napi_finalizer.h | 2 +- src/bun.js/bindings/napi_handle_scope.h | 2 +- src/napi/js_native_api_types.h | 182 ++++++++++++------------ src/napi/napi.zig | 12 ++ 14 files changed, 168 insertions(+), 139 deletions(-) diff --git a/src/bun.js/api/FFI.h b/src/bun.js/api/FFI.h index 6ca644a1e2..c3796712aa 100644 --- a/src/bun.js/api/FFI.h +++ b/src/bun.js/api/FFI.h @@ -39,7 +39,7 @@ typedef _Bool bool; #define false 0 #ifndef SRC_JS_NATIVE_API_TYPES_H_ -typedef struct napi_env__ *napi_env; +typedef struct NapiEnv *napi_env; typedef int64_t napi_value; typedef enum { napi_ok, @@ -67,7 +67,7 @@ typedef enum { } napi_status; BUN_FFI_IMPORT void* NapiHandleScope__open(void* napi_env, bool detached); BUN_FFI_IMPORT void NapiHandleScope__close(void* napi_env, void* handleScope); -BUN_FFI_IMPORT extern struct napi_env__ Bun__thisFFIModuleNapiEnv; +BUN_FFI_IMPORT extern struct NapiEnv Bun__thisFFIModuleNapiEnv; #endif diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 9ce22609dc..11ccf98275 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -637,7 +637,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb auto env = globalObject->makeNapiEnv(nmodule); env->filename = filename_cstr; - auto encoded = reinterpret_cast(napi_register_module_v1(env, reinterpret_cast(exportsValue))); + auto encoded = reinterpret_cast(napi_register_module_v1(env.ptr(), reinterpret_cast(exportsValue))); if (env->throwPendingException()) { return {}; } @@ -656,7 +656,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb // TODO: think about the finalizer here // currently we do not dealloc napi modules so we don't have to worry about it right now auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, env, nullptr); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr, env.ptr()); bool success = resultObject->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); ASSERT(success); RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/NapiRef.cpp b/src/bun.js/bindings/NapiRef.cpp index d33ac46cef..03660630b9 100644 --- a/src/bun.js/bindings/NapiRef.cpp +++ b/src/bun.js/bindings/NapiRef.cpp @@ -37,7 +37,7 @@ void NapiRef::unref() void NapiRef::clear() { NAPI_LOG("ref clear %p", this); - finalizer.call(env, nativeObject); + finalizer.call(env.ptr(), nativeObject); globalObject.clear(); weakValueRef.clear(); strongRef.clear(); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 0311fd6f3e..a2f8b35b4c 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3517,10 +3517,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h } } -napi_env GlobalObject::makeNapiEnv(const napi_module& mod) +Ref GlobalObject::makeNapiEnv(const napi_module& mod) { - m_napiEnvs.append(std::make_unique(this, mod)); - return m_napiEnvs.last().get(); + m_napiEnvs.append(NapiEnv::create(this, mod)); + return m_napiEnvs.last(); } napi_env GlobalObject::makeNapiEnvForFFI() @@ -3534,7 +3534,7 @@ napi_env GlobalObject::makeNapiEnvForFFI() .nm_priv = nullptr, .reserved = {}, }); - return out; + return &out.leakRef(); } bool GlobalObject::hasNapiFinalizers() const diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 6366c7ba92..937da8c906 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -724,8 +724,8 @@ public: // De-optimization once `require("module").runMain` is written to bool hasOverriddenModuleRunMain = false; - WTF::Vector> m_napiEnvs; - napi_env makeNapiEnv(const napi_module&); + WTF::Vector> m_napiEnvs; + Ref makeNapiEnv(const napi_module&); napi_env makeNapiEnvForFFI(); bool hasNapiFinalizers() const; diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index b06b4a2b6f..cc8f63465f 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -698,7 +698,7 @@ void Napi::executePendingNapiModule(Zig::GlobalObject* globalObject) ASSERT(globalObject->m_pendingNapiModule); auto& mod = *globalObject->m_pendingNapiModule; - napi_env env = globalObject->makeNapiEnv(mod); + Ref env = globalObject->makeNapiEnv(mod); auto keyStr = WTF::String::fromUTF8(mod.nm_modname); JSValue pendingNapiModule = globalObject->m_pendingNapiModuleAndExports[0].get(); JSObject* object = (pendingNapiModule && pendingNapiModule.isObject()) ? pendingNapiModule.getObject() @@ -727,7 +727,7 @@ void Napi::executePendingNapiModule(Zig::GlobalObject* globalObject) JSValue resultValue; if (mod.nm_register_func) { - resultValue = toJS(mod.nm_register_func(env, toNapi(object, globalObject))); + resultValue = toJS(mod.nm_register_func(env.ptr(), toNapi(object, globalObject))); } else { JSValue errorInstance = createError(globalObject, makeString("Module has no declared entry point."_s)); globalObject->m_pendingNapiModuleAndExports[0].set(vm, globalObject, errorInstance); @@ -751,7 +751,7 @@ void Napi::executePendingNapiModule(Zig::GlobalObject* globalObject) auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); // TODO: think about the finalizer here - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, env, nullptr); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr, env.ptr()); bool success = resultValue.getObject()->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); ASSERT(success); @@ -791,7 +791,7 @@ static void wrap_cleanup(napi_env env, void* data, void* hint) { auto* ref = reinterpret_cast(data); ASSERT(ref->boundCleanup != nullptr); - ref->boundCleanup->deactivate(env); + ref->boundCleanup->deactivate(*env); ref->boundCleanup = nullptr; ref->callFinalizer(); } @@ -842,7 +842,7 @@ extern "C" napi_status napi_wrap(napi_env env, NAPI_RETURN_EARLY_IF_FALSE(env, existing_wrap == nullptr, napi_invalid_arg); // create a new weak reference (refcount 0) - auto* ref = new NapiRef(env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); + auto* ref = new NapiRef(*env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); // In case the ref's finalizer is never called, we'll add a finalizer to execute on exit. const auto& bound_cleanup = env->addFinalizer(wrap_cleanup, native_object, ref); ref->boundCleanup = &bound_cleanup; @@ -852,7 +852,7 @@ extern "C" napi_status napi_wrap(napi_env env, napi_instance->napiRef = ref; } else { // wrap the ref in an external so that it can serve as a JSValue - auto* external = Bun::NapiExternal::create(JSC::getVM(globalObject), globalObject->NapiExternalStructure(), ref, nullptr, env, nullptr); + auto* external = Bun::NapiExternal::create(JSC::getVM(globalObject), globalObject->NapiExternalStructure(), ref, nullptr, nullptr, env); jsc_object->putDirect(vm, propertyName, JSValue(external)); } @@ -1082,7 +1082,7 @@ extern "C" napi_status napi_create_reference(napi_env env, napi_value value, can_be_weak = false; } - auto* ref = new NapiRef(env, initial_refcount, Bun::NapiFinalizer {}); + auto* ref = new NapiRef(*env, initial_refcount, Bun::NapiFinalizer {}); ref->setValueInitial(val, can_be_weak); *result = toNapi(ref); @@ -1119,14 +1119,14 @@ extern "C" napi_status napi_add_finalizer(napi_env env, napi_value js_object, if (result) { // If they're expecting a Ref, use the ref. - auto* ref = new NapiRef(env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); + auto* ref = new NapiRef(*env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); // TODO(@heimskr): consider detecting whether the value can't be weak, as we do in napi_create_reference. ref->setValueInitial(object, true); ref->nativeObject = native_object; *result = toNapi(ref); } else { // Otherwise, it's cheaper to just call .addFinalizer. - vm.heap.addFinalizer(object, [env, finalize_cb, native_object, finalize_hint](JSCell* cell) -> void { + vm.heap.addFinalizer(object, [env = WTF::Ref(*env), finalize_cb, native_object, finalize_hint](JSCell* cell) -> void { NAPI_LOG("finalizer %p", finalize_hint); env->doFinalizer(finalize_cb, native_object, finalize_hint); }); @@ -1991,7 +1991,7 @@ extern "C" napi_status napi_create_external_buffer(napi_env env, size_t length, Zig::GlobalObject* globalObject = toJS(env); - auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(data), length }, createSharedTask([env, finalize_hint, finalize_cb](void* p) { + auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(data), length }, createSharedTask([env = WTF::Ref(*env), finalize_hint, finalize_cb](void* p) { NAPI_LOG("external buffer finalizer"); env->doFinalizer(finalize_cb, p, finalize_hint); })); @@ -2303,7 +2303,7 @@ extern "C" napi_status napi_create_external(napi_env env, void* data, JSC::VM& vm = JSC::getVM(globalObject); auto* structure = globalObject->NapiExternalStructure(); - JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, env, finalize_cb); + JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, finalize_cb, env); JSC::EnsureStillAliveScope ensureStillAlive(value); *result = toNapi(value, globalObject); NAPI_RETURN_SUCCESS(env); @@ -2902,4 +2902,14 @@ extern "C" bool NapiEnv__getAndClearPendingException(napi_env env, JSC::EncodedJ return false; } +extern "C" void NapiEnv__ref(napi_env env) +{ + env->ref(); +} + +extern "C" void NapiEnv__deref(napi_env env) +{ + env->deref(); +} + } diff --git a/src/bun.js/bindings/napi.h b/src/bun.js/bindings/napi.h index f5a54314fe..56346cd019 100644 --- a/src/bun.js/bindings/napi.h +++ b/src/bun.js/bindings/napi.h @@ -168,9 +168,11 @@ static bool equal(napi_async_cleanup_hook_handle one, napi_async_cleanup_hook_ha } while (0) // Named this way so we can manipulate napi_env values directly (since napi_env is defined as a pointer to struct napi_env__) -struct napi_env__ { +struct NapiEnv : public WTF::RefCounted { + WTF_MAKE_STRUCT_TZONE_ALLOCATED(NapiEnv); + public: - napi_env__(Zig::GlobalObject* globalObject, const napi_module& napiModule) + NapiEnv(Zig::GlobalObject* globalObject, const napi_module& napiModule) : m_globalObject(globalObject) , m_napiModule(napiModule) , m_vm(JSC::getVM(globalObject)) @@ -178,7 +180,12 @@ public: napi_internal_register_cleanup_zig(this); } - ~napi_env__() + static Ref create(Zig::GlobalObject* globalObject, const napi_module& napiModule) + { + return adoptRef(*new NapiEnv(globalObject, napiModule)); + } + + ~NapiEnv() { delete[] filename; } @@ -434,12 +441,12 @@ public: } } - void deactivate(napi_env env) const + void deactivate(NapiEnv& env) const { - if (env->isFinishingFinalizers()) { + if (env.isFinishingFinalizers()) { active = false; } else { - env->removeFinalizer(*this); + env.removeFinalizer(*this); // At this point the BoundFinalizer has been destroyed, but because we're not doing anything else here it's safe. // https://isocpp.org/wiki/faq/freestore-mgmt#delete-this } @@ -451,7 +458,7 @@ public: } struct Hash { - std::size_t operator()(const napi_env__::BoundFinalizer& bound) const + std::size_t operator()(const NapiEnv::BoundFinalizer& bound) const { constexpr std::hash hasher; constexpr std::ptrdiff_t magic = 0x9e3779b9; @@ -659,7 +666,7 @@ public: void unref(); void clear(); - NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer) + NapiRef(Ref&& env, uint32_t count, Bun::NapiFinalizer finalizer) : env(env) , globalObject(JSC::Weak(env->globalObject())) , finalizer(WTFMove(finalizer)) @@ -708,7 +715,7 @@ public: // calling the finalizer Bun::NapiFinalizer saved_finalizer = this->finalizer; this->finalizer.clear(); - saved_finalizer.call(env, nativeObject, !env->mustDeferFinalizers() || !env->inGC()); + saved_finalizer.call(env.ptr(), nativeObject, !env->mustDeferFinalizers() || !env->inGC()); } ~NapiRef() @@ -728,12 +735,12 @@ public: weakValueRef.clear(); } - napi_env env = nullptr; + WTF::Ref env; JSC::Weak globalObject; NapiWeakValue weakValueRef; JSC::Strong strongRef; Bun::NapiFinalizer finalizer; - const napi_env__::BoundFinalizer* boundCleanup = nullptr; + const NapiEnv::BoundFinalizer* boundCleanup = nullptr; void* nativeObject = nullptr; uint32_t refCount = 0; bool releaseOnWeaken = false; diff --git a/src/bun.js/bindings/napi_external.cpp b/src/bun.js/bindings/napi_external.cpp index c303c85c4b..239ba8c2fe 100644 --- a/src/bun.js/bindings/napi_external.cpp +++ b/src/bun.js/bindings/napi_external.cpp @@ -5,8 +5,8 @@ namespace Bun { NapiExternal::~NapiExternal() { - ASSERT(m_env); - m_finalizer.call(m_env, m_value, !m_env->mustDeferFinalizers()); + auto* env = m_env.get(); + m_finalizer.call(env, m_value, env && !env->mustDeferFinalizers()); } void NapiExternal::destroy(JSC::JSCell* cell) @@ -14,6 +14,6 @@ void NapiExternal::destroy(JSC::JSCell* cell) static_cast(cell)->~NapiExternal(); } -const ClassInfo NapiExternal::s_info = { "External"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NapiExternal) }; +const ClassInfo NapiExternal::s_info = { "NapiExternal"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NapiExternal) }; } diff --git a/src/bun.js/bindings/napi_external.h b/src/bun.js/bindings/napi_external.h index c34a4272a7..2d104fceb8 100644 --- a/src/bun.js/bindings/napi_external.h +++ b/src/bun.js/bindings/napi_external.h @@ -22,8 +22,9 @@ class NapiExternal : public JSC::JSDestructibleObject { using Base = JSC::JSDestructibleObject; public: - NapiExternal(JSC::VM& vm, JSC::Structure* structure) + NapiExternal(JSC::VM& vm, JSC::Structure* structure, WTF::RefPtr env) : Base(vm, structure) + , m_env(env) { } @@ -53,11 +54,11 @@ public: JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, napi_env env, napi_finalize callback) + static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, napi_finalize callback, WTF::RefPtr env = nullptr) { - NapiExternal* accessor = new (NotNull, JSC::allocateCell(vm)) NapiExternal(vm, structure); + NapiExternal* accessor = new (NotNull, JSC::allocateCell(vm)) NapiExternal(vm, structure, env); - accessor->finishCreation(vm, value, finalizer_hint, env, callback); + accessor->finishCreation(vm, value, finalizer_hint, callback); #if ASSERT_ENABLED if (auto* callFrame = vm.topCallFrame) { @@ -81,11 +82,10 @@ public: return accessor; } - void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, napi_env env, napi_finalize callback) + void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, napi_finalize callback) { Base::finishCreation(vm); m_value = value; - m_env = env; m_finalizer = NapiFinalizer { callback, finalizer_hint }; } @@ -95,7 +95,7 @@ public: void* m_value; NapiFinalizer m_finalizer; - napi_env m_env; + WTF::RefPtr m_env; #if ASSERT_ENABLED String sourceOriginURL = String(); diff --git a/src/bun.js/bindings/napi_finalizer.cpp b/src/bun.js/bindings/napi_finalizer.cpp index cc2c25ea09..afc566f8b2 100644 --- a/src/bun.js/bindings/napi_finalizer.cpp +++ b/src/bun.js/bindings/napi_finalizer.cpp @@ -5,14 +5,14 @@ namespace Bun { -void NapiFinalizer::call(napi_env env, void* data, bool immediate) +void NapiFinalizer::call(WTF::RefPtr env, void* data, bool immediate) { if (m_callback) { NAPI_LOG_CURRENT_FUNCTION; if (immediate) { - m_callback(env, data, m_hint); + m_callback(env.get(), data, m_hint); } else { - napi_internal_enqueue_finalizer(env, m_callback, data, m_hint); + napi_internal_enqueue_finalizer(env.get(), m_callback, data, m_hint); } } } diff --git a/src/bun.js/bindings/napi_finalizer.h b/src/bun.js/bindings/napi_finalizer.h index 65d4bbccfa..4bf6e08382 100644 --- a/src/bun.js/bindings/napi_finalizer.h +++ b/src/bun.js/bindings/napi_finalizer.h @@ -17,7 +17,7 @@ public: NapiFinalizer() = default; - void call(napi_env env, void* data, bool immediate = false); + void call(WTF::RefPtr env, void* data, bool immediate = false); void clear(); inline napi_finalize callback() const { return m_callback; } diff --git a/src/bun.js/bindings/napi_handle_scope.h b/src/bun.js/bindings/napi_handle_scope.h index a8dcc14a6f..a004716637 100644 --- a/src/bun.js/bindings/napi_handle_scope.h +++ b/src/bun.js/bindings/napi_handle_scope.h @@ -3,7 +3,7 @@ #include "BunClientData.h" #include "root.h" -typedef struct napi_env__* napi_env; +typedef struct NapiEnv* napi_env; namespace Bun { diff --git a/src/napi/js_native_api_types.h b/src/napi/js_native_api_types.h index 16f09afe13..9341e7b91a 100644 --- a/src/napi/js_native_api_types.h +++ b/src/napi/js_native_api_types.h @@ -13,86 +13,86 @@ typedef uint16_t char16_t; // JSVM API types are all opaque pointers for ABI stability // typedef undefined structs instead of void* for compile time type safety -typedef struct napi_env__ *napi_env; -typedef struct napi_value__ *napi_value; -typedef struct napi_ref__ *napi_ref; -typedef struct napi_handle_scope__ *napi_handle_scope; -typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope; -typedef struct napi_callback_info__ *napi_callback_info; -typedef struct napi_deferred__ *napi_deferred; +typedef struct NapiEnv* napi_env; +typedef struct napi_value__* napi_value; +typedef struct napi_ref__* napi_ref; +typedef struct napi_handle_scope__* napi_handle_scope; +typedef struct napi_escapable_handle_scope__* napi_escapable_handle_scope; +typedef struct napi_callback_info__* napi_callback_info; +typedef struct napi_deferred__* napi_deferred; typedef enum { - napi_default = 0, - napi_writable = 1 << 0, - napi_enumerable = 1 << 1, - napi_configurable = 1 << 2, + napi_default = 0, + napi_writable = 1 << 0, + napi_enumerable = 1 << 1, + napi_configurable = 1 << 2, - // Used with napi_define_class to distinguish static properties - // from instance properties. Ignored by napi_define_properties. - napi_static = 1 << 10, + // Used with napi_define_class to distinguish static properties + // from instance properties. Ignored by napi_define_properties. + napi_static = 1 << 10, #if NAPI_VERSION >= 8 - // Default for class methods. - napi_default_method = napi_writable | napi_configurable, + // Default for class methods. + napi_default_method = napi_writable | napi_configurable, - // Default for object properties, like in JS obj[prop]. - napi_default_jsproperty = napi_writable | napi_enumerable | napi_configurable, + // Default for object properties, like in JS obj[prop]. + napi_default_jsproperty = napi_writable | napi_enumerable | napi_configurable, #endif // NAPI_VERSION >= 8 } napi_property_attributes; typedef enum { - // ES6 types (corresponds to typeof) - napi_undefined, - napi_null, - napi_boolean, - napi_number, - napi_string, - napi_symbol, - napi_object, - napi_function, - napi_external, - napi_bigint, + // ES6 types (corresponds to typeof) + napi_undefined, + napi_null, + napi_boolean, + napi_number, + napi_string, + napi_symbol, + napi_object, + napi_function, + napi_external, + napi_bigint, } napi_valuetype; typedef enum { - napi_int8_array, - napi_uint8_array, - napi_uint8_clamped_array, - napi_int16_array, - napi_uint16_array, - napi_int32_array, - napi_uint32_array, - napi_float32_array, - napi_float64_array, - napi_bigint64_array, - napi_biguint64_array, + napi_int8_array, + napi_uint8_array, + napi_uint8_clamped_array, + napi_int16_array, + napi_uint16_array, + napi_int32_array, + napi_uint32_array, + napi_float32_array, + napi_float64_array, + napi_bigint64_array, + napi_biguint64_array, } napi_typedarray_type; typedef enum { - napi_ok, - napi_invalid_arg, - napi_object_expected, - napi_string_expected, - napi_name_expected, - napi_function_expected, - napi_number_expected, - napi_boolean_expected, - napi_array_expected, - napi_generic_failure, - napi_pending_exception, - napi_cancelled, - napi_escape_called_twice, - napi_handle_scope_mismatch, - napi_callback_scope_mismatch, - napi_queue_full, - napi_closing, - napi_bigint_expected, - napi_date_expected, - napi_arraybuffer_expected, - napi_detachable_arraybuffer_expected, - napi_would_deadlock, // unused - napi_no_external_buffers_allowed, - napi_cannot_run_js, + napi_ok, + napi_invalid_arg, + napi_object_expected, + napi_string_expected, + napi_name_expected, + napi_function_expected, + napi_number_expected, + napi_boolean_expected, + napi_array_expected, + napi_generic_failure, + napi_pending_exception, + napi_cancelled, + napi_escape_called_twice, + napi_handle_scope_mismatch, + napi_callback_scope_mismatch, + napi_queue_full, + napi_closing, + napi_bigint_expected, + napi_date_expected, + napi_arraybuffer_expected, + napi_detachable_arraybuffer_expected, + napi_would_deadlock, // unused + napi_no_external_buffers_allowed, + napi_cannot_run_js, } napi_status; // Note: when adding a new enum value to `napi_status`, please also update // * `constexpr int last_status` in the definition of `napi_get_last_error_info()' @@ -101,55 +101,55 @@ typedef enum { // message explaining the error. typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info); -typedef void (*napi_finalize)(napi_env env, void *finalize_data, - void *finalize_hint); +typedef void (*napi_finalize)(napi_env env, void* finalize_data, + void* finalize_hint); typedef struct { - // One of utf8name or name should be NULL. - const char *utf8name; - napi_value name; + // One of utf8name or name should be NULL. + const char* utf8name; + napi_value name; - napi_callback method; - napi_callback getter; - napi_callback setter; - napi_value value; + napi_callback method; + napi_callback getter; + napi_callback setter; + napi_value value; - napi_property_attributes attributes; - void *data; + napi_property_attributes attributes; + void* data; } napi_property_descriptor; typedef struct { - const char *error_message; - void *engine_reserved; - uint32_t engine_error_code; - napi_status error_code; + const char* error_message; + void* engine_reserved; + uint32_t engine_error_code; + napi_status error_code; } napi_extended_error_info; #if NAPI_VERSION >= 6 typedef enum { - napi_key_include_prototypes, - napi_key_own_only + napi_key_include_prototypes, + napi_key_own_only } napi_key_collection_mode; typedef enum { - napi_key_all_properties = 0, - napi_key_writable = 1, - napi_key_enumerable = 1 << 1, - napi_key_configurable = 1 << 2, - napi_key_skip_strings = 1 << 3, - napi_key_skip_symbols = 1 << 4 + napi_key_all_properties = 0, + napi_key_writable = 1, + napi_key_enumerable = 1 << 1, + napi_key_configurable = 1 << 2, + napi_key_skip_strings = 1 << 3, + napi_key_skip_symbols = 1 << 4 } napi_key_filter; typedef enum { - napi_key_keep_numbers, - napi_key_numbers_to_strings + napi_key_keep_numbers, + napi_key_numbers_to_strings } napi_key_conversion; #endif // NAPI_VERSION >= 6 #if NAPI_VERSION >= 8 typedef struct { - uint64_t lower; - uint64_t upper; + uint64_t lower; + uint64_t upper; } napi_type_tag; #endif // NAPI_VERSION >= 8 diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 708b453b1c..1be2beb975 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -55,9 +55,19 @@ pub const NapiEnv = opaque { return null; } + pub fn ref(self: *NapiEnv) void { + NapiEnv__ref(self); + } + + pub fn deref(self: *NapiEnv) void { + NapiEnv__deref(self); + } + extern fn NapiEnv__globalObject(*NapiEnv) *jsc.JSGlobalObject; extern fn NapiEnv__getAndClearPendingException(*NapiEnv, *JSValue) bool; extern fn napi_internal_get_version(*NapiEnv) u32; + extern fn NapiEnv__deref(*NapiEnv) void; + extern fn NapiEnv__ref(*NapiEnv) void; }; fn envIsNull() napi_status { @@ -1660,6 +1670,7 @@ pub const ThreadSafeFunction = struct { this.callback.deinit(); this.queue.deinit(); + this.env.deref(); bun.destroy(this); } @@ -1757,6 +1768,7 @@ pub export fn napi_create_threadsafe_function( // nodejs by default keeps the event loop alive until the thread-safe function is unref'd function.ref(); function.tracker.didSchedule(vm.global); + function.env.ref(); result.* = function; return env.ok(); From b90abdda084ab189dc3f70b36a2069c95c9fd106 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 22 Oct 2025 12:13:14 -0700 Subject: [PATCH 224/391] BUmp --- LATEST | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LATEST b/LATEST index f0bb29e763..3a3cd8cc8b 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.3.0 +1.3.1 diff --git a/package.json b/package.json index b6ed0b981f..bc4df314a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "bun", - "version": "1.3.1", + "version": "1.3.2", "workspaces": [ "./packages/bun-types", "./packages/@types/bun" From 0ad4e6af2dfb31173a27aa576d09a341162833e1 Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 22 Oct 2025 16:15:29 -0700 Subject: [PATCH 225/391] Fix Buffer.isEncoding('') to return false (#23968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes `Buffer.isEncoding('')` to return `false` instead of `true`, matching Node.js behavior. ## Description Previously, `Buffer.isEncoding('')` incorrectly returned `true` in Bun, while Node.js correctly returns `false`. This was caused by `parseEnumerationFromView` in `JSBufferEncodingType.cpp` treating empty strings (length 0) as valid utf8 encoding. The fix modifies the switch statement to return `std::nullopt` for empty strings, along with other invalid short strings. ## Changes - Modified `src/bun.js/bindings/JSBufferEncodingType.cpp` to return `std::nullopt` for empty strings - Added regression test `test/regression/issue23966.test.ts` ## Test Plan - [x] Test fails with `USE_SYSTEM_BUN=1 bun test test/regression/issue23966.test.ts` (confirms bug exists) - [x] Test passes with `bun bd test test/regression/issue23966.test.ts` (confirms fix works) - [x] Verified behavior matches Node.js v24.3.0 - [x] All test cases for valid/invalid encodings pass Fixes #23966 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/JSBufferEncodingType.cpp | 4 +- test/regression/issue23966.test.ts | 42 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/regression/issue23966.test.ts diff --git a/src/bun.js/bindings/JSBufferEncodingType.cpp b/src/bun.js/bindings/JSBufferEncodingType.cpp index 312e30d806..f297c899fd 100644 --- a/src/bun.js/bindings/JSBufferEncodingType.cpp +++ b/src/bun.js/bindings/JSBufferEncodingType.cpp @@ -104,9 +104,7 @@ template<> std::optional parseEnumerationFromView { + expect(Buffer.isEncoding("")).toBe(false); +}); + +const validEncodings = [ + "utf8", + "utf-8", + "hex", + "base64", + "ascii", + "latin1", + "binary", + "ucs2", + "ucs-2", + "utf16le", + "utf-16le", +]; +const invalidEncodings = ["invalid", "utf32", "something"]; +const nonStringValues = [ + { value: 123, name: "number" }, + { value: null, name: "null" }, + { value: undefined, name: "undefined" }, + { value: {}, name: "object" }, + { value: [], name: "array" }, +]; + +test.concurrent.each(validEncodings)("Buffer.isEncoding('%s') should return true", encoding => { + expect(Buffer.isEncoding(encoding)).toBe(true); +}); + +test.concurrent.each(invalidEncodings)("Buffer.isEncoding('%s') should return false", encoding => { + expect(Buffer.isEncoding(encoding)).toBe(false); +}); + +test.concurrent.each(nonStringValues)("Buffer.isEncoding($name) should return false for non-string", ({ value }) => { + expect(Buffer.isEncoding(value as any)).toBe(false); +}); From 066f706a992215e0b13d529bef5416d3a896d4ab Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 22 Oct 2025 16:45:03 -0700 Subject: [PATCH 226/391] Fix CSS view-transition pseudo-elements with class selectors (#23957) --- src/css/selectors/parser.zig | 14 ++++ .../bundler/css/view-transition-23600.test.ts | 73 +++++++++++++++++++ test/js/bun/css/css.test.ts | 3 + 3 files changed, 90 insertions(+) create mode 100644 test/bundler/css/view-transition-23600.test.ts diff --git a/src/css/selectors/parser.zig b/src/css/selectors/parser.zig index b4ce5d2181..9a73bc0858 100644 --- a/src/css/selectors/parser.zig +++ b/src/css/selectors/parser.zig @@ -3591,11 +3591,17 @@ pub const ViewTransitionPartName = union(enum) { all, /// name: css.css_values.ident.CustomIdent, + /// . + class: css.css_values.ident.CustomIdent, pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void { return switch (this.*) { .all => try dest.writeStr("*"), .name => |name| try css.CustomIdentFns.toCss(&name, W, dest), + .class => |name| { + try dest.writeChar('.'); + try css.CustomIdentFns.toCss(&name, W, dest); + }, }; } @@ -3604,6 +3610,14 @@ pub const ViewTransitionPartName = union(enum) { return .{ .result = .all }; } + // Try to parse a class selector (.) + if (input.tryParse(css.Parser.expectDelim, .{'.'}).isOk()) { + return .{ .result = .{ .class = switch (css.css_values.ident.CustomIdent.parse(input)) { + .result => |v| v, + .err => |e| return .{ .err = e }, + } } }; + } + return .{ .result = .{ .name = switch (css.css_values.ident.CustomIdent.parse(input)) { .result => |v| v, .err => |e| return .{ .err = e }, diff --git a/test/bundler/css/view-transition-23600.test.ts b/test/bundler/css/view-transition-23600.test.ts new file mode 100644 index 0000000000..e29384fa98 --- /dev/null +++ b/test/bundler/css/view-transition-23600.test.ts @@ -0,0 +1,73 @@ +import { itBundled } from "../expectBundled"; + +describe("css", () => { + itBundled("css/view-transition-class-selector-23600", { + files: { + "index.css": /* css */ ` + @keyframes slide-out { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(-100%); + } + } + + ::view-transition-old(.slide-out) { + animation-name: slide-out; + animation-timing-function: ease-in-out; + } + + ::view-transition-new(.fade-in) { + animation-name: fade-in; + } + + ::view-transition-group(.card) { + animation-duration: 1s; + } + + ::view-transition-image-pair(.hero) { + isolation: isolate; + } + `, + }, + outdir: "/out", + entryPoints: ["/index.css"], + onAfterBundle(api) { + api.expectFile("/out/index.css").toMatchInlineSnapshot(` + "/* index.css */ + @keyframes slide-out { + from { + opacity: 1; + transform: translateX(0); + } + + to { + opacity: 0; + transform: translateX(-100%); + } + } + + ::view-transition-old(.slide-out) { + animation-name: slide-out; + animation-timing-function: ease-in-out; + } + + ::view-transition-new(.fade-in) { + animation-name: fade-in; + } + + ::view-transition-group(.card) { + animation-duration: 1s; + } + + ::view-transition-image-pair(.hero) { + isolation: isolate; + } + " + `); + }, + }); +}); diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 69e14d53d1..f147451099 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -5575,6 +5575,9 @@ describe("css tests", () => { minify_test(`:root::${name}(*) {position: fixed}`, `:root::${name}(*){position:fixed}`); minify_test(`:root::${name}(foo) {position: fixed}`, `:root::${name}(foo){position:fixed}`); minify_test(`:root::${name}(foo):only-child {position: fixed}`, `:root::${name}(foo):only-child{position:fixed}`); + // Test class selector syntax (.class-name) + minify_test(`:root::${name}(.slide-out) {position: fixed}`, `:root::${name}(.slide-out){position:fixed}`); + minify_test(`:root::${name}(.fade-in) {animation-name: fade}`, `:root::${name}(.fade-in){animation-name:fade}`); error_test( `:root::${name}(foo):first-child {position: fixed}`, "ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement)", From b278c8575363665fda9f2c449d9691872051a1d7 Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 22 Oct 2025 21:46:26 -0700 Subject: [PATCH 227/391] Refactor NapiEnv to use ExternalShared for safer reference counting (#23982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR refactors `NapiEnv` to use `bun.ptr.ExternalShared` instead of manual `ref()`/`deref()` calls, fixing a use-after-free bug in the NAPI implementation. ## Bug Fixed The original issue was in `ThreadSafeFunction.deinit()`: 1. `maybeQueueFinalizer()` schedules a task that holds a pointer to `this` (which includes `this.env`) 2. The task will eventually call `onDispatch()` → `deinit()` 3. But `deinit()` immediately calls `this.env.deref()` before the task completes 4. This could cause the `NapiEnv` reference count to go to 0 while the pointer is still in use ## Changes ### Core Changes - Added `NapiEnv.external_shared_descriptor` and `NapiEnv.EnvRef` type alias - Changed struct fields from `*NapiEnv` to `NapiEnv.EnvRef` where ownership is required: - `ThreadSafeFunction.env` - `napi_async_work.env` - `Finalizer.env` (now `NapiEnv.EnvRef.Optional`) ### API Changes - Use `.get()` to access the raw `*NapiEnv` pointer from `EnvRef` - Use `.cloneFromRaw(env)` when storing `env` in long-lived structs - Use `EnvRef.deinit()` instead of manual `env.deref()` - Removed manual `env.ref()` calls (now handled automatically by `cloneFromRaw`) ### Safety Improvements - Reference counting is now managed by the `ExternalShared` wrapper - Prevents manual ref/deref mistakes - Ensures proper cleanup even when operations are cancelled or fail - No more use-after-free risks from premature deref ## Testing Built successfully with `bun bd`. NAPI tests pass (66/83 tests, with 17 timeouts that appear to be pre-existing issues). ## Implementation Notes Following the pattern from `Blob.zig` and `array_buffer.zig`, structs that own a reference use `NapiEnv.EnvRef`, while functions that only borrow temporarily continue to use `*NapiEnv` parameters. The `ExternalShared` interface ensures: - `.clone()` increments the ref count - `.deinit()` decrements the ref count - No direct access to the internal ref/deref functions This makes the ownership semantics explicit and type-safe. --------- Co-authored-by: Claude Bot Co-authored-by: taylor.fish --- src/napi/napi.zig | 86 +++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 1be2beb975..a2109168a1 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -55,19 +55,18 @@ pub const NapiEnv = opaque { return null; } - pub fn ref(self: *NapiEnv) void { - NapiEnv__ref(self); - } - - pub fn deref(self: *NapiEnv) void { - NapiEnv__deref(self); - } - extern fn NapiEnv__globalObject(*NapiEnv) *jsc.JSGlobalObject; extern fn NapiEnv__getAndClearPendingException(*NapiEnv, *JSValue) bool; extern fn napi_internal_get_version(*NapiEnv) u32; extern fn NapiEnv__deref(*NapiEnv) void; extern fn NapiEnv__ref(*NapiEnv) void; + + pub const external_shared_descriptor = struct { + pub const ref = NapiEnv__ref; + pub const deref = NapiEnv__deref; + }; + + pub const Ref = bun.ptr.ExternalShared(NapiEnv); }; fn envIsNull() napi_status { @@ -249,7 +248,8 @@ pub const napi_status = c_uint; pub const napi_callback = ?*const fn (napi_env, napi_callback_info) callconv(.C) napi_value; /// expects `napi_env`, `callback_data`, `context` -pub const napi_finalize = ?*const fn (napi_env, ?*anyopaque, ?*anyopaque) callconv(.C) void; +pub const NapiFinalizeFunction = *const fn (napi_env, ?*anyopaque, ?*anyopaque) callconv(.C) void; +pub const napi_finalize = ?NapiFinalizeFunction; pub const napi_property_descriptor = extern struct { utf8name: [*c]const u8, name: napi_value, @@ -1038,7 +1038,7 @@ pub const napi_async_work = struct { concurrent_task: jsc.ConcurrentTask = .{}, event_loop: *jsc.EventLoop, global: *jsc.JSGlobalObject, - env: *NapiEnv, + env: NapiEnv.Ref, execute: napi_async_execute_callback, complete: ?napi_async_complete_callback, data: ?*anyopaque = null, @@ -1058,7 +1058,7 @@ pub const napi_async_work = struct { const work = bun.new(napi_async_work, .{ .global = global, - .env = env, + .env = .cloneFromRaw(env), .execute = execute, .event_loop = global.bunVM().eventLoop(), .complete = complete, @@ -1068,6 +1068,7 @@ pub const napi_async_work = struct { } pub fn destroy(this: *napi_async_work) void { + this.env.deinit(); bun.destroy(this); } @@ -1089,7 +1090,7 @@ pub const napi_async_work = struct { return; } } - this.execute(this.env, this.data); + this.execute(this.env.get(), this.data); this.status.store(.completed, .seq_cst); this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); @@ -1109,7 +1110,7 @@ pub const napi_async_work = struct { return; }; - const env = this.env; + const env = this.env.get(); const handle_scope = NapiHandleScope.open(env, false); defer if (handle_scope) |scope| scope.close(env); @@ -1368,20 +1369,17 @@ pub export fn napi_internal_suppress_crash_on_abort_if_desired() void { extern fn napi_internal_remove_finalizer(env: napi_env, fun: napi_finalize, hint: ?*anyopaque, data: ?*anyopaque) callconv(.C) void; pub const Finalizer = struct { - env: napi_env, - fun: napi_finalize, + env: NapiEnv.Ref, + fun: NapiFinalizeFunction, data: ?*anyopaque = null, hint: ?*anyopaque = null, pub fn run(this: *Finalizer) void { - const env = this.env.?; + const env = this.env.get(); const handle_scope = NapiHandleScope.open(env, false); defer if (handle_scope) |scope| scope.close(env); - if (this.fun) |fun| { - fun(env, this.data, this.hint); - } - + this.fun(env, this.data, this.hint); napi_internal_remove_finalizer(env, this.fun, this.hint, this.data); if (env.toJS().tryTakeException()) |exception| { @@ -1393,12 +1391,28 @@ pub const Finalizer = struct { } } + pub fn deinit(this: *Finalizer) void { + this.env.deinit(); + this.* = undefined; + } + /// For Node-API modules not built with NAPI_EXPERIMENTAL, finalizers should be deferred to the /// immediate task queue instead of run immediately. This lets finalizers perform allocations, /// which they couldn't if they ran immediately while the garbage collector is still running. pub export fn napi_internal_enqueue_finalizer(env: napi_env, fun: napi_finalize, data: ?*anyopaque, hint: ?*anyopaque) callconv(.C) void { - const task = NapiFinalizerTask.init(.{ .env = env, .fun = fun, .data = data, .hint = hint }); - task.schedule(); + var this: Finalizer = .{ + .fun = fun orelse return, + .env = .cloneFromRaw(env orelse return), + .data = data, + .hint = hint, + }; + this.enqueue(); + } + + /// Takes ownership of `this`. + pub fn enqueue(this: *Finalizer) void { + NapiFinalizerTask.init(this.*).schedule(); + this.* = undefined; } }; @@ -1439,9 +1453,10 @@ pub const ThreadSafeFunction = struct { event_loop: *jsc.EventLoop, tracker: jsc.Debugger.AsyncTaskTracker, - env: *NapiEnv, + env: NapiEnv.Ref, + finalizer_fun: napi_finalize = null, + finalizer_data: ?*anyopaque = null, - finalizer: Finalizer = Finalizer{ .env = null, .fun = null, .data = null }, has_queued_finalizer: bool = false, queue: Queue = .{ .data = std.fifo.LinearFifo(?*anyopaque, .Dynamic).init(bun.default_allocator), @@ -1590,7 +1605,7 @@ pub const ThreadSafeFunction = struct { /// See: https://github.com/nodejs/node/pull/38506 /// In that case, we need to drain microtasks. fn call(this: *ThreadSafeFunction, task: ?*anyopaque, is_first: bool) bun.JSTerminated!void { - const env = this.env; + const env = this.env.get(); if (!is_first) { try this.event_loop.drainMicrotasks(); } @@ -1664,13 +1679,19 @@ pub const ThreadSafeFunction = struct { pub fn deinit(this: *ThreadSafeFunction) void { this.unref(); - if (this.finalizer.fun) |fun| { - Finalizer.napi_internal_enqueue_finalizer(this.env, fun, this.finalizer.data, this.ctx); + if (this.finalizer_fun) |fun| { + var finalizer: Finalizer = .{ + .env = this.env, + .fun = fun, + .data = this.finalizer_data, + }; + finalizer.enqueue(); + } else { + this.env.deinit(); } this.callback.deinit(); this.queue.deinit(); - this.env.deref(); bun.destroy(this); } @@ -1748,7 +1769,7 @@ pub export fn napi_create_threadsafe_function( const vm = env.toJS().bunVM(); var function = ThreadSafeFunction.new(.{ .event_loop = vm.eventLoop(), - .env = env, + .env = .cloneFromRaw(env), .callback = if (call_js_cb) |c| .{ .c = .{ .napi_threadsafe_function_call_js = c, @@ -1762,13 +1783,13 @@ pub export fn napi_create_threadsafe_function( .thread_count = .{ .raw = @intCast(initial_thread_count) }, .poll_ref = Async.KeepAlive.init(), .tracker = jsc.Debugger.AsyncTaskTracker.init(vm), + .finalizer_fun = thread_finalize_cb, + .finalizer_data = thread_finalize_data, }); - function.finalizer = .{ .env = env, .data = thread_finalize_data, .fun = thread_finalize_cb }; // nodejs by default keeps the event loop alive until the thread-safe function is unref'd function.ref(); function.tracker.didSchedule(vm.global); - function.env.ref(); result.* = function; return env.ok(); @@ -2486,7 +2507,7 @@ pub const NapiFinalizerTask = struct { } pub fn schedule(this: *NapiFinalizerTask) void { - const globalThis = this.finalizer.env.?.toJS(); + const globalThis = this.finalizer.env.get().toJS(); const vm, const thread_kind = globalThis.tryBunVM(); @@ -2505,6 +2526,7 @@ pub const NapiFinalizerTask = struct { } pub fn deinit(this: *NapiFinalizerTask) void { + this.finalizer.deinit(); bun.default_allocator.destroy(this); } From 24d9d642de0e42190c03a89c0f4bbb0c63989cc7 Mon Sep 17 00:00:00 2001 From: avarayr <7735415+avarayr@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:04:23 -0400 Subject: [PATCH 228/391] ProxyTunnel: close-delimited responses via proxy cause ECONNRESET (#23719) fixes: oven-sh/bun#23717 ### What does this PR do? - Align ProxyTunnel.onClose with [HTTPClient.onClose](https://github.com/oven-sh/bun/blob/bun-v1.3.0/src/http.zig#L223-L241): when a tunneled HTTPS response is in-progress and either - parsing chunked trailers (trailer-line states), or - transfer-encoding is identity with content_length == null while in .body, treat EOF as end-of-message and complete the request, rather than ECONNRESET. - Schedule proxy deref instead of deref inside callbacks to avoid lifetime hazards. ### How did you verify your code works? - `test/js/bun/http/proxy.test.ts`: raw TLS origin returns close-delimited 200 OK; verified no ECONNRESET and body delivered. - Test suite passes under bun bd test. ## Risk/compat - Only affects CONNECT/TLS path. Direct HTTP/HTTPS unchanged. Behavior mirrors existing [HTTPClient.onClose](https://github.com/oven-sh/bun/blob/bun-v1.3.0/src/http.zig#L223-L241). ## Repro (minimal) See issue; core condition is no Content-Length and no Transfer-Encoding (close-delimited). Co-authored-by: Ciro Spaciari --- src/http/ProxyTunnel.zig | 38 ++++++++++++++++++++++++++++++++-- test/js/bun/http/proxy.test.ts | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/http/ProxyTunnel.zig b/src/http/ProxyTunnel.zig index 3dac05f961..20da4057e6 100644 --- a/src/http/ProxyTunnel.zig +++ b/src/http/ProxyTunnel.zig @@ -216,8 +216,32 @@ fn onClose(this: *HTTPClient) void { log("ProxyTunnel onClose {s}", .{if (this.proxy_tunnel == null) "tunnel is detached" else "tunnel exists"}); if (this.proxy_tunnel) |proxy| { proxy.ref(); - // defer the proxy deref the proxy tunnel may still be in use after triggering the close callback - defer bun.http.http_thread.scheduleProxyDeref(proxy); + + // If a response is in progress, mirror HTTPClient.onClose semantics: + // treat connection close as end-of-body for identity transfer when no content-length. + const in_progress = this.state.stage != .done and this.state.stage != .fail and this.state.flags.is_redirect_pending == false; + if (in_progress) { + if (this.state.isChunkedEncoding()) { + switch (this.state.chunked_decoder._state) { + .CHUNKED_IN_TRAILERS_LINE_HEAD, .CHUNKED_IN_TRAILERS_LINE_MIDDLE => { + this.state.flags.received_last_chunk = true; + progressUpdateForProxySocket(this, proxy); + // Drop our temporary ref asynchronously to avoid freeing within callback + bun.http.http_thread.scheduleProxyDeref(proxy); + return; + }, + else => {}, + } + } else if (this.state.content_length == null and this.state.response_stage == .body) { + this.state.flags.received_last_chunk = true; + progressUpdateForProxySocket(this, proxy); + // Balance the ref we took asynchronously + bun.http.http_thread.scheduleProxyDeref(proxy); + return; + } + } + + // Otherwise, treat as failure. const err = proxy.shutdown_err; switch (proxy.socket) { .ssl => |socket| { @@ -229,6 +253,16 @@ fn onClose(this: *HTTPClient) void { .none => {}, } proxy.detachSocket(); + // Deref after returning to the event loop to avoid lifetime hazards. + bun.http.http_thread.scheduleProxyDeref(proxy); + } +} + +fn progressUpdateForProxySocket(this: *HTTPClient, proxy: *ProxyTunnel) void { + switch (proxy.socket) { + .ssl => |socket| this.progressUpdate(true, &bun.http.http_thread.https_context, socket), + .tcp => |socket| this.progressUpdate(false, &bun.http.http_thread.http_context, socket), + .none => {}, } } diff --git a/test/js/bun/http/proxy.test.ts b/test/js/bun/http/proxy.test.ts index c36c0809ca..02f088a7f1 100644 --- a/test/js/bun/http/proxy.test.ts +++ b/test/js/bun/http/proxy.test.ts @@ -301,3 +301,39 @@ test("HTTPS over HTTP proxy preserves TLS record order with large bodies", async expect(result).toBe(String(size)); } }); + +test("HTTPS origin close-delimited body via HTTP proxy does not ECONNRESET", async () => { + // Inline raw HTTPS origin: 200 + no Content-Length then close + const originServer = tls.createServer( + { ...tlsCert, rejectUnauthorized: false }, + (clientSocket: net.Socket | tls.TLSSocket) => { + clientSocket.once("data", () => { + const body = "ok"; + // ! Notice we are not using a Content-Length header here, this is what is causing the issue + const resp = "HTTP/1.1 200 OK\r\n" + "content-type: text/plain\r\n" + "connection: close\r\n" + "\r\n" + body; + clientSocket.write(resp); + clientSocket.end(); + }); + clientSocket.on("error", () => {}); + }, + ); + originServer.listen(0); + await once(originServer, "listening"); + const originURL = `https://localhost:${(originServer.address() as net.AddressInfo).port}`; + try { + const res = await fetch(originURL, { + method: "POST", + body: "x", + proxy: httpProxyServer.url, + keepalive: false, + tls: { ca: tlsCert.cert, rejectUnauthorized: false }, + }); + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text).toBe("ok"); + } finally { + originServer.close(); + await once(originServer, "close"); + } +}); From fb75e077a2167fd0915f7511b0e29238ba7763fd Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Fri, 24 Oct 2025 05:14:36 +0900 Subject: [PATCH 229/391] Add missing empty JSValue checking for `Bun.cookieMap#delete` (#23951) ### What does this PR do? Adds missing null checking for `Bun.CookieMap#delete`. ### How did you verify your code works? Tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/bindings/webcore/JSCookieMap.cpp | 2 +- test/js/bun/cookie/cookie-map.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/bun.js/bindings/webcore/JSCookieMap.cpp b/src/bun.js/bindings/webcore/JSCookieMap.cpp index 0f1aacd447..ecebad0b37 100644 --- a/src/bun.js/bindings/webcore/JSCookieMap.cpp +++ b/src/bun.js/bindings/webcore/JSCookieMap.cpp @@ -480,7 +480,7 @@ static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_deleteBody(JSC::J } } - if (nameValue.isString()) { + if (nameValue && nameValue.isString()) { RETURN_IF_EXCEPTION(throwScope, {}); if (!nameValue.isUndefined() && !nameValue.isNull()) { diff --git a/test/js/bun/cookie/cookie-map.test.ts b/test/js/bun/cookie/cookie-map.test.ts index 1f61e68d28..65adcd49f5 100644 --- a/test/js/bun/cookie/cookie-map.test.ts +++ b/test/js/bun/cookie/cookie-map.test.ts @@ -331,3 +331,15 @@ describe("iterator", () => { `); }); }); + +describe("invalid delete usage", () => { + test("invalid usage does not crash", () => { + expect(() => { + const v1 = Bun.CookieMap; + // @ts-ignore + const v2 = new v1(v1, v1, Bun, v1); + // @ts-ignore + v2.delete(v2); + }).toThrow("Cookie name is required"); + }); +}); From 7bf67e78d7665f34f8680bd1ac1147f449fcce8d Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 23 Oct 2025 13:17:51 -0700 Subject: [PATCH 230/391] Fix incorrect/suspicious uses of `ZigString.Slice.cloneIfNeeded` (#23937) `ZigString.Slice.cloneIfNeeded` does *not* guarantee that the returned slice will have been allocated by the provided allocator, which makes it very easy to use this method incorrectly. (For internal tracking: fixes ENG-21284) --- src/bun.js/api/filesystem_router.zig | 22 ++++++++++++++++----- src/bun.js/api/glob.zig | 21 +++++++++++--------- src/bun.js/api/server/ServerConfig.zig | 8 ++------ src/bun.js/bindings/JSValue.zig | 10 ++++++++-- src/bun.js/bindings/ZigStackFrame.zig | 6 +++++- src/bun.js/bindings/ZigString.zig | 13 +++++++++---- src/bun.js/webcore/Blob.zig | 27 +++++++++++--------------- src/string.zig | 12 +++++++++++- 8 files changed, 75 insertions(+), 44 deletions(-) diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig index 2b1147239b..bd7e8b8118 100644 --- a/src/bun.js/api/filesystem_router.zig +++ b/src/bun.js/api/filesystem_router.zig @@ -87,7 +87,7 @@ pub const FileSystemRouter = struct { return globalThis.throwInvalidArguments("Expected fileExtensions to be an Array of strings", .{}); } if (try val.getLength(globalThis) == 0) continue; - extensions.appendAssumeCapacity(((try val.toSlice(globalThis, allocator)).cloneIfNeeded(allocator) catch unreachable).slice()[1..]); + extensions.appendAssumeCapacity((try val.toUTF8Bytes(globalThis, allocator))[1..]); } } @@ -99,7 +99,7 @@ pub const FileSystemRouter = struct { return globalThis.throwInvalidArguments("Expected assetPrefix to be a string", .{}); } - asset_prefix_slice = (try asset_prefix.toSlice(globalThis, allocator)).cloneIfNeeded(allocator) catch unreachable; + asset_prefix_slice = try (try asset_prefix.toSlice(globalThis, allocator)).cloneIfBorrowed(allocator); } const orig_log = vm.transpiler.resolver.log; var log = Log.Log.init(allocator); @@ -165,6 +165,10 @@ pub const FileSystemRouter = struct { router.config.dir = fs_router.base_dir.?.slice(); fs_router.base_dir.?.ref(); + // TODO: Memory leak? We haven't freed `asset_prefix_slice`, but we can't do so because the + // underlying string is borrowed in `fs_router.router.config.asset_prefix_path`. + // `FileSystemRouter.deinit` frees `fs_router.asset_prefix`, but that's a clone of + // `asset_prefix_slice`. The original is not freed. return fs_router; } @@ -271,7 +275,7 @@ pub const FileSystemRouter = struct { var path: ZigString.Slice = brk: { if (argument.isString()) { - break :brk (try argument.toSlice(globalThis, globalThis.allocator())).cloneIfNeeded(globalThis.allocator()) catch unreachable; + break :brk try (try argument.toSlice(globalThis, globalThis.allocator())).cloneIfBorrowed(globalThis.allocator()); } if (argument.isCell()) { @@ -289,13 +293,14 @@ pub const FileSystemRouter = struct { }; if (path.len == 0 or (path.len == 1 and path.ptr[0] == '/')) { + path.deinit(); path = ZigString.Slice.fromUTF8NeverFree("/"); } if (strings.hasPrefixComptime(path.slice(), "http://") or strings.hasPrefixComptime(path.slice(), "https://") or strings.hasPrefixComptime(path.slice(), "file://")) { const prev_path = path; - path = ZigString.init(URL.parse(path.slice()).pathname).toSliceFast(globalThis.allocator()).cloneIfNeeded(globalThis.allocator()) catch unreachable; - prev_path.deinit(); + defer prev_path.deinit(); + path = try .initDupe(globalThis.allocator(), URL.parse(path.slice()).pathname); } const url_path = URLPath.parse(path.slice()) catch |err| { @@ -319,6 +324,13 @@ pub const FileSystemRouter = struct { this.asset_prefix, this.base_dir.?, ) catch unreachable; + + // TODO: Memory leak? We haven't freed `path`, but we can't do so because the underlying + // string is borrowed in `result.route_holder.pathname` and `result.route_holder.query_string` + // (see `Routes.matchPageWithAllocator`, which does not clone these fields but rather + // directly reuses parts of the `URLPath`, which itself borrows from `path`). + // `MatchedRoute.deinit` doesn't free any fields of `route_holder`, so the string is not + // freed. return result.toJS(globalThis); } diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index c393448566..44e459c3ff 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -18,20 +18,24 @@ const ScanOpts = struct { error_on_broken_symlinks: bool, fn parseCWD(globalThis: *JSGlobalObject, allocator: std.mem.Allocator, cwdVal: jsc.JSValue, absolute: bool, comptime fnName: string) bun.JSError![]const u8 { - const cwd_str_raw = try cwdVal.toSlice(globalThis, allocator); - if (cwd_str_raw.len == 0) return ""; + const cwd_string: bun.String = try .fromJS(cwdVal, globalThis); + defer cwd_string.deref(); + if (cwd_string.isEmpty()) return ""; + + const cwd_str: []const u8 = cwd_str: { + const cwd_utf8 = cwd_string.toUTF8WithoutRef(allocator); - const cwd_str = cwd_str: { // If its absolute return as is - if (ResolvePath.Platform.auto.isAbsolute(cwd_str_raw.slice())) { - const cwd_str = try cwd_str_raw.cloneIfNeeded(allocator); - break :cwd_str cwd_str.ptr[0..cwd_str.len]; + if (ResolvePath.Platform.auto.isAbsolute(cwd_utf8.slice())) { + break :cwd_str (try cwd_utf8.cloneIfBorrowed(allocator)).slice(); } + defer cwd_utf8.deinit(); var path_buf2: [bun.MAX_PATH_BYTES * 2]u8 = undefined; if (!absolute) { - const cwd_str = ResolvePath.joinStringBuf(&path_buf2, &[_][]const u8{cwd_str_raw.slice()}, .auto); + const parts: []const []const u8 = &.{cwd_utf8.slice()}; + const cwd_str = ResolvePath.joinStringBuf(&path_buf2, parts, .auto); break :cwd_str try allocator.dupe(u8, cwd_str); } @@ -47,9 +51,8 @@ const ScanOpts = struct { const cwd_str = ResolvePath.joinStringBuf(&path_buf2, &[_][]const u8{ cwd, - cwd_str_raw.slice(), + cwd_utf8.slice(), }, .auto); - break :cwd_str try allocator.dupe(u8, cwd_str); }; diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 8a1caca83d..5ba1941675 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -803,13 +803,9 @@ pub fn fromJS( if (id.isUndefinedOrNull()) { args.allow_hot = false; } else { - const id_str = try id.toSlice( - global, - bun.default_allocator, - ); - + const id_str = try id.toUTF8Bytes(global, bun.default_allocator); if (id_str.len > 0) { - args.id = (id_str.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(); + args.id = id_str; } else { args.allow_hot = false; } diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 8dfff1c386..9a208b2d4b 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1187,7 +1187,6 @@ pub const JSValue = enum(i64) { pub fn toSlice(this: JSValue, global: *JSGlobalObject, allocator: std.mem.Allocator) JSError!ZigString.Slice { const str = try bun.String.fromJS(this, global); defer str.deref(); - return str.toUTF8(allocator); } @@ -1195,6 +1194,13 @@ pub const JSValue = enum(i64) { return getZigString(this, global).toSliceZ(allocator); } + /// The returned slice is always owned by `allocator`. + pub fn toUTF8Bytes(this: JSValue, global: *JSGlobalObject, allocator: std.mem.Allocator) JSError![]u8 { + const str: bun.String = try .fromJS(this, global); + defer str.deref(); + return str.toUTF8Bytes(allocator); + } + pub fn toJSString(this: JSValue, globalThis: *JSGlobalObject) bun.JSError!*JSString { return bun.cpp.JSC__JSValue__toStringOrNull(this, globalThis); } @@ -1242,7 +1248,7 @@ pub const JSValue = enum(i64) { allocator: std.mem.Allocator, ) ?ZigString.Slice { var str = this.toJSString(globalThis) catch return null; - return str.toSlice(globalThis, allocator).cloneIfNeeded(allocator) catch { + return str.toSliceClone(globalThis, allocator) catch { globalThis.throwOutOfMemory() catch {}; // TODO: properly propagate exception upwards return null; }; diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index 4082b86f25..5a5039f220 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -23,7 +23,11 @@ pub const ZigStackFrame = extern struct { var frame: api.StackFrame = comptime std.mem.zeroes(api.StackFrame); if (!this.function_name.isEmpty()) { var slicer = this.function_name.toUTF8(allocator); - frame.function_name = (try slicer.cloneIfNeeded(allocator)).slice(); + frame.function_name = (try slicer.cloneIfBorrowed(allocator)).slice(); + // TODO: Memory leak? `frame.function_name` may have just been allocated by this + // function, but it doesn't seem like we ever free it. Changing to `toUTF8Owned` would + // make the ownership clearer, but would also make the memory leak worse without an + // additional free. } if (!this.source_url.isEmpty()) { diff --git a/src/bun.js/bindings/ZigString.zig b/src/bun.js/bindings/ZigString.zig index 9a954969b6..2ea8bf825f 100644 --- a/src/bun.js/bindings/ZigString.zig +++ b/src/bun.js/bindings/ZigString.zig @@ -330,6 +330,10 @@ pub const ZigString = extern struct { }; } + pub fn initDupe(allocator: std.mem.Allocator, input: []const u8) OOM!Slice { + return .init(allocator, try allocator.dupe(u8, input)); + } + pub fn byteLength(this: *const Slice) usize { return this.len; } @@ -394,7 +398,7 @@ pub const ZigString = extern struct { } /// Note that the returned slice is not guaranteed to be allocated by `allocator`. - pub fn cloneIfNeeded(this: Slice, allocator: std.mem.Allocator) bun.OOM!Slice { + pub fn cloneIfBorrowed(this: Slice, allocator: std.mem.Allocator) bun.OOM!Slice { if (this.isAllocated()) { return this; } @@ -642,7 +646,7 @@ pub const ZigString = extern struct { if (this.len == 0) return Slice.empty; if (is16Bit(&this)) { - const buffer = this.toOwnedSlice(allocator) catch unreachable; + const buffer = bun.handleOom(this.toOwnedSlice(allocator)); return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buffer.ptr, @@ -662,7 +666,7 @@ pub const ZigString = extern struct { if (this.len == 0) return Slice.empty; if (is16Bit(&this)) { - const buffer = this.toOwnedSlice(allocator) catch unreachable; + const buffer = bun.handleOom(this.toOwnedSlice(allocator)); return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buffer.ptr, @@ -671,7 +675,7 @@ pub const ZigString = extern struct { } if (!this.isUTF8() and !strings.isAllASCII(untagged(this._unsafe_ptr_do_not_use)[0..this.len])) { - const buffer = this.toOwnedSlice(allocator) catch unreachable; + const buffer = bun.handleOom(this.toOwnedSlice(allocator)); return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buffer.ptr, @@ -685,6 +689,7 @@ pub const ZigString = extern struct { }; } + /// The returned slice is always allocated by `allocator`. pub fn toSliceClone(this: ZigString, allocator: std.mem.Allocator) OOM!Slice { if (this.len == 0) return Slice.empty; diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 68c85a3138..173d9df9fb 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -542,8 +542,7 @@ const URLSearchParamsConverter = struct { buf: []u8 = "", globalThis: *jsc.JSGlobalObject, pub fn convert(this: *URLSearchParamsConverter, str: ZigString) void { - var out = bun.handleOom(str.toSlice(this.allocator).cloneIfNeeded(this.allocator)); - this.buf = @constCast(out.slice()); + this.buf = bun.handleOom(str.toOwnedSlice(this.allocator)); } }; @@ -628,8 +627,8 @@ export fn Blob__setAsFile(this: *Blob, path_str: *bun.String) void { if (this.store) |store| { if (store.data == .bytes) { if (store.data.bytes.stored_name.len == 0) { - var utf8 = path_str.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch unreachable; - store.data.bytes.stored_name = bun.PathString.init(utf8.slice()); + const utf8 = path_str.toUTF8Bytes(bun.default_allocator); + store.data.bytes.stored_name = bun.PathString.init(utf8); } } } @@ -1738,7 +1737,7 @@ pub fn JSDOMFile__construct_(globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca switch (store_.data) { .bytes => |*bytes| { bytes.stored_name = bun.PathString.init( - bun.handleOom(name_value_str.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator)).slice(), + name_value_str.toUTF8Bytes(bun.default_allocator), ); }, .s3, .file => { @@ -1750,9 +1749,7 @@ pub fn JSDOMFile__construct_(globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca blob.store = Blob.Store.new(.{ .data = .{ .bytes = Blob.Store.Bytes.initEmptyWithName( - bun.PathString.init( - bun.handleOom(name_value_str.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator)).slice(), - ), + bun.PathString.init(name_value_str.toUTF8Bytes(bun.default_allocator)), allocator, ), }, @@ -2483,11 +2480,10 @@ pub fn pipeReadableStreamToBlob(this: *Blob, globalThis: *jsc.JSGlobalObject, re break :brk .{ .fd = store.data.file.pathlike.fd }; } else { break :brk .{ - .path = ZigString.Slice.fromUTF8NeverFree( - store.data.file.pathlike.path.slice(), - ).cloneIfNeeded( + .path = bun.handleOom(ZigString.Slice.initDupe( bun.default_allocator, - ) catch |err| bun.handleOom(err), + store.data.file.pathlike.path.slice(), + )), }; } }; @@ -2723,11 +2719,10 @@ pub fn getWriter( break :brk .{ .fd = store.data.file.pathlike.fd }; } else { break :brk .{ - .path = ZigString.Slice.fromUTF8NeverFree( - store.data.file.pathlike.path.slice(), - ).cloneIfNeeded( + .path = bun.handleOom(ZigString.Slice.initDupe( bun.default_allocator, - ) catch |err| bun.handleOom(err), + store.data.file.pathlike.path.slice(), + )), }; } }; diff --git a/src/string.zig b/src/string.zig index 6572b8e5cf..4c294314c7 100644 --- a/src/string.zig +++ b/src/string.zig @@ -107,7 +107,7 @@ pub const String = extern struct { else .unknown; // string was 16-bit; may or may not be all ascii - const owned_slice = try utf8_slice.cloneIfNeeded(allocator); + const owned_slice = try utf8_slice.cloneIfBorrowed(allocator); // `owned_slice.allocator` is guaranteed to be `allocator`. break :blk .{ owned_slice.mut(), ascii_status }; }, @@ -768,6 +768,16 @@ pub const String = extern struct { return ZigString.Slice.empty; } + /// Equivalent to calling `toUTF8WithoutRef` followed by `cloneIfBorrowed`. + pub fn toUTF8Owned(this: String, allocator: std.mem.Allocator) ZigString.Slice { + return bun.handleOom(this.toUTF8WithoutRef(allocator).cloneIfBorrowed(allocator)); + } + + /// The returned slice is always allocated by `allocator`. + pub fn toUTF8Bytes(this: String, allocator: std.mem.Allocator) []u8 { + return this.toUTF8Owned(allocator).mut(); + } + /// use `byteSlice` to get a `[]const u8`. pub fn toSlice(this: *String, allocator: std.mem.Allocator) SliceWithUnderlyingString { defer this.* = .empty; From 5a82e858763d46466d697b040ccc7ce043ebc2b7 Mon Sep 17 00:00:00 2001 From: Logan Brown Date: Thu, 23 Oct 2025 16:30:49 -0400 Subject: [PATCH 231/391] Fix integer overflow when reading MySQL OK packets (#23993) ### Description This PR fixes a crash caused by integer underflow in `OKPacket.decodeInternal`. Previously, when `read_size` exceeded `packet_size`, the subtraction `packet_size - read_size` wrapped around, producing a huge `count` value passed into `reader.read()`. This led to an integer overflow panic at runtime. ### What does this PR do - Added a safe subtraction guard in `decodeInternal` to clamp `remaining` to `0` when `read_size >= packet_size`. - Ensures empty or truncated OK packets no longer cause crashes. - Behavior for valid packets remains unchanged. ### Impact Prevents integer overflow panics in MySQL OK packet parsing, improving stability when handling short or empty responses (e.g., queries that return no rows or minimal metadata). ### How did you verify your code works? Tested with proof of concept: https://github.com/Lillious/Bun-MySql-Integer-Overflow-PoC --------- Co-authored-by: Ciro Spaciari --- src/sql/mysql/protocol/OKPacket.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sql/mysql/protocol/OKPacket.zig b/src/sql/mysql/protocol/OKPacket.zig index d9483d6b8b..876d6f070d 100644 --- a/src/sql/mysql/protocol/OKPacket.zig +++ b/src/sql/mysql/protocol/OKPacket.zig @@ -33,9 +33,9 @@ pub fn decodeInternal(this: *OKPacket, comptime Context: type, reader: NewReader this.warnings = try reader.int(u16); // Info (EOF-terminated string) - if (reader.peek().len > 0) { - // everything else is info - this.info = try reader.read(@truncate(this.packet_size - read_size)); + if (reader.peek().len > 0 and this.packet_size > read_size) { + const remaining = this.packet_size - read_size; + this.info = try reader.read(@truncate(remaining)); } } From 29028bbabefcedc1357ec2c05439567b643b42ca Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:59:35 -0700 Subject: [PATCH 232/391] docs(watch): rename filename to relativePath in recursive example (#23990) When using `fs.watch()` with `recursive: true`, the callback receives a relative path from the watched directory (e.g., `'subdir/file.txt'`), not just a filename. Renaming the parameter from `filename` to `relativePath` makes this behavior immediately clear to developers. **Before:** ```ts (event, filename) => { console.log(`Detected ${event} in ${filename}`); } ``` **After:** ```ts (event, relativePath) => { console.log(`Detected ${event} in ${relativePath}`); } ``` This is a documentation-only change that improves clarity without altering any functionality. Co-authored-by: Braden Wong --- docs/guides/read-file/watch.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/read-file/watch.md b/docs/guides/read-file/watch.md index b97c08d0e9..c1a792903f 100644 --- a/docs/guides/read-file/watch.md +++ b/docs/guides/read-file/watch.md @@ -24,8 +24,8 @@ import { watch } from "fs"; const watcher = watch( import.meta.dir, { recursive: true }, - (event, filename) => { - console.log(`Detected ${event} in ${filename}`); + (event, relativePath) => { + console.log(`Detected ${event} in ${relativePath}`); }, ); ``` From 787a46d110cc339072b14aa341b993fe687a43a4 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Oct 2025 17:52:13 -0700 Subject: [PATCH 233/391] Write more data faster (#23989) ### What does this PR do? ### How did you verify your code works? --- src/http.zig | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/http.zig b/src/http.zig index 833dacd709..3c74237589 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1008,11 +1008,19 @@ pub fn flushStream(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPCont /// Write data to the socket (Just a error wrapper to easly handle amount written and error handling) fn writeToSocket(comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket, data: []const u8) !usize { - const amount = socket.write(data); - if (amount < 0) { - return error.WriteFailed; + var remaining = data; + var total_written: usize = 0; + while (remaining.len > 0) { + const amount = socket.write(remaining); + if (amount < 0) { + return error.WriteFailed; + } + const wrote: usize = @intCast(amount); + total_written += wrote; + remaining = remaining[wrote..]; + if (wrote == 0) break; } - return @intCast(amount); + return total_written; } /// Write data to the socket and buffer the unwritten data if there is backpressure From d648547942f8cfe5386fc04165c57ac0a91181e2 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Fri, 24 Oct 2025 14:16:01 +0900 Subject: [PATCH 234/391] Fix segv when `process.nextTick` is overwritten (#23971) ### What does this PR do? When `process.nextTick` is overwritten, segv will be occured via internal `processTick` call. This patch fixes it. ### How did you verify your code works? Tests. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/bindings/BunProcess.cpp | 13 +++++++++-- test/js/web/websocket/websocket.test.js | 29 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 11ccf98275..5931a39568 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -3430,14 +3430,23 @@ void Process::queueNextTick(JSC::JSGlobalObject* globalObject, const ArgList& ar { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue nextTick; if (!this->m_nextTickFunction) { - this->get(globalObject, Identifier::fromString(vm, "nextTick"_s)); + nextTick = this->get(globalObject, Identifier::fromString(vm, "nextTick"_s)); RETURN_IF_EXCEPTION(scope, void()); } ASSERT(!args.isEmpty()); JSObject* nextTickFn = this->m_nextTickFunction.get(); - ASSERT(nextTickFn); + if (!nextTickFn) [[unlikely]] { + if (nextTick && nextTick.isObject()) + nextTickFn = asObject(nextTick); + else { + throwVMError(globalObject, scope, "Failed to call nextTick"_s); + return; + } + } ASSERT_WITH_MESSAGE(!args.at(0).inherits(), "queueNextTick must not pass an AsyncContextFrame. This will cause a crash."); JSC::call(globalObject, nextTickFn, args, "Failed to call nextTick"_s); RELEASE_AND_RETURN(scope, void()); diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index 1c7a288120..e40a2d17ac 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import crypto from "crypto"; import { readFileSync } from "fs"; -import { bunEnv, bunExe, gc, tls } from "harness"; +import { bunEnv, bunExe, gc, tempDir, tls } from "harness"; import { createServer } from "net"; import { join } from "path"; import process from "process"; @@ -731,6 +731,33 @@ describe.concurrent("websocket in subprocess", () => { expect(messageReceived).toBe(true); }); + it.concurrent("should work with process.nextTick override", async () => { + using dir = tempDir("websocket-nexttick", { + "test.js": `{ + process.nextTick = function (arg) { + console.log(arg) + } + using server = Bun.serve({ + port: 0, + fetch() { return new Response(); }, + websocket: { message() {} }, + }); + const ws = new WebSocket(\`ws://\${server.hostname}:\${server.port}\`, {}); + ws.addEventListener("open", null); +}`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const exitCode = await proc.exited; + expect(exitCode).toBe(0); + }); + it("should exit after killed", async () => { await using subprocess = Bun.spawn({ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", TEST_WEBSOCKET_HOST], From e76570f452fb2ba3bb7d0335deddfc6251b60507 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 23 Oct 2025 23:08:08 -0700 Subject: [PATCH 235/391] feat(ENG-21362): Environment Variables Store (#23930) --- src/StandaloneModuleGraph.zig | 4 +- src/analytics.zig | 8 +- src/bake.zig | 2 +- src/bake/DevServer.zig | 10 +- src/bun.js/ModuleLoader.zig | 2 +- src/bun.js/RuntimeTranspilerCache.zig | 10 +- src/bun.js/VirtualMachine.zig | 16 +- src/bun.js/api/bun/dns.zig | 31 +- src/bun.js/api/bun/subprocess.zig | 2 +- src/bun.js/api/ffi.zig | 4 +- .../GarbageCollectionController.zig | 2 +- src/bun.js/node/node_os.zig | 8 +- src/bun.js/node/node_process.zig | 4 +- src/bun.js/test/debug.zig | 10 +- src/bun.js/webcore/blob/copy_file.zig | 2 +- src/bun.zig | 51 +- src/bundler/ThreadPool.zig | 4 +- .../linker_context/StaticRouteVisitor.zig | 2 +- .../findImportedFilesInCSSOrder.zig | 2 +- src/ci_info.zig | 6 +- src/cli.zig | 14 +- src/cli/Arguments.zig | 17 +- src/cli/bunx_command.zig | 4 +- src/cli/create_command.zig | 4 +- src/cli/init_command.zig | 12 +- src/cli/install_completions_command.zig | 41 +- src/cli/package_manager_command.zig | 4 +- src/cli/pm_version_command.zig | 8 +- src/cli/run_command.zig | 15 +- src/cli/test_command.zig | 14 +- src/cli/upgrade_command.zig | 6 +- src/compile_target.zig | 2 +- src/copy_file.zig | 4 +- src/crash_handler.zig | 27 +- src/env_loader.zig | 2 - src/env_var.zig | 656 ++++++++++++++++++ src/feature_flags.zig | 68 +- src/fs.zig | 12 +- .../websocket_client/WebSocketDeflate.zig | 2 +- src/install/NetworkTask.zig | 2 +- src/install/PackageManager.zig | 5 +- .../PackageManagerDirectories.zig | 4 +- .../PackageManagerLifecycle.zig | 2 +- .../PackageManager/PackageManagerOptions.zig | 30 +- src/install/PackageManager/patchPackage.zig | 2 +- .../updatePackageJSONAndInstall.zig | 4 +- src/install/extract_tarball.zig | 2 +- src/install/lockfile.zig | 2 +- src/install/repository.zig | 15 +- src/interchange/yaml.zig | 2 +- src/linux.zig | 2 +- src/macho.zig | 4 +- src/napi/napi.zig | 2 +- src/output.zig | 72 +- src/patch.zig | 4 +- src/perf.zig | 4 +- src/shell/Builtin.zig | 2 +- src/sql/mysql/MySQLRequestQueue.zig | 2 +- src/sql/postgres/DebugSocketMonitorReader.zig | 2 +- src/sql/postgres/DebugSocketMonitorWriter.zig | 2 +- src/sql/postgres/PostgresSQLConnection.zig | 2 +- src/tracy.zig | 2 +- src/transpiler.zig | 2 +- src/valkey/js_valkey.zig | 2 +- src/watcher/INotifyWatcher.zig | 4 +- src/watcher/WatcherTrace.zig | 2 +- test/internal/ban-limits.json | 2 +- 67 files changed, 886 insertions(+), 388 deletions(-) create mode 100644 src/env_var.zig diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 72b7b38cff..49c659e0dd 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -432,7 +432,7 @@ pub const StandaloneModuleGraph = struct { }; if (comptime bun.Environment.is_canary or bun.Environment.isDebug) { - if (bun.getenvZ("BUN_FEATURE_FLAG_DUMP_CODE")) |dump_code_dir| { + if (bun.env_var.BUN_FEATURE_FLAG_DUMP_CODE.get()) |dump_code_dir| { const buf = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); const dest_z = bun.path.joinAbsStringBufZ(dump_code_dir, buf, &.{dest_path}, .auto); @@ -1328,7 +1328,7 @@ pub const StandaloneModuleGraph = struct { var whichbuf: bun.PathBuffer = undefined; if (bun.which( &whichbuf, - bun.getenvZ("PATH") orelse return error.FileNotFound, + bun.env_var.PATH.get() orelse return error.FileNotFound, "", bun.argv[0], )) |path| { diff --git a/src/analytics.zig b/src/analytics.zig index a536511be4..0ac7156967 100644 --- a/src/analytics.zig +++ b/src/analytics.zig @@ -12,12 +12,10 @@ pub fn isEnabled() bool { .no => false, .unknown => { enabled = detect: { - if (bun.getenvZ("DO_NOT_TRACK")) |x| { - if (x.len == 1 and x[0] == '1') { - break :detect .no; - } + if (bun.env_var.DO_NOT_TRACK.get()) { + break :detect .no; } - if (bun.getenvZ("HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET") != null) { + if (bun.env_var.HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET.get() != null) { break :detect .no; } break :detect .yes; diff --git a/src/bake.zig b/src/bake.zig index d240ece752..49b045ef60 100644 --- a/src/bake.zig +++ b/src/bake.zig @@ -984,7 +984,7 @@ pub const PatternBuffer = struct { pub fn printWarning() void { // Silence this for the test suite - if (bun.getenvZ("BUN_DEV_SERVER_TEST_RUNNER") == null) { + if (bun.env_var.BUN_DEV_SERVER_TEST_RUNNER.get() == null) { bun.Output.warn( \\Be advised that Bun Bake is highly experimental, and its API \\will have breaking changes. Join the #bake Discord diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index e14cc0564c..e85e874890 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -318,7 +318,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { .memory_visualizer_timer = .initPaused(.DevServerMemoryVisualizerTick), .has_pre_crash_handler = bun.FeatureFlags.bake_debugging_features and options.dump_state_on_crash orelse - bun.getRuntimeFeatureFlag(.BUN_DUMP_STATE_ON_CRASH), + bun.feature_flag.BUN_DUMP_STATE_ON_CRASH.get(), .frontend_only = options.framework.file_system_router_types.len == 0, .client_graph = .empty, .server_graph = .empty, @@ -343,13 +343,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { .source_maps = .empty, .plugin_state = .unknown, .bundling_failures = .{}, - .assume_perfect_incremental_bundling = if (bun.Environment.isDebug) - if (bun.getenvZ("BUN_ASSUME_PERFECT_INCREMENTAL")) |env| - !bun.strings.eqlComptime(env, "0") - else - true - else - bun.getRuntimeFeatureFlag(.BUN_ASSUME_PERFECT_INCREMENTAL), + .assume_perfect_incremental_bundling = bun.feature_flag.BUN_ASSUME_PERFECT_INCREMENTAL.get() orelse bun.Environment.isDebug, .testing_batch_events = .disabled, .broadcast_console_log_from_browser_to_server = options.broadcast_console_log_from_browser_to_server, .server_transpiler = undefined, diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index 90e21151ca..d1ce74545a 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -2037,7 +2037,7 @@ fn dumpSourceString(vm: *VirtualMachine, specifier: string, written: []const u8) fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written: []const u8) !void { if (!Environment.isDebug) return; - if (bun.getRuntimeFeatureFlag(.BUN_DEBUG_NO_DUMP)) return; + if (bun.feature_flag.BUN_DEBUG_NO_DUMP.get()) return; const BunDebugHolder = struct { pub var dir: ?std.fs.Dir = null; diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index d6d3c8842b..50b05809b3 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -383,10 +383,10 @@ pub const RuntimeTranspilerCache = struct { fn reallyGetCacheDir(buf: *bun.PathBuffer) [:0]const u8 { if (comptime bun.Environment.isDebug) { - bun_debug_restore_from_cache = bun.getenvZ("BUN_DEBUG_ENABLE_RESTORE_FROM_TRANSPILER_CACHE") != null; + bun_debug_restore_from_cache = bun.env_var.BUN_DEBUG_ENABLE_RESTORE_FROM_TRANSPILER_CACHE.get(); } - if (bun.getenvZ("BUN_RUNTIME_TRANSPILER_CACHE_PATH")) |dir| { + if (bun.env_var.BUN_RUNTIME_TRANSPILER_CACHE_PATH.get()) |dir| { if (dir.len == 0 or (dir.len == 1 and dir[0] == '0')) { return ""; } @@ -397,7 +397,7 @@ pub const RuntimeTranspilerCache = struct { return buf[0..len :0]; } - if (bun.getenvZ("XDG_CACHE_HOME")) |dir| { + if (bun.env_var.XDG_CACHE_HOME.get()) |dir| { const parts = &[_][]const u8{ dir, "bun", "@t@" }; return bun.fs.FileSystem.instance.absBufZ(parts, buf); } @@ -405,7 +405,7 @@ pub const RuntimeTranspilerCache = struct { if (comptime bun.Environment.isMac) { // On a mac, default to ~/Library/Caches/bun/* // This is different than ~/.bun/install/cache, and not configurable by the user. - if (bun.getenvZ("HOME")) |home| { + if (bun.env_var.HOME.get()) |home| { const parts = &[_][]const u8{ home, "Library/", @@ -417,7 +417,7 @@ pub const RuntimeTranspilerCache = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |dir| { + if (bun.env_var.HOME.get()) |dir| { const parts = &[_][]const u8{ dir, ".bun", "install", "cache", "@t@" }; return bun.fs.FileSystem.instance.absBufZ(parts, buf); } diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index ea0095dfea..61f3fa5ae6 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -220,7 +220,7 @@ pub fn initRequestBodyValue(this: *VirtualMachine, body: jsc.WebCore.Body.Value) /// Worker VMs are always destroyed on exit, regardless of this setting. Setting this to /// true may expose bugs that would otherwise only occur using Workers. Controlled by pub fn shouldDestructMainThreadOnExit(_: *const VirtualMachine) bool { - return bun.getRuntimeFeatureFlag(.BUN_DESTRUCT_VM_ON_EXIT); + return bun.feature_flag.BUN_DESTRUCT_VM_ON_EXIT.get(); } pub threadlocal var is_bundler_thread_for_bytecode_cache: bool = false; @@ -464,7 +464,7 @@ pub fn loadExtraEnvAndSourceCodePrinter(this: *VirtualMachine) void { this.hide_bun_stackframes = false; } - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER.get()) { this.transpiler_store.enabled = false; } @@ -1199,12 +1199,12 @@ pub inline fn assertOnJSThread(vm: *const VirtualMachine) void { } fn configureDebugger(this: *VirtualMachine, cli_flag: bun.cli.Command.Debugger) void { - if (bun.getenvZ("HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET") != null) { + if (bun.env_var.HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET.get() != null) { return; } - const unix = bun.getenvZ("BUN_INSPECT") orelse ""; - const connect_to = bun.getenvZ("BUN_INSPECT_CONNECT_TO") orelse ""; + const unix = bun.env_var.BUN_INSPECT.get(); + const connect_to = bun.env_var.BUN_INSPECT_CONNECT_TO.get(); const set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1"); // If we should set a breakpoint on the first line const wait_for_debugger = unix.len > 0 and strings.endsWith(unix, "?wait=1"); // If we should wait for the debugger to connect before starting the event loop @@ -2648,8 +2648,8 @@ pub fn remapZigException( ) void { error_instance.toZigException(this.global, exception); const enable_source_code_preview = allow_source_code_preview and - !(bun.getRuntimeFeatureFlag(.BUN_DISABLE_SOURCE_CODE_PREVIEW) or - bun.getRuntimeFeatureFlag(.BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW)); + !(bun.feature_flag.BUN_DISABLE_SOURCE_CODE_PREVIEW.get() or + bun.feature_flag.BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW.get()); defer { if (Environment.isDebug) { @@ -3348,7 +3348,7 @@ pub noinline fn printGithubAnnotation(exception: *ZigException) void { const message = exception.message; const frames = exception.stack.frames(); const top_frame = if (frames.len > 0) frames[0] else null; - const dir = bun.getenvZ("GITHUB_WORKSPACE") orelse bun.fs.FileSystem.instance.top_level_dir; + const dir = bun.env_var.GITHUB_WORKSPACE.get() orelse bun.fs.FileSystem.instance.top_level_dir; const allocator = bun.default_allocator; Output.flush(); diff --git a/src/bun.js/api/bun/dns.zig b/src/bun.js/api/bun/dns.zig index 50256ff78f..39dc1bb30e 100644 --- a/src/bun.js/api/bun/dns.zig +++ b/src/bun.js/api/bun/dns.zig @@ -1147,26 +1147,11 @@ pub const internal = struct { var __max_dns_time_to_live_seconds: ?u32 = null; pub fn getMaxDNSTimeToLiveSeconds() u32 { - // Amazon Web Services recommends 5 seconds: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/jvm-ttl-dns.html - const default_max_dns_time_to_live_seconds = 30; - // This is racy, but it's okay because the number won't be invalid, just stale. return __max_dns_time_to_live_seconds orelse { - if (bun.getenvZ("BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS")) |string_value| { - const value = std.fmt.parseInt(i64, string_value, 10) catch { - __max_dns_time_to_live_seconds = default_max_dns_time_to_live_seconds; - return default_max_dns_time_to_live_seconds; - }; - if (value < 0) { - __max_dns_time_to_live_seconds = std.math.maxInt(u32); - } else { - __max_dns_time_to_live_seconds = @truncate(@as(u64, @intCast(value))); - } - return __max_dns_time_to_live_seconds.?; - } - - __max_dns_time_to_live_seconds = default_max_dns_time_to_live_seconds; - return default_max_dns_time_to_live_seconds; + const value = bun.env_var.BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS.get(); + __max_dns_time_to_live_seconds = @truncate(@as(u64, @intCast(value))); + return __max_dns_time_to_live_seconds.?; }; } @@ -1393,12 +1378,12 @@ pub const internal = struct { }; pub fn getHints() std.c.addrinfo { var hints_copy = default_hints; - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG.get()) { hints_copy.flags.ADDRCONFIG = false; } - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_IPV6)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_IPV6.get()) { hints_copy.family = std.c.AF.INET; - } else if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_IPV4)) { + } else if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_IPV4.get()) { hints_copy.family = std.c.AF.INET6; } @@ -1685,7 +1670,7 @@ pub const internal = struct { getaddrinfo_calls += 1; var timestamp_to_store: u32 = 0; // is there a cache hit? - if (!bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_DNS_CACHE)) { + if (!bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_DNS_CACHE.get()) { if (global_cache.get(key, ×tamp_to_store)) |entry| { if (preload) { global_cache.lock.unlock(); @@ -1724,7 +1709,7 @@ pub const internal = struct { global_cache.lock.unlock(); if (comptime Environment.isMac) { - if (!bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO)) { + if (!bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO.get()) { const res = lookupLibinfo(req, loop.internal_loop_data.getParent()); log("getaddrinfo({s}) = cache miss (libinfo)", .{host orelse ""}); if (res) return req; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index c07203e565..c0f0024e06 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1351,7 +1351,7 @@ pub fn spawnMaybeSync( !jsc_vm.auto_killer.enabled and !jsc_vm.jsc_vm.hasExecutionTimeLimit() and !jsc_vm.isInspectorEnabled() and - !bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH); + !bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH.get(); const spawn_options = bun.spawn.SpawnOptions{ .cwd = cwd, diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index 8325117440..ff84db1694 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -321,7 +321,7 @@ pub const FFI = struct { pub fn compile(this: *CompileC, globalThis: *JSGlobalObject) !struct { *TCC.State, []u8 } { const compile_options: [:0]const u8 = if (this.flags.len > 0) this.flags - else if (bun.getenvZ("BUN_TCC_OPTIONS")) |tcc_options| + else if (bun.env_var.BUN_TCC_OPTIONS.get()) |tcc_options| @ptrCast(tcc_options) else default_tcc_options; @@ -349,7 +349,7 @@ pub const FFI = struct { if (Environment.isMac) { add_system_include_dir: { const dirs_to_try = [_][]const u8{ - bun.getenvZ("SDKROOT") orelse "", + bun.env_var.SDKROOT.get() orelse "", getSystemIncludeDir() orelse "", }; diff --git a/src/bun.js/event_loop/GarbageCollectionController.zig b/src/bun.js/event_loop/GarbageCollectionController.zig index 7b2088f93b..2a13be5a9b 100644 --- a/src/bun.js/event_loop/GarbageCollectionController.zig +++ b/src/bun.js/event_loop/GarbageCollectionController.zig @@ -37,7 +37,7 @@ pub fn init(this: *GarbageCollectionController, vm: *VirtualMachine) void { actual.internal_loop_data.jsc_vm = vm.jsc_vm; if (comptime Environment.isDebug) { - if (bun.getenvZ("BUN_TRACK_LAST_FN_NAME") != null) { + if (bun.env_var.BUN_TRACK_LAST_FN_NAME.get()) { vm.eventLoop().debug.track_last_fn_name = true; } } diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 186e60377e..f9426e7ecc 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -314,7 +314,7 @@ pub fn homedir(global: *jsc.JSGlobalObject) !bun.String { // The posix implementation of uv_os_homedir first checks the HOME // environment variable, then falls back to reading the passwd entry. - if (bun.getenvZ("HOME")) |home| { + if (bun.env_var.HOME.get()) |home| { if (home.len > 0) return bun.String.init(home); } @@ -938,15 +938,15 @@ pub fn userInfo(globalThis: *jsc.JSGlobalObject, options: gen.UserInfoOptions) b result.put(globalThis, jsc.ZigString.static("homedir"), home.toJS(globalThis)); if (comptime Environment.isWindows) { - result.put(globalThis, jsc.ZigString.static("username"), jsc.ZigString.init(bun.getenvZ("USERNAME") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, jsc.ZigString.static("username"), jsc.ZigString.init(bun.env_var.USER.get() orelse "unknown").withEncoding().toJS(globalThis)); result.put(globalThis, jsc.ZigString.static("uid"), jsc.JSValue.jsNumber(-1)); result.put(globalThis, jsc.ZigString.static("gid"), jsc.JSValue.jsNumber(-1)); result.put(globalThis, jsc.ZigString.static("shell"), jsc.JSValue.jsNull()); } else { - const username = bun.getenvZ("USER") orelse "unknown"; + const username = bun.env_var.USER.get() orelse "unknown"; result.put(globalThis, jsc.ZigString.static("username"), jsc.ZigString.init(username).withEncoding().toJS(globalThis)); - result.put(globalThis, jsc.ZigString.static("shell"), jsc.ZigString.init(bun.getenvZ("SHELL") orelse "unknown").withEncoding().toJS(globalThis)); + result.put(globalThis, jsc.ZigString.static("shell"), jsc.ZigString.init(bun.env_var.SHELL.get() orelse "unknown").withEncoding().toJS(globalThis)); result.put(globalThis, jsc.ZigString.static("uid"), jsc.JSValue.jsNumber(c.getuid())); result.put(globalThis, jsc.ZigString.static("gid"), jsc.JSValue.jsNumber(c.getgid())); } diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index d8172ec238..91362e45f0 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -339,11 +339,11 @@ comptime { } pub export fn Bun__NODE_NO_WARNINGS() bool { - return bun.getRuntimeFeatureFlag(.NODE_NO_WARNINGS); + return bun.feature_flag.NODE_NO_WARNINGS.get(); } pub export fn Bun__suppressCrashOnProcessKillSelfIfDesired() void { - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF.get()) { bun.crash_handler.suppressReporting(); } } diff --git a/src/bun.js/test/debug.zig b/src/bun.js/test/debug.zig index 3b56f5fda9..7933ab53f9 100644 --- a/src/bun.js/test/debug.zig +++ b/src/bun.js/test/debug.zig @@ -52,16 +52,8 @@ pub const group = struct { } var indent: usize = 0; var last_was_start = false; - var wants_quiet: ?bool = null; fn getLogEnabledRuntime() bool { - if (wants_quiet) |v| return !v; - if (bun.getenvZ("WANTS_LOUD")) |val| { - const loud = !std.mem.eql(u8, val, "0"); - wants_quiet = !loud; - return loud; - } - wants_quiet = true; // default quiet - return false; + return bun.env_var.WANTS_LOUD.get(); } inline fn getLogEnabledStaticFalse() bool { return false; diff --git a/src/bun.js/webcore/blob/copy_file.zig b/src/bun.js/webcore/blob/copy_file.zig index 8b045280b9..7868e277cd 100644 --- a/src/bun.js/webcore/blob/copy_file.zig +++ b/src/bun.js/webcore/blob/copy_file.zig @@ -881,7 +881,7 @@ pub const CopyFileWindows = struct { fn copyfile(this: *CopyFileWindows) void { // This is for making it easier for us to test this code path - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE.get()) { this.prepareReadWriteLoop(); return; } diff --git a/src/bun.zig b/src/bun.zig index e7c09f62dd..f1f13acbef 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -7,6 +7,8 @@ const bun = @This(); pub const Environment = @import("./env.zig"); +pub const env_var = @import("./env_var.zig"); +pub const feature_flag = env_var.feature_flag; pub const use_mimalloc = @import("build_options").use_mimalloc; pub const default_allocator: std.mem.Allocator = allocators.c_allocator; @@ -494,11 +496,10 @@ pub fn fastRandom() u64 { // and we only need to do it once per process var value = seed_value.load(.monotonic); while (value == 0) : (value = seed_value.load(.monotonic)) { - if (comptime Environment.isDebug or Environment.is_canary) outer: { - if (getenvZ("BUN_DEBUG_HASH_RANDOM_SEED")) |env| { - value = std.fmt.parseInt(u64, env, 10) catch break :outer; - seed_value.store(value, .monotonic); - return value; + if (comptime Environment.isDebug or Environment.is_canary) { + if (bun.env_var.BUN_DEBUG_HASH_RANDOM_SEED.get()) |v| { + seed_value.store(v, .monotonic); + return v; } } csprng(std.mem.asBytes(&value)); @@ -820,31 +821,11 @@ pub fn openDirAbsoluteNotForDeletingOrRenaming(path_: []const u8) !std.fs.Dir { return fd.stdDir(); } -pub fn getRuntimeFeatureFlag(comptime flag: FeatureFlags.RuntimeFeatureFlag) bool { - return struct { - const state = enum(u8) { idk, disabled, enabled }; - var is_enabled: std.atomic.Value(state) = std.atomic.Value(state).init(.idk); - pub fn get() bool { - // .monotonic is okay because there are no side effects we need to observe from a thread that has - // written to this variable. This variable is simply a cache, and if its value is not ready yet, we - // compute it below. There are no correctness issues if multiple threads perform this computation - // simultaneously, as they will all store the same value. - return switch (is_enabled.load(.monotonic)) { - .enabled => true, - .disabled => false, - .idk => { - const enabled = if (getenvZ(@tagName(flag))) |val| - strings.eqlComptime(val, "1") or strings.eqlComptime(val, "true") - else - false; - is_enabled.store(if (enabled) .enabled else .disabled, .monotonic); - return enabled; - }, - }; - } - }.get(); -} - +/// Note: You likely do not need this function. See the pattern in env_var.zig for adding +/// environment variables. +/// TODO(markovejnovic): Sunset this function when its last usage is removed. +/// This wrapper exists to avoid the call to sliceTo(0) +/// Zig's sliceTo(0) is scalar pub fn getenvZAnyCase(key: [:0]const u8) ?[]const u8 { for (std.os.environ) |lineZ| { const line = sliceTo(lineZ, 0); @@ -857,6 +838,9 @@ pub fn getenvZAnyCase(key: [:0]const u8) ?[]const u8 { return null; } +/// Note: You likely do not need this function. See the pattern in env_var.zig for adding +/// environment variables. +/// TODO(markovejnovic): Sunset this function when its last usage is removed. /// This wrapper exists to avoid the call to sliceTo(0) /// Zig's sliceTo(0) is scalar pub fn getenvZ(key: [:0]const u8) ?[]const u8 { @@ -872,6 +856,9 @@ pub fn getenvZ(key: [:0]const u8) ?[]const u8 { return sliceTo(pointer, 0); } +/// Note: You likely do not need this function. See the pattern in env_var.zig for adding +/// environment variables. +/// TODO(markovejnovic): Sunset this function when its last usage is removed. pub fn getenvTruthy(key: [:0]const u8) bool { if (getenvZ(key)) |value| return std.mem.eql(u8, value, "true") or std.mem.eql(u8, value, "1"); return false; @@ -1330,7 +1317,7 @@ pub fn getFdPath(fd: FileDescriptor, buf: *bun.PathBuffer) ![]u8 { if (!ProcSelfWorkAroundForDebugging.has_checked) { ProcSelfWorkAroundForDebugging.has_checked = true; - needs_proc_self_workaround = strings.eql(getenvZ("BUN_NEEDS_PROC_SELF_WORKAROUND") orelse "0", "1"); + needs_proc_self_workaround = bun.env_var.BUN_NEEDS_PROC_SELF_WORKAROUND.get(); } } else if (comptime !Environment.isLinux) { return try std.os.getFdPath(fd.native(), buf); @@ -2221,7 +2208,7 @@ pub fn initArgv(allocator: std.mem.Allocator) !void { argv = try std.process.argsAlloc(allocator); } - if (bun.getenvZ("BUN_OPTIONS")) |opts| { + if (bun.env_var.BUN_OPTIONS.get()) |opts| { var argv_list = std.ArrayList([:0]const u8).fromOwnedSlice(allocator, argv); try appendOptionsEnv(opts, &argv_list, allocator); argv = argv_list.items; diff --git a/src/bundler/ThreadPool.zig b/src/bundler/ThreadPool.zig index fb4a8c1db7..f1f260a279 100644 --- a/src/bundler/ThreadPool.zig +++ b/src/bundler/ThreadPool.zig @@ -118,12 +118,12 @@ pub const ThreadPool = struct { } pub fn usesIOPool() bool { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_FORCE_IO_POOL)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_FORCE_IO_POOL.get()) { // For testing. return true; } - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_IO_POOL)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_IO_POOL.get()) { // For testing. return false; } diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index fbade1b0ff..ba06fb00ef 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -18,7 +18,7 @@ pub fn deinit(this: *StaticRouteVisitor) void { /// Investigate performance. It can have false negatives (it doesn't properly /// handle cycles), but that's okay as it's just used an optimization pub fn hasTransitiveUseClient(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { - if (bun.Environment.isDebug and bun.getenvZ("BUN_SSG_DISABLE_STATIC_ROUTE_VISITOR") != null) { + if (bun.Environment.isDebug and bun.env_var.BUN_SSG_DISABLE_STATIC_ROUTE_VISITOR.get()) { return false; } diff --git a/src/bundler/linker_context/findImportedFilesInCSSOrder.zig b/src/bundler/linker_context/findImportedFilesInCSSOrder.zig index 2384be6932..74e5c7fb5c 100644 --- a/src/bundler/linker_context/findImportedFilesInCSSOrder.zig +++ b/src/bundler/linker_context/findImportedFilesInCSSOrder.zig @@ -604,7 +604,7 @@ const CssOrderDebugStep = enum { fn debugCssOrder(this: *LinkerContext, order: *const BabyList(Chunk.CssImportOrder), comptime step: CssOrderDebugStep) void { if (comptime bun.Environment.isDebug) { const env_var = "BUN_DEBUG_CSS_ORDER_" ++ @tagName(step); - const enable_all = bun.getenvTruthy("BUN_DEBUG_CSS_ORDER"); + const enable_all = bun.env_var.BUN_DEBUG_CSS_ORDER.get(); if (enable_all or bun.getenvTruthy(env_var)) { debugCssOrderImpl(this, order, step); } diff --git a/src/ci_info.zig b/src/ci_info.zig index 861fa30845..00a1f7099a 100644 --- a/src/ci_info.zig +++ b/src/ci_info.zig @@ -73,14 +73,14 @@ const CI = enum { var name: []const u8 = ""; defer ci_name = name; - if (bun.getenvZ("CI")) |ci| { - if (strings.eqlComptime(ci, "false")) { + if (bun.env_var.CI.get()) |ci| { + if (!ci) { return; } } // Special case Heroku - if (bun.getenvZ("NODE")) |node| { + if (bun.env_var.NODE.get()) |node| { if (strings.containsComptime(node, "/app/.heroku/node/bin/node")) { name = "heroku"; return; diff --git a/src/cli.zig b/src/cli.zig index 76845a6e6d..981269f3cc 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -224,7 +224,7 @@ pub const HelpCommand = struct { if (comptime Environment.isDebug) { if (bun.argv.len == 1) { if (bun.Output.isAIAgent()) { - if (bun.getenvZ("npm_lifecycle_event")) |event| { + if (bun.env_var.npm_lifecycle_event.get()) |event| { if (bun.strings.hasPrefixComptime(event, "bd")) { // claude gets very confused by the help menu // let's give claude some self confidence. @@ -528,9 +528,9 @@ pub const Command = struct { // if we are bunx, but NOT a symlink to bun. when we run ` install`, we dont // want to recursively run bunx. so this check lets us peek back into bun install. if (args_iter.next()) |next| { - if (bun.strings.eqlComptime(next, "add") and bun.getRuntimeFeatureFlag(.BUN_INTERNAL_BUNX_INSTALL)) { + if (bun.strings.eqlComptime(next, "add") and bun.feature_flag.BUN_INTERNAL_BUNX_INSTALL.get()) { return .AddCommand; - } else if (bun.strings.eqlComptime(next, "exec") and bun.getRuntimeFeatureFlag(.BUN_INTERNAL_BUNX_INSTALL)) { + } else if (bun.strings.eqlComptime(next, "exec") and bun.feature_flag.BUN_INTERNAL_BUNX_INSTALL.get()) { return .ExecCommand; } } @@ -659,13 +659,13 @@ pub const Command = struct { /// function or that stack space is used up forever. pub fn start(allocator: std.mem.Allocator, log: *logger.Log) !void { if (comptime Environment.allow_assert) { - if (bun.getenvZ("MI_VERBOSE") == null) { + if (!bun.env_var.MI_VERBOSE.get()) { bun.mimalloc.mi_option_set_enabled(.verbose, false); } } // bun build --compile entry point - if (!bun.getRuntimeFeatureFlag(.BUN_BE_BUN)) { + if (!bun.feature_flag.BUN_BE_BUN.get()) { if (try bun.StandaloneModuleGraph.fromExecutable(bun.default_allocator)) |graph| { var offset_for_passthrough: usize = 0; @@ -1152,8 +1152,8 @@ pub const Command = struct { Command.Tag.CreateCommand => { const intro_text = \\Usage: - \\ bun create \ - \\ bun create \ [...flags] dest + \\ bun create \ + \\ bun create \ [...flags] dest \\ bun create \ [...flags] dest \\ \\Environment variables: diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index a87f471e99..2c55ffb5d1 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -242,11 +242,16 @@ pub fn loadConfigPath(allocator: std.mem.Allocator, auto_loaded: bool, config_pa } fn getHomeConfigPath(buf: *bun.PathBuffer) ?[:0]const u8 { - if (bun.getenvZ("XDG_CONFIG_HOME") orelse bun.getenvZ(bun.DotEnv.home_env)) |data_dir| { - var paths = [_]string{".bunfig.toml"}; + var paths = [_]string{".bunfig.toml"}; + + if (bun.env_var.XDG_CONFIG_HOME.get()) |data_dir| { return resolve_path.joinAbsStringBufZ(data_dir, buf, &paths, .auto); } + if (bun.env_var.HOME.get()) |home_dir| { + return resolve_path.joinAbsStringBufZ(home_dir, buf, &paths, .auto); + } + return null; } pub fn loadConfig(allocator: std.mem.Allocator, user_config_path_: ?string, ctx: Command.Context, comptime cmd: Command.Tag) OOM!void { @@ -595,7 +600,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C const preloads = args.options("--preload"); const preloads2 = args.options("--require"); const preloads3 = args.options("--import"); - const preload4 = bun.getenvZ("BUN_INSPECT_PRELOAD"); + const preload4 = bun.env_var.BUN_INSPECT_PRELOAD.get(); const total_preloads = ctx.preloads.len + preloads.len + preloads2.len + preloads3.len + (if (preload4 != null) @as(usize, 1) else @as(usize, 0)); if (total_preloads > 0) { @@ -803,10 +808,8 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C } else if (use_system_ca) { Bun__Node__CAStore = .system; } else { - if (bun.getenvZ("NODE_USE_SYSTEM_CA")) |val| { - if (val.len > 0 and val[0] == '1') { - Bun__Node__CAStore = .system; - } + if (bun.env_var.NODE_USE_SYSTEM_CA.get()) { + Bun__Node__CAStore = .system; } } diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index f679eb59b1..09f7c29108 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -772,7 +772,7 @@ pub const BunxCommand = struct { switch (spawn_result.status) { .exited => |exit| { if (exit.signal.valid()) { - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } @@ -784,7 +784,7 @@ pub const BunxCommand = struct { } }, .signaled => |signal| { - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 1bea76377b..7edff915f2 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -1885,7 +1885,7 @@ pub const Example = struct { folders[1] = std.fs.cwd().openDir(outdir_path, .{}) catch bun.invalid_fd.stdDir(); } - if (env_loader.map.get(bun.DotEnv.home_env)) |home_dir| { + if (env_loader.map.get(bun.env_var.HOME.key())) |home_dir| { var parts = [_]string{ home_dir, BUN_CREATE_DIR }; const outdir_path = filesystem.absBuf(&parts, &home_dir_buf); folders[2] = std.fs.cwd().openDir(outdir_path, .{}) catch bun.invalid_fd.stdDir(); @@ -2301,7 +2301,7 @@ pub const CreateListExamplesCommand = struct { Output.prettyln("# You can also paste a GitHub repository:\n\n bun create ahfarmer/calculator calc\n\n", .{}); - if (env_loader.map.get(bun.DotEnv.home_env)) |homedir| { + if (env_loader.map.get(bun.env_var.HOME.key())) |homedir| { Output.prettyln( "This command is completely optional. To add a new local template, create a folder in {s}/.bun-create/. To publish a new template, git clone https://github.com/oven-sh/bun, add a new folder to the \"examples\" folder, and submit a PR.", .{homedir}, diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index ac6aa4a594..e2ed22ead6 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -995,14 +995,14 @@ const Template = enum { } // Give some way to opt out. - if (bun.getenvTruthy("BUN_AGENT_RULE_DISABLED") or bun.getenvTruthy("CLAUDE_CODE_AGENT_RULE_DISABLED")) { + if (bun.env_var.BUN_AGENT_RULE_DISABLED.get() or bun.env_var.CLAUDE_CODE_AGENT_RULE_DISABLED.get()) { return false; } const pathbuffer = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(pathbuffer); - return bun.which(pathbuffer, bun.getenvZ("PATH") orelse return false, bun.fs.FileSystem.instance.top_level_dir, "claude") != null; + return bun.which(pathbuffer, bun.env_var.PATH.get() orelse return false, bun.fs.FileSystem.instance.top_level_dir, "claude") != null; } pub fn createAgentRule() void { @@ -1054,15 +1054,13 @@ const Template = enum { fn isCursorInstalled() bool { // Give some way to opt-out. - if (bun.getenvTruthy("BUN_AGENT_RULE_DISABLED") or bun.getenvTruthy("CURSOR_AGENT_RULE_DISABLED")) { + if (bun.env_var.BUN_AGENT_RULE_DISABLED.get() or bun.env_var.CURSOR_AGENT_RULE_DISABLED.get()) { return false; } // Detect if they're currently using cursor. - if (bun.getenvZAnyCase("CURSOR_TRACE_ID")) |env| { - if (env.len > 0) { - return true; - } + if (bun.env_var.CURSOR_TRACE_ID.get()) { + return true; } if (Environment.isMac) { diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index a205cf6c95..b7cdb62024 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -7,7 +7,7 @@ pub const InstallCompletionsCommand = struct { var buf: bun.PathBuffer = undefined; // don't install it if it's already there - if (bun.which(&buf, bun.getenvZ("PATH") orelse cwd, cwd, bunx_name) != null) + if (bun.which(&buf, bun.env_var.PATH.get() orelse cwd, cwd, bunx_name) != null) return; // first try installing the symlink into the same directory as the bun executable @@ -16,7 +16,7 @@ pub const InstallCompletionsCommand = struct { var target = std.fmt.bufPrint(&target_buf, "{s}/" ++ bunx_name, .{std.fs.path.dirname(exe).?}) catch unreachable; std.posix.symlink(exe, target) catch { outer: { - if (bun.getenvZ("BUN_INSTALL")) |install_dir| { + if (bun.env_var.BUN_INSTALL.get()) |install_dir| { target = std.fmt.bufPrint(&target_buf, "{s}/bin/" ++ bunx_name, .{install_dir}) catch unreachable; std.posix.symlink(exe, target) catch break :outer; return; @@ -25,7 +25,7 @@ pub const InstallCompletionsCommand = struct { // if that fails, try $HOME/.bun/bin outer: { - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.HOME.get()) |home_dir| { target = std.fmt.bufPrint(&target_buf, "{s}/.bun/bin/" ++ bunx_name, .{home_dir}) catch unreachable; std.posix.symlink(exe, target) catch break :outer; return; @@ -34,7 +34,7 @@ pub const InstallCompletionsCommand = struct { // if that fails, try $HOME/.local/bin outer: { - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.HOME.get()) |home_dir| { target = std.fmt.bufPrint(&target_buf, "{s}/.local/bin/" ++ bunx_name, .{home_dir}) catch unreachable; std.posix.symlink(exe, target) catch break :outer; return; @@ -123,14 +123,14 @@ pub const InstallCompletionsCommand = struct { pub fn exec(allocator: std.mem.Allocator) !void { // Fail silently on auto-update. - const fail_exit_code: u8 = if (bun.getenvZ("IS_BUN_AUTO_UPDATE") == null) 1 else 0; + const fail_exit_code: u8 = if (!bun.env_var.IS_BUN_AUTO_UPDATE.get()) 1 else 0; var cwd_buf: bun.PathBuffer = undefined; var stdout = std.io.getStdOut(); var shell = ShellCompletions.Shell.unknown; - if (bun.getenvZ("SHELL")) |shell_name| { + if (bun.env_var.SHELL.platformGet()) |shell_name| { shell = ShellCompletions.Shell.fromEnv(@TypeOf(shell_name), shell_name); } @@ -169,7 +169,7 @@ pub const InstallCompletionsCommand = struct { else => {}, } - if (bun.getenvZ("IS_BUN_AUTO_UPDATE") == null) { + if (!bun.env_var.IS_BUN_AUTO_UPDATE.get()) { if (!stdout.isTty()) { try stdout.writeAll(shell.completions()); Global.exit(0); @@ -210,7 +210,7 @@ pub const InstallCompletionsCommand = struct { switch (shell) { .fish => { - if (bun.getenvZ("XDG_CONFIG_HOME")) |config_dir| { + if (bun.env_var.XDG_CONFIG_HOME.get()) |config_dir| { outer: { var paths = [_]string{ config_dir, "./fish/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -219,7 +219,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ("XDG_DATA_HOME")) |data_dir| { + if (bun.env_var.XDG_DATA_HOME.get()) |data_dir| { outer: { var paths = [_]string{ data_dir, "./fish/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -229,7 +229,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.HOME.get()) |home_dir| { outer: { var paths = [_]string{ home_dir, "./.config/fish/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -260,7 +260,7 @@ pub const InstallCompletionsCommand = struct { } }, .zsh => { - if (bun.getenvZ("fpath")) |fpath| { + if (bun.env_var.fpath.get()) |fpath| { var splitter = std.mem.splitScalar(u8, fpath, ' '); while (splitter.next()) |dir| { @@ -269,7 +269,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ("XDG_DATA_HOME")) |data_dir| { + if (bun.env_var.XDG_DATA_HOME.get()) |data_dir| { outer: { var paths = [_]string{ data_dir, "./zsh-completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -279,7 +279,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ("BUN_INSTALL")) |home_dir| { + if (bun.env_var.BUN_INSTALL.get()) |home_dir| { outer: { completions_dir = home_dir; break :found std.fs.openDirAbsolute(home_dir, .{}) catch @@ -287,7 +287,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.HOME.get()) |home_dir| { { outer: { var paths = [_]string{ home_dir, "./.oh-my-zsh/completions" }; @@ -320,7 +320,7 @@ pub const InstallCompletionsCommand = struct { } }, .bash => { - if (bun.getenvZ("XDG_DATA_HOME")) |data_dir| { + if (bun.env_var.XDG_DATA_HOME.get()) |data_dir| { outer: { var paths = [_]string{ data_dir, "./bash-completion/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -329,7 +329,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ("XDG_CONFIG_HOME")) |config_dir| { + if (bun.env_var.XDG_CONFIG_HOME.get()) |config_dir| { outer: { var paths = [_]string{ config_dir, "./bash-completion/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); @@ -339,7 +339,7 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.HOME.get()) |home_dir| { { outer: { var paths = [_]string{ home_dir, "./.oh-my-bash/custom/completions" }; @@ -439,7 +439,7 @@ pub const InstallCompletionsCommand = struct { // $ZDOTDIR/.zlogin // $ZDOTDIR/.zlogout - if (bun.getenvZ("ZDOTDIR")) |zdot_dir| { + if (bun.env_var.ZDOTDIR.get()) |zdot_dir| { bun.copy(u8, &zshrc_filepath, zdot_dir); bun.copy(u8, zshrc_filepath[zdot_dir.len..], "/.zshrc"); zshrc_filepath[zdot_dir.len + "/.zshrc".len] = 0; @@ -449,7 +449,7 @@ pub const InstallCompletionsCommand = struct { } second: { - if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| { + if (bun.env_var.HOME.get()) |zdot_dir| { bun.copy(u8, &zshrc_filepath, zdot_dir); bun.copy(u8, zshrc_filepath[zdot_dir.len..], "/.zshrc"); zshrc_filepath[zdot_dir.len + "/.zshrc".len] = 0; @@ -459,7 +459,7 @@ pub const InstallCompletionsCommand = struct { } third: { - if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| { + if (bun.env_var.HOME.get()) |zdot_dir| { bun.copy(u8, &zshrc_filepath, zdot_dir); bun.copy(u8, zshrc_filepath[zdot_dir.len..], "/.zshenv"); zshrc_filepath[zdot_dir.len + "/.zshenv".len] = 0; @@ -531,7 +531,6 @@ pub const InstallCompletionsCommand = struct { const string = []const u8; -const DotEnv = @import("../env_loader.zig"); const ShellCompletions = @import("./shell_completions.zig"); const fs = @import("../fs.zig"); const resolve_path = @import("../resolver/resolve_path.zig"); diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index a1d2acea64..cf296288c2 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -111,7 +111,7 @@ pub const PackageManagerCommand = struct { \\ bun pm version [increment] bump the version in package.json and create a git tag \\ increment patch, minor, major, prepatch, preminor, premajor, prerelease, from-git, or a specific version \\ bun pm pkg manage data in package.json - \\ get [key ...] + \\ get [key ...] \\ set key=value ... \\ delete key ... \\ fix auto-correct common package.json errors @@ -200,7 +200,7 @@ pub const PackageManagerCommand = struct { if (pm.options.global) { warner: { if (Output.enable_ansi_colors_stderr) { - if (bun.getenvZ("PATH")) |path| { + if (bun.env_var.PATH.get()) |path| { var path_iter = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter); while (path_iter.next()) |entry| { if (strings.eql(entry, output_path)) { diff --git a/src/cli/pm_version_command.zig b/src/cli/pm_version_command.zig index 21deb456cb..4f6c69768b 100644 --- a/src/cli/pm_version_command.zig +++ b/src/cli/pm_version_command.zig @@ -284,7 +284,7 @@ pub const PmVersionCommand = struct { \\ patch {s} → {s} \\ minor {s} → {s} \\ major {s} → {s} - \\ prerelease {s} → {s} + \\ prerelease {s} → {s} \\ ; Output.pretty(increment_help_text, .{ @@ -448,7 +448,7 @@ pub const PmVersionCommand = struct { fn isGitClean(cwd: []const u8) bun.OOM!bool { var path_buf: bun.PathBuffer = undefined; - const git_path = bun.which(&path_buf, bun.getenvZ("PATH") orelse "", cwd, "git") orelse { + const git_path = bun.which(&path_buf, bun.env_var.PATH.get() orelse "", cwd, "git") orelse { Output.errGeneric("git must be installed to use `bun pm version --git-tag-version`", .{}); Global.exit(1); }; @@ -481,7 +481,7 @@ pub const PmVersionCommand = struct { fn getVersionFromGit(allocator: std.mem.Allocator, cwd: []const u8) bun.OOM![]const u8 { var path_buf: bun.PathBuffer = undefined; - const git_path = bun.which(&path_buf, bun.getenvZ("PATH") orelse "", cwd, "git") orelse { + const git_path = bun.which(&path_buf, bun.env_var.PATH.get() orelse "", cwd, "git") orelse { Output.errGeneric("git must be installed to use `bun pm version from-git`", .{}); Global.exit(1); }; @@ -528,7 +528,7 @@ pub const PmVersionCommand = struct { fn gitCommitAndTag(allocator: std.mem.Allocator, version: []const u8, custom_message: ?[]const u8, cwd: []const u8) bun.OOM!void { var path_buf: bun.PathBuffer = undefined; - const git_path = bun.which(&path_buf, bun.getenvZ("PATH") orelse "", cwd, "git") orelse { + const git_path = bun.which(&path_buf, bun.env_var.PATH.get() orelse "", cwd, "git") orelse { Output.errGeneric("git must be installed to use `bun pm version --git-tag-version`", .{}); Global.exit(1); }; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 7c5fbb0818..870c6a342e 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -274,7 +274,7 @@ pub const RunCommand = struct { }; const ipc_fd: ?bun.FD = if (!Environment.isWindows) blk: { - const node_ipc_fd = bun.getenvZ("NODE_CHANNEL_FD") orelse break :blk null; + const node_ipc_fd = bun.env_var.NODE_CHANNEL_FD.get() orelse break :blk null; const fd = std.fmt.parseInt(u31, node_ipc_fd, 10) catch break :blk null; break :blk bun.FD.fromNative(fd); } else null; // TODO: implement on Windows @@ -321,7 +321,7 @@ pub const RunCommand = struct { Output.prettyErrorln("error: script \"{s}\" was terminated by signal {}", .{ name, exit_code.signal.fmt(Output.enable_ansi_colors_stderr) }); Output.flush(); - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } @@ -344,7 +344,7 @@ pub const RunCommand = struct { Output.flush(); } - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } @@ -521,7 +521,7 @@ pub const RunCommand = struct { }); } - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } @@ -538,7 +538,7 @@ pub const RunCommand = struct { }); } - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN.get()) { bun.crash_handler.suppressReporting(); } @@ -1502,10 +1502,7 @@ pub const RunCommand = struct { const preserve_symlinks = this_transpiler.resolver.opts.preserve_symlinks; defer this_transpiler.resolver.opts.preserve_symlinks = preserve_symlinks; this_transpiler.resolver.opts.preserve_symlinks = ctx.runtime_options.preserve_symlinks_main or - if (bun.getenvZ("NODE_PRESERVE_SYMLINKS_MAIN")) |env| - bun.strings.eqlComptime(env, "1") - else - false; + bun.env_var.NODE_PRESERVE_SYMLINKS_MAIN.get(); break :brk this_transpiler.resolver.resolve( this_transpiler.fs.top_level_dir, target_name, diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index ba35dd803e..8436e1394d 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -182,9 +182,9 @@ pub const JunitReporter = struct { const properties: PropertiesList = .{ .ci = brk: { - if (bun.getenvZ("GITHUB_RUN_ID")) |github_run_id| { - if (bun.getenvZ("GITHUB_SERVER_URL")) |github_server_url| { - if (bun.getenvZ("GITHUB_REPOSITORY")) |github_repository| { + if (bun.env_var.GITHUB_RUN_ID.get()) |github_run_id| { + if (bun.env_var.GITHUB_SERVER_URL.get()) |github_server_url| { + if (bun.env_var.GITHUB_REPOSITORY.get()) |github_repository| { if (github_run_id.len > 0 and github_server_url.len > 0 and github_repository.len > 0) { break :brk try std.fmt.allocPrint(allocator, "{s}/{s}/actions/runs/{s}", .{ github_server_url, github_repository, github_run_id }); } @@ -192,7 +192,7 @@ pub const JunitReporter = struct { } } - if (bun.getenvZ("CI_JOB_URL")) |ci_job_url| { + if (bun.env_var.CI_JOB_URL.get()) |ci_job_url| { if (ci_job_url.len > 0) { break :brk ci_job_url; } @@ -201,19 +201,19 @@ pub const JunitReporter = struct { break :brk ""; }, .commit = brk: { - if (bun.getenvZ("GITHUB_SHA")) |github_sha| { + if (bun.env_var.GITHUB_SHA.get()) |github_sha| { if (github_sha.len > 0) { break :brk github_sha; } } - if (bun.getenvZ("CI_COMMIT_SHA")) |sha| { + if (bun.env_var.CI_COMMIT_SHA.get()) |sha| { if (sha.len > 0) { break :brk sha; } } - if (bun.getenvZ("GIT_SHA")) |git_sha| { + if (bun.env_var.GIT_SHA.get()) |git_sha| { if (git_sha.len > 0) { break :brk git_sha; } diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index a372098a8b..8e8ebbcda9 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -557,7 +557,7 @@ pub const UpgradeCommand = struct { save_dir.deleteFileZ(tmpname) catch {}; Global.exit(1); } - } else if (Environment.isWindows) { + } else if (comptime Environment.isWindows) { // Run a powershell script to unzip the file const unzip_script = try std.fmt.allocPrint( ctx.allocator, @@ -570,9 +570,9 @@ pub const UpgradeCommand = struct { var buf: bun.PathBuffer = undefined; const powershell_path = - bun.which(&buf, bun.getenvZ("PATH") orelse "", "", "powershell") orelse + bun.which(&buf, bun.env_var.PATH.get() orelse "", "", "powershell") orelse hardcoded_system_powershell: { - const system_root = bun.getenvZ("SystemRoot") orelse "C:\\Windows"; + const system_root = bun.env_var.SYSTEMROOT.get() orelse "C:\\Windows"; const hardcoded_system_powershell = bun.path.joinAbsStringBuf(system_root, &buf, &.{ system_root, "System32\\WindowsPowerShell\\v1.0\\powershell.exe" }, .windows); if (bun.sys.exists(hardcoded_system_powershell)) { break :hardcoded_system_powershell hardcoded_system_powershell; diff --git a/src/compile_target.zig b/src/compile_target.zig index 5ec1c5cecc..b0f88742ec 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -66,7 +66,7 @@ pub fn isDefault(this: *const CompileTarget) bool { } pub fn toNPMRegistryURL(this: *const CompileTarget, buf: []u8) ![]const u8 { - if (bun.getenvZ("BUN_COMPILE_TARGET_TARBALL_URL")) |url| { + if (bun.env_var.BUN_COMPILE_TARGET_TARBALL_URL.get()) |url| { if (strings.hasPrefixComptime(url, "http://") or strings.hasPrefixComptime(url, "https://")) return url; } diff --git a/src/copy_file.zig b/src/copy_file.zig index 08093297cb..0920457995 100644 --- a/src/copy_file.zig +++ b/src/copy_file.zig @@ -147,7 +147,7 @@ pub fn canUseCopyFileRangeSyscall() bool { const result = can_use_copy_file_range.load(.monotonic); if (result == 0) { // This flag mostly exists to make other code more easily testable. - if (bun.getenvZ("BUN_CONFIG_DISABLE_COPY_FILE_RANGE") != null) { + if (bun.env_var.BUN_CONFIG_DISABLE_COPY_FILE_RANGE.get()) { debug("copy_file_range is disabled by BUN_CONFIG_DISABLE_COPY_FILE_RANGE", .{}); can_use_copy_file_range.store(-1, .monotonic); return false; @@ -179,7 +179,7 @@ pub fn can_use_ioctl_ficlone() bool { const result = can_use_ioctl_ficlone_.load(.monotonic); if (result == 0) { // This flag mostly exists to make other code more easily testable. - if (bun.getenvZ("BUN_CONFIG_DISABLE_ioctl_ficlonerange") != null) { + if (bun.env_var.BUN_CONFIG_DISABLE_ioctl_ficlonerange.get()) { debug("ioctl_ficlonerange is disabled by BUN_CONFIG_DISABLE_ioctl_ficlonerange", .{}); can_use_ioctl_ficlone_.store(-1, .monotonic); return false; diff --git a/src/crash_handler.zig b/src/crash_handler.zig index b1fb33fa82..d1d98ff074 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -241,7 +241,7 @@ pub fn crashHandler( } else if (bun.analytics.Features.unsupported_uv_function > 0) { const name = unsupported_uv_function orelse ""; const fmt = - \\Bun encountered a crash when running a NAPI module that tried to call + \\Bun encountered a crash when running a NAPI module that tried to call \\the {s} libuv function. \\ \\Bun is actively working on supporting all libuv functions for POSIX @@ -403,7 +403,7 @@ pub fn crashHandler( } else if (bun.analytics.Features.unsupported_uv_function > 0) { const name = unsupported_uv_function orelse ""; const fmt = - \\Bun encountered a crash when running a NAPI module that tried to call + \\Bun encountered a crash when running a NAPI module that tried to call \\the {s} libuv function. \\ \\Bun is actively working on supporting all libuv functions for POSIX @@ -583,7 +583,7 @@ pub fn handleRootError(err: anyerror, error_return_trace: ?*std.builtin.StackTra }, ); - if (bun.getenvZ("USER")) |user| { + if (bun.env_var.USER.get()) |user| { if (user.len > 0) { Output.prettyError( \\ @@ -652,7 +652,7 @@ pub fn handleRootError(err: anyerror, error_return_trace: ?*std.builtin.StackTra }, ); - if (bun.getenvZ("USER")) |user| { + if (bun.env_var.USER.get()) |user| { if (user.len > 0) { Output.prettyError( \\ @@ -699,7 +699,7 @@ pub fn handleRootError(err: anyerror, error_return_trace: ?*std.builtin.StackTra ); if (bun.Environment.isLinux) { - if (bun.getenvZ("USER")) |user| { + if (bun.env_var.USER.get()) |user| { if (user.len > 0) { Output.prettyError( \\ @@ -804,7 +804,7 @@ pub fn reportBaseUrl() []const u8 { }; return static.base_url orelse { const computed = computed: { - if (bun.getenvZ("BUN_CRASH_REPORT_URL")) |url| { + if (bun.env_var.BUN_CRASH_REPORT_URL.get()) |url| { break :computed bun.strings.withoutTrailingSlash(url); } break :computed default_report_base_url; @@ -1412,18 +1412,13 @@ fn isReportingEnabled() bool { if (suppress_reporting) return false; // If trying to test the crash handler backend, implicitly enable reporting - if (bun.getenvZ("BUN_CRASH_REPORT_URL")) |value| { + if (bun.env_var.BUN_CRASH_REPORT_URL.get()) |value| { return value.len > 0; } // Environment variable to specifically enable or disable reporting - if (bun.getenvZ("BUN_ENABLE_CRASH_REPORTING")) |value| { - if (value.len > 0) { - if (bun.strings.eqlComptime(value, "1")) { - return true; - } - return false; - } + if (bun.env_var.BUN_ENABLE_CRASH_REPORTING.get()) |enable_crash_reporting| { + return enable_crash_reporting; } // Debug builds shouldn't report to the default url by default @@ -1512,7 +1507,7 @@ fn report(url: []const u8) void { var buf2: bun.PathBuffer = undefined; const curl = bun.which( &buf, - bun.getenvZ("PATH") orelse return, + bun.env_var.PATH.get() orelse return, bun.getcwd(&buf2) catch return, "curl", ) orelse return; @@ -2265,7 +2260,7 @@ export fn CrashHandler__setInsideNativePlugin(name: ?[*:0]const u8) callconv(.C) export fn CrashHandler__unsupportedUVFunction(name: ?[*:0]const u8) callconv(.C) void { bun.analytics.Features.unsupported_uv_function += 1; unsupported_uv_function = name; - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_ON_UV_STUB)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_ON_UV_STUB.get()) { suppressReporting(); } std.debug.panic("unsupported uv function: {s}", .{name.?}); diff --git a/src/env_loader.zig b/src/env_loader.zig index fb1787f758..ef876173ec 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -1332,8 +1332,6 @@ pub const Map = struct { pub var instance: ?*Loader = null; -pub const home_env = if (Environment.isWindows) "USERPROFILE" else "HOME"; - const string = []const u8; const Fs = @import("./fs.zig"); diff --git a/src/env_var.zig b/src/env_var.zig new file mode 100644 index 0000000000..99b35005f3 --- /dev/null +++ b/src/env_var.zig @@ -0,0 +1,656 @@ +//! Unified module for controlling and managing environment variables in Bun. +//! +//! This library uses metaprogramming to achieve type-safe accessors for environment variables. +//! Calling .get() on any of the environment variables will return the correct environment variable +//! type, whether it's a string, unsigned or boolean. This library also caches the environment +//! variables for you, for slightly faster access. +//! +//! If default values are provided, the .get() method is guaranteed not to return a nullable type, +//! whereas if no default is provided, the .get() method will return an optional type. +//! +//! TODO(markovejnovic): It would be neat if this library supported loading floats as +//! well as strings, integers and booleans, but for now this will do. +//! +//! TODO(markovejnovic): As this library migrates away from bun.getenvZ, it should return +//! NUL-terminated slices, rather than plain slices. Perhaps there should be a +//! .getZ() accessor? +//! +//! TODO(markovejnovic): This current implementation kind of does redundant work. Instead of +//! scanning envp, and preparing everything on bootup, we lazily load +//! everything. This means that we potentially scan through envp a lot of +//! times, even though we could only do it once. + +pub const AGENT = New(kind.string, "AGENT", .{}); +pub const BUN_AGENT_RULE_DISABLED = New(kind.boolean, "BUN_AGENT_RULE_DISABLED", .{ .default = false }); +pub const BUN_COMPILE_TARGET_TARBALL_URL = New(kind.string, "BUN_COMPILE_TARGET_TARBALL_URL", .{}); +pub const BUN_CONFIG_DISABLE_COPY_FILE_RANGE = New(kind.boolean, "BUN_CONFIG_DISABLE_COPY_FILE_RANGE", .{ .default = false }); +pub const BUN_CONFIG_DISABLE_ioctl_ficlonerange = New(kind.boolean, "BUN_CONFIG_DISABLE_ioctl_ficlonerange", .{ .default = false }); +/// TODO(markovejnovic): Legacy usage had the default at 30, even though a the attached comment +/// quoted: Amazon Web Services recommends 5 seconds: +/// https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/jvm-ttl-dns.html +/// +/// It's unclear why this was done. +pub const BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS = New(kind.unsigned, "BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS", .{ .default = 30 }); +pub const BUN_CRASH_REPORT_URL = New(kind.string, "BUN_CRASH_REPORT_URL", .{}); +pub const BUN_DEBUG = New(kind.string, "BUN_DEBUG", .{}); +pub const BUN_DEBUG_ALL = New(kind.boolean, "BUN_DEBUG_ALL", .{}); +pub const BUN_DEBUG_CSS_ORDER = New(kind.boolean, "BUN_DEBUG_CSS_ORDER", .{ .default = false }); +pub const BUN_DEBUG_ENABLE_RESTORE_FROM_TRANSPILER_CACHE = New(kind.boolean, "BUN_DEBUG_ENABLE_RESTORE_FROM_TRANSPILER_CACHE", .{ .default = false }); +pub const BUN_DEBUG_HASH_RANDOM_SEED = New(kind.unsigned, "BUN_DEBUG_HASH_RANDOM_SEED", .{ .deser = .{ .error_handling = .not_set } }); +pub const BUN_DEBUG_QUIET_LOGS = New(kind.boolean, "BUN_DEBUG_QUIET_LOGS", .{}); +pub const BUN_DEBUG_TEST_TEXT_LOCKFILE = New(kind.boolean, "BUN_DEBUG_TEST_TEXT_LOCKFILE", .{ .default = false }); +pub const BUN_DEV_SERVER_TEST_RUNNER = New(kind.string, "BUN_DEV_SERVER_TEST_RUNNER", .{}); +pub const BUN_ENABLE_CRASH_REPORTING = New(kind.boolean, "BUN_ENABLE_CRASH_REPORTING", .{}); +pub const BUN_FEATURE_FLAG_DUMP_CODE = New(kind.string, "BUN_FEATURE_FLAG_DUMP_CODE", .{}); +/// TODO(markovejnovic): It's unclear why the default here is 100_000, but this was legacy behavior +/// so we'll keep it for now. +pub const BUN_INOTIFY_COALESCE_INTERVAL = New(kind.unsigned, "BUN_INOTIFY_COALESCE_INTERVAL", .{ .default = 100_000 }); +pub const BUN_INSPECT = New(kind.string, "BUN_INSPECT", .{ .default = "" }); +pub const BUN_INSPECT_CONNECT_TO = New(kind.string, "BUN_INSPECT_CONNECT_TO", .{ .default = "" }); +pub const BUN_INSPECT_PRELOAD = New(kind.string, "BUN_INSPECT_PRELOAD", .{}); +pub const BUN_INSTALL = New(kind.string, "BUN_INSTALL", .{}); +pub const BUN_INSTALL_BIN = New(kind.string, "BUN_INSTALL_BIN", .{}); +pub const BUN_INSTALL_GLOBAL_DIR = New(kind.string, "BUN_INSTALL_GLOBAL_DIR", .{}); +pub const BUN_NEEDS_PROC_SELF_WORKAROUND = New(kind.boolean, "BUN_NEEDS_PROC_SELF_WORKAROUND", .{ .default = false }); +pub const BUN_OPTIONS = New(kind.string, "BUN_OPTIONS", .{}); +pub const BUN_POSTGRES_SOCKET_MONITOR = New(kind.string, "BUN_POSTGRES_SOCKET_MONITOR", .{}); +pub const BUN_POSTGRES_SOCKET_MONITOR_READER = New(kind.string, "BUN_POSTGRES_SOCKET_MONITOR_READER", .{}); +pub const BUN_RUNTIME_TRANSPILER_CACHE_PATH = New(kind.string, "BUN_RUNTIME_TRANSPILER_CACHE_PATH", .{}); +pub const BUN_SSG_DISABLE_STATIC_ROUTE_VISITOR = New(kind.boolean, "BUN_SSG_DISABLE_STATIC_ROUTE_VISITOR", .{ .default = false }); +pub const BUN_TCC_OPTIONS = New(kind.string, "BUN_TCC_OPTIONS", .{}); +pub const BUN_TMPDIR = New(kind.string, "BUN_TMPDIR", .{}); +pub const BUN_TRACK_LAST_FN_NAME = New(kind.boolean, "BUN_TRACK_LAST_FN_NAME", .{ .default = false }); +pub const BUN_TRACY_PATH = New(kind.string, "BUN_TRACY_PATH", .{}); +pub const BUN_WATCHER_TRACE = New(kind.string, "BUN_WATCHER_TRACE", .{}); +pub const CI = New(kind.boolean, "CI", .{}); +pub const CI_COMMIT_SHA = New(kind.string, "CI_COMMIT_SHA", .{}); +pub const CI_JOB_URL = New(kind.string, "CI_JOB_URL", .{}); +pub const CLAUDE_CODE_AGENT_RULE_DISABLED = New(kind.boolean, "CLAUDE_CODE_AGENT_RULE_DISABLED", .{ .default = false }); +pub const CLAUDECODE = New(kind.boolean, "CLAUDECODE", .{ .default = false }); +pub const COLORTERM = New(kind.string, "COLORTERM", .{}); +pub const CURSOR_AGENT_RULE_DISABLED = New(kind.boolean, "CURSOR_AGENT_RULE_DISABLED", .{ .default = false }); +pub const CURSOR_TRACE_ID = New(kind.boolean, "CURSOR_TRACE_ID", .{ .default = false }); +pub const DO_NOT_TRACK = New(kind.boolean, "DO_NOT_TRACK", .{ .default = false }); +pub const DYLD_ROOT_PATH = PlatformSpecificNew(kind.string, "DYLD_ROOT_PATH", null, .{}); +/// TODO(markovejnovic): We should support enums in this library, and force_color's usage is, +/// indeed, an enum. The 80-20 is to make it an unsigned value (which also works well). +pub const FORCE_COLOR = New(kind.unsigned, "FORCE_COLOR", .{ .deser = .{ .error_handling = .truthy_cast, .empty_string_as = .{ .value = 1 } } }); +pub const fpath = PlatformSpecificNew(kind.string, "fpath", null, .{}); +pub const GIT_SHA = New(kind.string, "GIT_SHA", .{}); +pub const GITHUB_ACTIONS = New(kind.boolean, "GITHUB_ACTIONS", .{ .default = false }); +pub const GITHUB_REPOSITORY = New(kind.string, "GITHUB_REPOSITORY", .{}); +pub const GITHUB_RUN_ID = New(kind.string, "GITHUB_RUN_ID", .{}); +pub const GITHUB_SERVER_URL = New(kind.string, "GITHUB_SERVER_URL", .{}); +pub const GITHUB_SHA = New(kind.string, "GITHUB_SHA", .{}); +pub const GITHUB_WORKSPACE = New(kind.string, "GITHUB_WORKSPACE", .{}); +pub const HOME = PlatformSpecificNew(kind.string, "HOME", "USERPROFILE", .{}); +pub const HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET = New(kind.string, "HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET", .{}); +pub const IS_BUN_AUTO_UPDATE = New(kind.boolean, "IS_BUN_AUTO_UPDATE", .{ .default = false }); +pub const JENKINS_URL = New(kind.string, "JENKINS_URL", .{}); +/// Dump mimalloc statistics at the end of the process. Note that this is not the same as +/// `MIMALLOC_VERBOSE`, documented here: https://microsoft.github.io/mimalloc/environment.html +pub const MI_VERBOSE = New(kind.boolean, "MI_VERBOSE", .{ .default = false }); +pub const NO_COLOR = New(kind.boolean, "NO_COLOR", .{ .default = false }); +pub const NODE = New(kind.string, "NODE", .{}); +pub const NODE_CHANNEL_FD = New(kind.string, "NODE_CHANNEL_FD", .{}); +pub const NODE_PRESERVE_SYMLINKS_MAIN = New(kind.boolean, "NODE_PRESERVE_SYMLINKS_MAIN", .{ .default = false }); +pub const NODE_USE_SYSTEM_CA = New(kind.boolean, "NODE_USE_SYSTEM_CA", .{ .default = false }); +pub const npm_lifecycle_event = New(kind.string, "npm_lifecycle_event", .{}); +pub const PATH = New(kind.string, "PATH", .{}); +pub const REPL_ID = New(kind.boolean, "REPL_ID", .{ .default = false }); +pub const RUNNER_DEBUG = New(kind.boolean, "RUNNER_DEBUG", .{ .default = false }); +pub const SDKROOT = PlatformSpecificNew(kind.string, "SDKROOT", null, .{}); +pub const SHELL = PlatformSpecificNew(kind.string, "SHELL", null, .{}); +/// C:\Windows, for example. +/// Note: Do not use this variable directly -- use os.zig's implementation instead. +pub const SYSTEMROOT = PlatformSpecificNew(kind.string, null, "SYSTEMROOT", .{}); +pub const TEMP = PlatformSpecificNew(kind.string, null, "TEMP", .{}); +pub const TERM = New(kind.string, "TERM", .{}); +pub const TERM_PROGRAM = New(kind.string, "TERM_PROGRAM", .{}); +pub const TMP = PlatformSpecificNew(kind.string, null, "TMP", .{}); +pub const TMPDIR = PlatformSpecificNew(kind.string, "TMPDIR", null, .{}); +pub const TMUX = New(kind.string, "TMUX", .{}); +pub const TODIUM = New(kind.string, "TODIUM", .{}); +pub const USER = PlatformSpecificNew(kind.string, "USER", "USERNAME", .{}); +pub const WANTS_LOUD = New(kind.boolean, "WANTS_LOUD", .{ .default = false }); +/// The same as system_root. +/// Note: Do not use this variable directly -- use os.zig's implementation instead. +/// TODO(markovejnovic): Perhaps we could add support for aliases in the library, so you could +/// specify both WINDIR and SYSTEMROOT and the loader would check both? +pub const WINDIR = PlatformSpecificNew(kind.string, null, "WINDIR", .{}); +/// XDG Base Directory Specification variables. +/// For some reason, legacy usage respected these even on Windows. To avoid compatibility issues, +/// we respect them too. +pub const XDG_CACHE_HOME = New(kind.string, "XDG_CACHE_HOME", .{}); +pub const XDG_CONFIG_HOME = New(kind.string, "XDG_CONFIG_HOME", .{}); +pub const XDG_DATA_HOME = New(kind.string, "XDG_DATA_HOME", .{}); +pub const ZDOTDIR = New(kind.string, "ZDOTDIR", .{}); + +pub const feature_flag = struct { + pub const BUN_ASSUME_PERFECT_INCREMENTAL = newFeatureFlag("BUN_ASSUME_PERFECT_INCREMENTAL", .{ .default = null }); + pub const BUN_BE_BUN = newFeatureFlag("BUN_BE_BUN", .{}); + pub const BUN_DEBUG_NO_DUMP = newFeatureFlag("BUN_DEBUG_NO_DUMP", .{}); + pub const BUN_DESTRUCT_VM_ON_EXIT = newFeatureFlag("BUN_DESTRUCT_VM_ON_EXIT", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_DNS_CACHE = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_DNS_CACHE", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_IO_POOL = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_IO_POOL", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_IPV4 = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_IPV4", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_IPV6 = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_IPV6", .{}); + /// The RedisClient supports auto-pipelining by default. This flag disables that behavior. + pub const BUN_FEATURE_FLAG_DISABLE_REDIS_AUTO_PIPELINING = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_REDIS_AUTO_PIPELINING", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK", .{}); + pub const BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING = newFeatureFlag("BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING", .{}); + pub const BUN_DISABLE_SOURCE_CODE_PREVIEW = newFeatureFlag("BUN_DISABLE_SOURCE_CODE_PREVIEW", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING", .{}); + pub const BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW = newFeatureFlag("BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW", .{}); + pub const BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE = newFeatureFlag("BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE", .{}); + pub const BUN_DUMP_STATE_ON_CRASH = newFeatureFlag("BUN_DUMP_STATE_ON_CRASH", .{}); + pub const BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS = newFeatureFlag("BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS", .{}); + pub const BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE = newFeatureFlag("BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE", .{}); + pub const BUN_FEATURE_FLAG_FORCE_IO_POOL = newFeatureFlag("BUN_FEATURE_FLAG_FORCE_IO_POOL", .{}); + pub const BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS = newFeatureFlag("BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS", .{}); + pub const BUN_INSTRUMENTS = newFeatureFlag("BUN_INSTRUMENTS", .{}); + pub const BUN_INTERNAL_BUNX_INSTALL = newFeatureFlag("BUN_INTERNAL_BUNX_INSTALL", .{}); + pub const BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN = newFeatureFlag("BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN", .{}); + pub const BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT = newFeatureFlag("BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT", .{}); + pub const BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF = newFeatureFlag("BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF", .{}); + pub const BUN_INTERNAL_SUPPRESS_CRASH_ON_UV_STUB = newFeatureFlag("BUN_INTERNAL_SUPPRESS_CRASH_ON_UV_STUB", .{}); + pub const BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304 = newFeatureFlag("BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304", .{}); + pub const BUN_NO_CODESIGN_MACHO_BINARY = newFeatureFlag("BUN_NO_CODESIGN_MACHO_BINARY", .{}); + pub const BUN_FEATURE_FLAG_NO_LIBDEFLATE = newFeatureFlag("BUN_FEATURE_FLAG_NO_LIBDEFLATE", .{}); + pub const NODE_NO_WARNINGS = newFeatureFlag("NODE_NO_WARNINGS", .{}); + pub const BUN_TRACE = newFeatureFlag("BUN_TRACE", .{}); +}; + +/// Interface between each of the different EnvVar types and the common logic. +fn CacheOutput(comptime ValueType: type) type { + return union(enum) { + /// The environment variable hasn't been loaded yet. + unknown: void, + /// The environment variable has been loaded but its not set. + not_set: void, + /// The environment variable is set to a value. + value: ValueType, + }; +} + +fn CacheConfigurationType(comptime CtorOptionsType: type) type { + return struct { + var_name: []const u8, + opts: CtorOptionsType, + }; +} + +/// Structure which encodes the different types of environment variables supported. +/// +/// This requires the following static members: +/// +/// - `ValueType`: The underlying environment variable type if one is set. For +/// example, a string `$PATH` ought return a `[]const u8` when set. +/// - `Cache`: A struct implementing the following methods: +/// - `getCached() CacheOutput(ValueType)`: Retrieve the cached value of the +/// environment variable, if any. +/// - `deserAndInvalidate(raw_env: ?[]const u8) ?ValueType` +/// - `CtorOptions`: A struct containing the options passed to the constructor of the environment +/// variable definition. +/// +/// This type will communicate with the common logic via the `CacheOutput` type. +const kind = struct { + const string = struct { + const ValueType = []const u8; + const Input = CacheConfigurationType(CtorOptions); + const Output = CacheOutput(ValueType); + const CtorOptions = struct { + default: ?ValueType = null, + }; + + fn Cache(comptime ip: Input) type { + _ = ip; + + const PointerType = ?[*]const u8; + const LenType = usize; + + return struct { + const Self = @This(); + + const not_loaded_sentinel = struct { + const ptr: PointerType = null; + const len: LenType = std.math.maxInt(LenType); + }; + + const not_set_sentinel = struct { + const ptr: PointerType = null; + const len: LenType = std.math.maxInt(LenType) - 1; + }; + + ptr_value: std.atomic.Value(PointerType) = .init(null), + len_value: std.atomic.Value(LenType) = .init(std.math.maxInt(LenType)), + + fn getCached(self: *Self) Output { + const len = self.len_value.load(.acquire); + + if (len == not_loaded_sentinel.len) { + return .{ .unknown = {} }; + } + + if (len == not_set_sentinel.len) { + return .{ .not_set = {} }; + } + + const ptr = self.ptr_value.load(.monotonic); + + return .{ .value = ptr.?[0..len] }; + } + + inline fn deserAndInvalidate(self: *Self, raw_env: ?[]const u8) ?ValueType { + // The implementation is racy and allows two threads to both set the value at + // the same time, as long as the value they are setting is the same. This is + // difficult to write an assertion for since it requires the DEV path take a + // .swap() path rather than a plain .store(). + + if (raw_env) |ev| { + self.ptr_value.store(ev.ptr, .monotonic); + self.len_value.store(ev.len, .release); + } else { + self.ptr_value.store(not_set_sentinel.ptr, .monotonic); + self.len_value.store(not_set_sentinel.len, .release); + } + + return raw_env; + } + }; + } + }; + + const boolean = struct { + const ValueType = bool; + const Input = CacheConfigurationType(CtorOptions); + const Output = CacheOutput(ValueType); + const CtorOptions = struct { + default: ?ValueType = null, + }; + + fn stringIsTruthy(s: []const u8) bool { + // Most values are considered truthy, except for "", "0", "false", "no", and "off". + const false_values = .{ "", "0", "false", "no", "off" }; + + inline for (false_values) |tv| { + if (std.ascii.eqlIgnoreCase(s, tv)) { + return false; + } + } + + return true; + } + + // This is a template which ignores its parameter, but is necessary so that a separate + // Cache type is emitted for every environment variable. + fn Cache(comptime ip: Input) type { + return struct { + const Self = @This(); + + const StoredType = enum(u8) { unknown, not_set, no, yes }; + + value: std.atomic.Value(StoredType) = .init(.unknown), + + inline fn getCached(self: *Self) Output { + _ = ip; + + const cached = self.value.load(.monotonic); + switch (cached) { + .unknown => { + @branchHint(.unlikely); + return .{ .unknown = {} }; + }, + .not_set => { + return .{ .not_set = {} }; + }, + .no => { + return .{ .value = false }; + }, + .yes => { + return .{ .value = true }; + }, + } + } + + inline fn deserAndInvalidate(self: *Self, raw_env: ?[]const u8) ?ValueType { + if (raw_env == null) { + self.value.store(.not_set, .monotonic); + return null; + } + + const string_is_truthy = stringIsTruthy(raw_env.?); + self.value.store(if (string_is_truthy) .yes else .no, .monotonic); + return string_is_truthy; + } + }; + } + }; + + const unsigned = struct { + const ValueType = u64; + const Input = CacheConfigurationType(CtorOptions); + const Output = CacheOutput(ValueType); + const CtorOptions = struct { + default: ?ValueType = null, + deser: struct { + /// Control how deserializing and deserialization errors are handled. + error_handling: enum { + /// panic on deserialization errors. + panic, + /// Ignore deserialization errors and treat the variable as not set. + not_set, + /// Fallback to default. + default_fallback, + /// Formatting errors are treated as truthy values. + /// + /// If this library fails to parse the value as an integer and truthy cast is + /// enabled, truthy values will be set to 1 or 0. + /// + /// Note: Most values are considered truthy, except for "", "0", "false", "no", + /// and "off". + truthy_cast, + } = .panic, + + /// Control what empty strings are treated as. + empty_string_as: union(enum) { + /// Empty strings are handled as the given value. + value: ValueType, + /// Empty strings are treated as deserialization errors. + erroneous: void, + } = .erroneous, + } = .{}, + }; + + fn Cache(comptime ip: Input) type { + return struct { + const Self = @This(); + + const StoredType = ValueType; + + /// The value meaning an environment variable that hasn't been loaded yet. + const unknown_sentinel: comptime_int = std.math.maxInt(StoredType); + /// The unique value representing an environment variable that is not set. + const not_set_sentinel: comptime_int = std.math.maxInt(StoredType) - 1; + + value: std.atomic.Value(StoredType) = .init(unknown_sentinel), + + inline fn getCached(self: *Self) Output { + switch (self.value.load(.monotonic)) { + unknown_sentinel => { + @branchHint(.unlikely); + return .{ .unknown = {} }; + }, + not_set_sentinel => { + return .{ .not_set = {} }; + }, + else => |v| { + return .{ .value = v }; + }, + } + } + + inline fn deserAndInvalidate(self: *Self, raw_env: ?[]const u8) ?ValueType { + if (raw_env == null) { + self.value.store(not_set_sentinel, .monotonic); + return null; + } + + if (std.mem.eql(u8, raw_env.?, "")) { + switch (ip.opts.deser.empty_string_as) { + .value => |v| { + self.value.store(v, .monotonic); + return v; + }, + .erroneous => { + return self.handleError(raw_env.?, "is an empty string"); + }, + } + } + + const formatted = std.fmt.parseInt(StoredType, raw_env.?, 10) catch |err| { + switch (err) { + error.Overflow => { + return self.handleError(raw_env.?, "overflows u64"); + }, + error.InvalidCharacter => { + return self.handleError(raw_env.?, "is not a valid integer"); + }, + } + }; + + if (formatted == not_set_sentinel or formatted == unknown_sentinel) { + return self.handleError(raw_env.?, "is a reserved value"); + } + + self.value.store(formatted, .monotonic); + return formatted; + } + + fn handleError( + self: *Self, + raw_env: []const u8, + comptime reason: []const u8, + ) ?ValueType { + const base_fmt = "Environment variable '{s}' has value '{s}' which "; + const fmt = base_fmt ++ reason ++ "."; + const missing_default_fmt = "Environment variable '{s}' is configured to " ++ + "fallback to default on {s}, but no default is set."; + + switch (ip.opts.deser.error_handling) { + .panic => { + bun.Output.panic(fmt, .{ ip.var_name, raw_env }); + }, + .not_set => { + self.value.store(not_set_sentinel, .monotonic); + return null; + }, + .truthy_cast => { + if (kind.boolean.stringIsTruthy(raw_env)) { + self.value.store(1, .monotonic); + return 1; + } else { + self.value.store(0, .monotonic); + return 0; + } + }, + .default_fallback => { + if (comptime ip.opts.default) |d| { + return deserAndInvalidate(d); + } + @compileError(std.fmt.comptimePrint(missing_default_fmt, .{ + ip.var_name, + "default_fallback", + })); + }, + } + } + }; + } + }; +}; + +/// Create a new environment variable definition. +/// +/// The resulting type has methods for interacting with the environment variable. +/// +/// Technically, none of the operations here are thread-safe, so writing to environment variables +/// does not guarantee that other threads will see the changes. You should avoid writing to +/// environment variables. +fn New( + comptime VariantType: type, + comptime key: [:0]const u8, + comptime opts: VariantType.CtorOptions, +) type { + return PlatformSpecificNew(VariantType, key, key, opts); +} + +/// Identical to new, except it allows you to specify different keys for POSIX and Windows. +/// +/// If the current platform does not have a key specified, all methods that attempt to read the +/// environment variable will fail at compile time, except for `platformGet` and `platformKey`, +/// which will return null instead. +fn PlatformSpecificNew( + comptime VariantType: type, + comptime posix_key: ?[:0]const u8, + comptime windows_key: ?[:0]const u8, + comptime opts: VariantType.CtorOptions, +) type { + const DefaultType = if (comptime opts.default) |d| @TypeOf(d) else void; + + const comptime_key: []const u8 = + if (posix_key) |pk| pk else if (windows_key) |wk| wk else ""; + + if (posix_key == null and windows_key == null) { + @compileError("Environment variable " ++ comptime_key ++ " has no keys for POSIX " ++ + "nor Windows specified. Provide a key for either POSIX or Windows."); + } + + const KeyType = [:0]const u8; + + // Return type as returned by each of the variants of kind. + const ValueType = VariantType.ValueType; + + // The actual return type of public methods. + const ReturnType = if (opts.default != null) ValueType else ?ValueType; + + return struct { + const Self = @This(); + + var cache: VariantType.Cache(.{ .var_name = comptime_key, .opts = opts }) = .{}; + + /// Attempt to retrieve the value of the environment variable for the current platform, if + /// the current platform has a supported definition. Returns null otherwise, unlike the + /// other methods which will fail at compile time if the platform is unsupported. + pub fn platformGet() ?ValueType { + // Get the platform-specific key + const platform_key: ?KeyType = if (comptime bun.Environment.isPosix) + posix_key + else if (comptime bun.Environment.isWindows) + windows_key + else + null; + + // If platform doesn't have a key, return null + const k = platform_key orelse return null; + + // Inline the logic from get() without calling assertPlatformSupported() + switch (cache.getCached()) { + .unknown => { + @branchHint(.unlikely); + + const env_var = bun.getenvZ(k); + const maybe_reloaded = cache.deserAndInvalidate(env_var); + + if (maybe_reloaded) |v| return v; + if (opts.default) |d| { + return d; + } + + return null; + }, + .not_set => { + if (opts.default) |d| { + return d; + } + return null; + }, + .value => |v| return v, + } + } + + /// Equal to `.platformKey()` except fails to compile if current platform is supported. + pub fn key() KeyType { + assertPlatformSupported(); + return Self.platformKey().?; + } + + /// Retrieve the key of the environment variable for the current platform, if any. + pub fn platformKey() ?KeyType { + if (bun.Environment.isPosix) { + return posix_key; + } + + if (bun.Environment.isWindows) { + return windows_key; + } + + return null; + } + + /// Retrieve the value of the environment variable, loading it if necessary. + /// Fails if the current platform is unsupported. + pub fn get() ReturnType { + assertPlatformSupported(); + + const cached_result = cache.getCached(); + + switch (cached_result) { + .unknown => { + @branchHint(.unlikely); + return getForceReload(); + }, + .not_set => { + if (opts.default) |d| { + return d; + } + return null; + }, + .value => |v| { + return v; + }, + } + } + + /// Retrieve the value of the environment variable, reloading it from the environment. + /// Fails if the current platform is unsupported. + fn getForceReload() ReturnType { + assertPlatformSupported(); + const env_var = bun.getenvZ(key()); + const maybe_reloaded = cache.deserAndInvalidate(env_var); + + if (maybe_reloaded) |v| { + return v; + } + + if (opts.default) |d| { + return d; + } + + return null; + } + + /// Fetch the default value of this environment variable, if any. + /// + /// It is safe to compare the result of .get() to default to test if the variable is set to + /// its default value. + pub const default: DefaultType = if (opts.default) |d| d else {}; + + fn assertPlatformSupported() void { + const missing_key_fmt = "Cannot retrieve the value of " ++ comptime_key ++ + " for {s} since no {s} key is associated with it."; + if (comptime bun.Environment.isWindows and windows_key == null) { + @compileError(std.fmt.comptimePrint(missing_key_fmt, .{ "Windows", "Windows" })); + } else if (comptime bun.Environment.isPosix and posix_key == null) { + @compileError(std.fmt.comptimePrint(missing_key_fmt, .{ "POSIX", "POSIX" })); + } + } + }; +} + +const FeatureFlagOpts = struct { + default: ?bool = false, +}; + +fn newFeatureFlag(comptime env_var: [:0]const u8, comptime opts: FeatureFlagOpts) type { + return New(kind.boolean, env_var, .{ .default = opts.default }); +} + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 6ee4597fe9..86deb2be6c 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -1,48 +1,5 @@ -/// All runtime feature flags that can be toggled with an environment variable. -/// The field names correspond exactly to the expected environment variable names. -pub const RuntimeFeatureFlag = enum { - BUN_ASSUME_PERFECT_INCREMENTAL, - BUN_BE_BUN, - BUN_DEBUG_NO_DUMP, - BUN_DESTRUCT_VM_ON_EXIT, - BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING, - BUN_DISABLE_SOURCE_CODE_PREVIEW, - BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW, - BUN_DUMP_STATE_ON_CRASH, - BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS, - BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG, - BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, - BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, - BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, - BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX, - BUN_FEATURE_FLAG_DISABLE_IO_POOL, - BUN_FEATURE_FLAG_DISABLE_IPV4, - BUN_FEATURE_FLAG_DISABLE_IPV6, - BUN_FEATURE_FLAG_DISABLE_REDIS_AUTO_PIPELINING, - BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK, - BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS, - BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH, - BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING, - BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE, - BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE, - BUN_FEATURE_FLAG_FORCE_IO_POOL, - BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS, - BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304, - BUN_FEATURE_FLAG_NO_LIBDEFLATE, - BUN_INSTRUMENTS, - BUN_INTERNAL_BUNX_INSTALL, - /// Suppress crash reporting and creating a core dump when we abort due to an unsupported libuv function being called - BUN_INTERNAL_SUPPRESS_CRASH_ON_UV_STUB, - /// Suppress crash reporting and creating a core dump when we abort due to a fatal Node-API error - BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT, - /// Suppress crash reporting and creating a core dump when `process._kill()` is passed its own PID - BUN_INTERNAL_SUPPRESS_CRASH_ON_PROCESS_KILL_SELF, - /// Suppress crash reporting and creating a core dump when we abort due to a signal in `bun run` - BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN, - BUN_NO_CODESIGN_MACHO_BINARY, - BUN_TRACE, - NODE_NO_WARNINGS, -}; +//! If you are adding feature-flags to this file, you are in the wrong spot. Go to env_var.zig +//! instead. /// Enable breaking changes for the next major release of Bun // TODO: Make this a CLI flag / runtime var so that we can verify disabled code paths can compile @@ -52,8 +9,6 @@ pub const breaking_changes_1_4 = false; /// This was a ~5% performance improvement pub const store_file_descriptors = !env.isBrowser; -pub const jsx_runtime_is_cjs = true; - pub const tracing = true; pub const css_supports_fence = true; @@ -68,16 +23,8 @@ pub const watch_directories = true; // This feature flag exists so when you have defines inside package.json, you can use single quotes in nested strings. pub const allow_json_single_quotes = true; -pub const react_specific_warnings = true; - pub const is_macro_enabled = !env.isWasm and !env.isWasi; -// pretend everything is always the macro environment -// useful for debugging the macro's JSX transform -pub const force_macro = false; - -pub const include_filename_in_jsx = false; - pub const disable_compression_in_http_client = false; pub const enable_keepalive = true; @@ -172,13 +119,6 @@ pub const runtime_transpiler_cache = true; /// order to isolate your bug. pub const windows_bunx_fast_path = true; -// This causes strange bugs where writing via console.log (sync) has a different -// order than via Bun.file.writer() so we turn it off until there's a unified, -// buffered writer abstraction shared throughout Bun -pub const nonblocking_stdout_and_stderr_on_posix = false; - -pub const postgresql = env.is_canary or env.isDebug; - // TODO: fix Windows-only test failures in fetch-preconnect.test.ts pub const is_fetch_preconnect_supported = env.isPosix; @@ -190,14 +130,14 @@ pub fn isLibdeflateEnabled() bool { return false; } - return !bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_NO_LIBDEFLATE); + return !bun.feature_flag.BUN_FEATURE_FLAG_NO_LIBDEFLATE.get(); } /// Enable the "app" option in Bun.serve. This option will likely be removed /// in favor of HTML loaders and configuring framework options in bunfig.toml pub fn bake() bool { // In canary or if an environment variable is specified. - return env.is_canary or env.isDebug or bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE); + return env.is_canary or env.isDebug or bun.feature_flag.BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE.get(); } /// Additional debugging features for bake.DevServer, such as the incremental visualizer. diff --git a/src/fs.zig b/src/fs.zig index 912cdbcecb..29e3a52a6f 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -536,8 +536,8 @@ pub const FileSystem = struct { return switch (Environment.os) { // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks .windows => win_tempdir_cache orelse { - const value = bun.getenvZ("TEMP") orelse bun.getenvZ("TMP") orelse brk: { - if (bun.getenvZ("SystemRoot") orelse bun.getenvZ("windir")) |windir| { + const value = bun.env_var.TEMP.get() orelse bun.env_var.TMP.get() orelse brk: { + if (bun.env_var.SYSTEMROOT.get() orelse bun.env_var.WINDIR.get()) |windir| { break :brk std.fmt.allocPrint( bun.default_allocator, "{s}\\Temp", @@ -545,7 +545,7 @@ pub const FileSystem = struct { ) catch |err| bun.handleOom(err); } - if (bun.getenvZ("USERPROFILE")) |profile| { + if (bun.env_var.HOME.get()) |profile| { var buf: bun.PathBuffer = undefined; var parts = [_]string{"AppData\\Local\\Temp"}; const out = bun.path.joinAbsStringBuf(profile, &buf, &parts, .loose); @@ -578,7 +578,7 @@ pub const FileSystem = struct { pub var tmpdir_path_set = false; pub fn tmpdirPath(_: *const @This()) []const u8 { if (!tmpdir_path_set) { - tmpdir_path = bun.getenvZ("BUN_TMPDIR") orelse bun.getenvZ("TMPDIR") orelse platformTempDir(); + tmpdir_path = bun.env_var.BUN_TMPDIR.get() orelse platformTempDir(); tmpdir_path_set = true; } @@ -587,7 +587,7 @@ pub const FileSystem = struct { pub fn openTmpDir(_: *const RealFS) !std.fs.Dir { if (!tmpdir_path_set) { - tmpdir_path = bun.getenvZ("BUN_TMPDIR") orelse bun.getenvZ("TMPDIR") orelse platformTempDir(); + tmpdir_path = bun.env_var.BUN_TMPDIR.get() orelse platformTempDir(); tmpdir_path_set = true; } @@ -636,7 +636,7 @@ pub const FileSystem = struct { } pub fn getDefaultTempDir() string { - return bun.getenvZ("BUN_TMPDIR") orelse bun.getenvZ("TMPDIR") orelse platformTempDir(); + return bun.env_var.BUN_TMPDIR.get() orelse platformTempDir(); } pub fn setTempdir(path: ?string) void { diff --git a/src/http/websocket_client/WebSocketDeflate.zig b/src/http/websocket_client/WebSocketDeflate.zig index 6521669279..f03ddba3b0 100644 --- a/src/http/websocket_client/WebSocketDeflate.zig +++ b/src/http/websocket_client/WebSocketDeflate.zig @@ -129,7 +129,7 @@ pub fn deinit(self: *PerMessageDeflate) void { } fn canUseLibDeflate(len: usize) bool { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_NO_LIBDEFLATE)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_NO_LIBDEFLATE.get()) { return false; } diff --git a/src/install/NetworkTask.zig b/src/install/NetworkTask.zig index 4401cff736..9462007e9f 100644 --- a/src/install/NetworkTask.zig +++ b/src/install/NetworkTask.zig @@ -234,7 +234,7 @@ pub fn forManifest( } // Incase the ETag causes invalidation, we fallback to the last modified date. - if (last_modified.len != 0 and bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304)) { + if (last_modified.len != 0 and bun.feature_flag.BUN_FEATURE_FLAG_LAST_MODIFIED_PRETEND_304.get()) { this.unsafe_http_client.client.flags.force_last_modified = true; this.unsafe_http_client.client.if_modified_since = last_modified; } diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index 41ebb0629f..308f1b9a22 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -791,7 +791,8 @@ pub fn init( try env.load(entries_option.entries, &[_][]u8{}, .production, false); initializeStore(); - if (bun.getenvZ("XDG_CONFIG_HOME") orelse bun.getenvZ(bun.DotEnv.home_env)) |data_dir| { + + if (bun.env_var.XDG_CONFIG_HOME.get() orelse bun.env_var.HOME.get()) |data_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ "./.npmrc", @@ -831,7 +832,7 @@ pub fn init( bun.spawn.process.WaiterThread.setShouldUseWaiterThread(); } - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS.get()) { bun.sys.WindowsSymlinkOptions.has_failed_to_create_symlink = true; } diff --git a/src/install/PackageManager/PackageManagerDirectories.zig b/src/install/PackageManager/PackageManagerDirectories.zig index 3f83c43ce3..ed24ca03e3 100644 --- a/src/install/PackageManager/PackageManagerDirectories.zig +++ b/src/install/PackageManager/PackageManagerDirectories.zig @@ -165,12 +165,12 @@ pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader, options: ?*const Options) Ca return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } - if (env.get("XDG_CACHE_HOME")) |dir| { + if (bun.env_var.XDG_CACHE_HOME.get()) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } - if (env.get(bun.DotEnv.home_env)) |dir| { + if (bun.env_var.HOME.get()) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } diff --git a/src/install/PackageManager/PackageManagerLifecycle.zig b/src/install/PackageManager/PackageManagerLifecycle.zig index 986a010029..3ec8dfd2c8 100644 --- a/src/install/PackageManager/PackageManagerLifecycle.zig +++ b/src/install/PackageManager/PackageManagerLifecycle.zig @@ -176,7 +176,7 @@ pub fn sleep(this: *PackageManager) void { pub fn reportSlowLifecycleScripts(this: *PackageManager) void { const log_level = this.options.log_level; if (log_level == .silent) return; - if (bun.getRuntimeFeatureFlag(.BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING)) { + if (bun.feature_flag.BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING.get()) { return; } diff --git a/src/install/PackageManager/PackageManagerOptions.zig b/src/install/PackageManager/PackageManagerOptions.zig index 32ae941e07..ab131760fc 100644 --- a/src/install/PackageManager/PackageManagerOptions.zig +++ b/src/install/PackageManager/PackageManagerOptions.zig @@ -171,7 +171,7 @@ pub const Update = struct { }; pub fn openGlobalDir(explicit_global_dir: string) !std.fs.Dir { - if (bun.getenvZ("BUN_INSTALL_GLOBAL_DIR")) |home_dir| { + if (bun.env_var.BUN_INSTALL_GLOBAL_DIR.get()) |home_dir| { return try std.fs.cwd().makeOpenPath(home_dir, .{}); } @@ -179,34 +179,25 @@ pub fn openGlobalDir(explicit_global_dir: string) !std.fs.Dir { return try std.fs.cwd().makeOpenPath(explicit_global_dir, .{}); } - if (bun.getenvZ("BUN_INSTALL")) |home_dir| { + if (bun.env_var.BUN_INSTALL.get()) |home_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ "install", "global" }; const path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); return try std.fs.cwd().makeOpenPath(path, .{}); } - if (!Environment.isWindows) { - if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.getenvZ("HOME")) |home_dir| { - var buf: bun.PathBuffer = undefined; - var parts = [_]string{ ".bun", "install", "global" }; - const path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); - return try std.fs.cwd().makeOpenPath(path, .{}); - } - } else { - if (bun.getenvZ("USERPROFILE")) |home_dir| { - var buf: bun.PathBuffer = undefined; - var parts = [_]string{ ".bun", "install", "global" }; - const path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); - return try std.fs.cwd().makeOpenPath(path, .{}); - } + if (bun.env_var.XDG_CACHE_HOME.get() orelse bun.env_var.HOME.get()) |home_dir| { + var buf: bun.PathBuffer = undefined; + var parts = [_]string{ ".bun", "install", "global" }; + const path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); + return try std.fs.cwd().makeOpenPath(path, .{}); } return error.@"No global directory found"; } pub fn openGlobalBinDir(opts_: ?*const Api.BunInstall) !std.fs.Dir { - if (bun.getenvZ("BUN_INSTALL_BIN")) |home_dir| { + if (bun.env_var.BUN_INSTALL_BIN.get()) |home_dir| { return try std.fs.cwd().makeOpenPath(home_dir, .{}); } @@ -218,7 +209,7 @@ pub fn openGlobalBinDir(opts_: ?*const Api.BunInstall) !std.fs.Dir { } } - if (bun.getenvZ("BUN_INSTALL")) |home_dir| { + if (bun.env_var.BUN_INSTALL.get()) |home_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ "bin", @@ -227,7 +218,7 @@ pub fn openGlobalBinDir(opts_: ?*const Api.BunInstall) !std.fs.Dir { return try std.fs.cwd().makeOpenPath(path, .{}); } - if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + if (bun.env_var.XDG_CACHE_HOME.get() orelse bun.env_var.HOME.get()) |home_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ ".bun", @@ -751,7 +742,6 @@ const std = @import("std"); const bun = @import("bun"); const DotEnv = bun.DotEnv; -const Environment = bun.Environment; const FD = bun.FD; const OOM = bun.OOM; const Output = bun.Output; diff --git a/src/install/PackageManager/patchPackage.zig b/src/install/PackageManager/patchPackage.zig index 6221283813..706c1e4418 100644 --- a/src/install/PackageManager/patchPackage.zig +++ b/src/install/PackageManager/patchPackage.zig @@ -320,7 +320,7 @@ pub fn doPatchCommit( }, }; var gitbuf: bun.PathBuffer = undefined; - const git = bun.which(&gitbuf, bun.getenvZ("PATH") orelse "", cwd, "git") orelse { + const git = bun.which(&gitbuf, bun.env_var.PATH.get() orelse "", cwd, "git") orelse { Output.prettyError( "error: git must be installed to use `bun patch --commit` \n", .{}, diff --git a/src/install/PackageManager/updatePackageJSONAndInstall.zig b/src/install/PackageManager/updatePackageJSONAndInstall.zig index f2932e0349..8f1e9815ff 100644 --- a/src/install/PackageManager/updatePackageJSONAndInstall.zig +++ b/src/install/PackageManager/updatePackageJSONAndInstall.zig @@ -569,7 +569,7 @@ fn updatePackageJSONAndInstallAndCLI( if (manager.options.global) { if (manager.options.bin_path.len > 0 and manager.track_installed_bin == .basename) { var path_buf: bun.PathBuffer = undefined; - const needs_to_print = if (bun.getenvZ("PATH")) |PATH| + const needs_to_print = if (bun.env_var.PATH.get()) |PATH| // This is not perfect // // If you already have a different binary of the same @@ -667,7 +667,7 @@ fn updatePackageJSONAndInstallAndCLI( , .{ bun.fmt.quote(manager.track_installed_bin.basename), - MoreInstructions{ .shell = bun.cli.ShellCompletions.Shell.fromEnv([]const u8, bun.getenvZ("SHELL") orelse ""), .folder = manager.options.bin_path }, + MoreInstructions{ .shell = bun.cli.ShellCompletions.Shell.fromEnv([]const u8, bun.env_var.SHELL.platformGet() orelse ""), .folder = manager.options.bin_path }, }, ); Output.flush(); diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 5770c5d4f9..c0e98e3e66 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -495,7 +495,7 @@ fn extract(this: *const ExtractTarball, log: *logger.Log, tgz_bytes: []const u8) }; } - if (!bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX)) { + if (!bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX.get()) { // create an index storing each version of a package installed if (strings.indexOfChar(basename, '/') == null) create_index: { const dest_name = switch (this.resolution.tag) { diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 3f4a5c7313..38679c7c3e 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -313,7 +313,7 @@ pub fn loadFromDir( switch (result) { .ok => { - if (bun.getenvZ("BUN_DEBUG_TEST_TEXT_LOCKFILE") != null and manager != null) { + if (bun.env_var.BUN_DEBUG_TEST_TEXT_LOCKFILE.get() and manager != null) { // Convert the loaded binary lockfile into a text lockfile in memory, then // parse it back into a binary lockfile. diff --git a/src/install/repository.zig b/src/install/repository.zig index 59feeef8b6..0341f46420 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -16,21 +16,10 @@ const SloppyGlobalGitConfig = struct { } pub fn loadAndParse() void { - const home_dir_path = brk: { - if (comptime Environment.isWindows) { - if (bun.getenvZ("USERPROFILE")) |env| - break :brk env; - } else { - if (bun.getenvZ("HOME")) |env| - break :brk env; - } - - // won't find anything - return; - }; + const home_dir = bun.env_var.HOME.get() orelse return; var config_file_path_buf: bun.PathBuffer = undefined; - const config_file_path = bun.path.joinAbsStringBufZ(home_dir_path, &config_file_path_buf, &.{".gitconfig"}, .auto); + const config_file_path = bun.path.joinAbsStringBufZ(home_dir, &config_file_path_buf, &.{".gitconfig"}, .auto); var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); const source = File.toSource(config_file_path, allocator, .{ .convert_bom = true }).unwrap() catch { diff --git a/src/interchange/yaml.zig b/src/interchange/yaml.zig index 947307c874..5a899df9d4 100644 --- a/src/interchange/yaml.zig +++ b/src/interchange/yaml.zig @@ -4758,7 +4758,7 @@ pub fn Parser(comptime enc: Encoding) type { return this.str.len(); } - pub fn done(self: *const @This()) String { + pub fn done(self: *@This()) String { self.parser.whitespace_buf.clearRetainingCapacity(); return self.str; } diff --git a/src/linux.zig b/src/linux.zig index afe636afe4..86623f28b7 100644 --- a/src/linux.zig +++ b/src/linux.zig @@ -45,7 +45,7 @@ pub const RWFFlagSupport = enum(u8) { if (comptime !bun.Environment.isLinux) return false; switch (rwf_bool.load(.monotonic)) { .unknown => { - if (isLinuxKernelVersionWithBuggyRWF_NONBLOCK() or bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK)) { + if (isLinuxKernelVersionWithBuggyRWF_NONBLOCK() or bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK.get()) { rwf_bool.store(.unsupported, .monotonic); return false; } diff --git a/src/macho.zig b/src/macho.zig index a46fbba7cc..51feef8781 100644 --- a/src/macho.zig +++ b/src/macho.zig @@ -190,7 +190,7 @@ pub const MachoFile = struct { linkedit_seg.fileoff += @as(usize, @intCast(size_diff)); linkedit_seg.vmaddr += @as(usize, @intCast(size_diff)); - if (self.header.cputype == macho.CPU_TYPE_ARM64 and !bun.getRuntimeFeatureFlag(.BUN_NO_CODESIGN_MACHO_BINARY)) { + if (self.header.cputype == macho.CPU_TYPE_ARM64 and !bun.feature_flag.BUN_NO_CODESIGN_MACHO_BINARY.get()) { // We also update the sizes of the LINKEDIT segment to account for the hashes we're adding linkedit_seg.filesize += @as(usize, @intCast(size_of_new_hashes)); linkedit_seg.vmsize += @as(usize, @intCast(size_of_new_hashes)); @@ -341,7 +341,7 @@ pub const MachoFile = struct { } pub fn buildAndSign(self: *MachoFile, writer: anytype) !void { - if (self.header.cputype == macho.CPU_TYPE_ARM64 and !bun.getRuntimeFeatureFlag(.BUN_NO_CODESIGN_MACHO_BINARY)) { + if (self.header.cputype == macho.CPU_TYPE_ARM64 and !bun.feature_flag.BUN_NO_CODESIGN_MACHO_BINARY.get()) { var data = std.ArrayList(u8).init(self.allocator); defer data.deinit(); try self.build(data.writer()); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index a2109168a1..b51de870e1 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -1361,7 +1361,7 @@ pub export fn napi_internal_register_cleanup_zig(env_: napi_env) void { } pub export fn napi_internal_suppress_crash_on_abort_if_desired() void { - if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT)) { + if (bun.feature_flag.BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT.get()) { bun.crash_handler.suppressReporting(); } } diff --git a/src/output.zig b/src/output.zig index 6d1f79d90a..cb6ecd4a41 100644 --- a/src/output.zig +++ b/src/output.zig @@ -80,31 +80,22 @@ pub const Source = struct { } pub fn isNoColor() bool { - const no_color = bun.getenvZ("NO_COLOR") orelse return false; - // https://no-color.org/ - // "when present and not an empty string (regardless of its value)" - return no_color.len != 0; + return bun.env_var.NO_COLOR.get(); } pub fn getForceColorDepth() ?ColorDepth { - const force_color = bun.getenvZ("FORCE_COLOR") orelse return null; + const force_color = bun.env_var.FORCE_COLOR.get() orelse return null; // Supported by Node.js, if set will ignore NO_COLOR. // - "0" to indicate no color support // - "1", "true", or "" to indicate 16-color support // - "2" to indicate 256-color support // - "3" to indicate 16 million-color support - if (strings.eqlComptime(force_color, "1") or strings.eqlComptime(force_color, "true") or strings.eqlComptime(force_color, "")) { - return ColorDepth.@"16"; - } - - if (strings.eqlComptime(force_color, "2")) { - return ColorDepth.@"256"; - } - if (strings.eqlComptime(force_color, "3")) { - return ColorDepth.@"16m"; - } - - return ColorDepth.none; + return switch (force_color) { + 0 => .none, + 1 => .@"16", + 2 => .@"256", + else => .@"16m", + }; } pub fn isForceColor() bool { @@ -273,29 +264,22 @@ pub const Source = struct { return; } - const term = bun.getenvZ("TERM") orelse ""; + const term = bun.env_var.TERM.get() orelse ""; if (strings.eqlComptime(term, "dumb")) { return; } - if (bun.getenvZ("TMUX") != null) { + if (bun.env_var.TMUX.get() != null) { lazy_color_depth = .@"256"; return; } - if (bun.getenvZ("CI")) |ci| { - inline for (.{ "APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS" }) |ci_env| { - if (strings.eqlComptime(ci, ci_env)) { - lazy_color_depth = .@"256"; - return; - } - } - + if (bun.env_var.CI.get() != null) { lazy_color_depth = .@"16"; return; } - if (bun.getenvZ("TERM_PROGRAM")) |term_program| { + if (bun.env_var.TERM_PROGRAM.get()) |term_program| { const use_16m = .{ "ghostty", "MacTerm", @@ -313,7 +297,7 @@ pub const Source = struct { var has_color_term_set = false; - if (bun.getenvZ("COLORTERM")) |color_term| { + if (bun.env_var.COLORTERM.get()) |color_term| { if (strings.eqlComptime(color_term, "truecolor") or strings.eqlComptime(color_term, "24bit")) { lazy_color_depth = .@"16m"; return; @@ -450,10 +434,9 @@ pub inline fn isEmojiEnabled() bool { } pub fn isGithubAction() bool { - if (bun.getenvZ("GITHUB_ACTIONS")) |value| { - return strings.eqlComptime(value, "true") and - // Do not print github annotations for AI agents because that wastes the context window. - !isAIAgent(); + if (bun.env_var.GITHUB_ACTIONS.get()) { + // Do not print github annotations for AI agents because that wastes the context window. + return !isAIAgent(); } return false; } @@ -462,7 +445,7 @@ pub fn isAIAgent() bool { const get_is_agent = struct { var value = false; fn evaluate() bool { - if (bun.getenvZ("AGENT")) |env| { + if (bun.env_var.AGENT.get()) |env| { return strings.eqlComptime(env, "1"); } @@ -471,12 +454,12 @@ pub fn isAIAgent() bool { } // Claude Code. - if (bun.getenvTruthy("CLAUDECODE")) { + if (bun.env_var.CLAUDECODE.get()) { return true; } // Replit. - if (bun.getenvTruthy("REPL_ID")) { + if (bun.env_var.REPL_ID.get()) { return true; } @@ -509,12 +492,7 @@ pub fn isAIAgent() bool { pub fn isVerbose() bool { // Set by Github Actions when a workflow is run using debug mode. - if (bun.getenvZ("RUNNER_DEBUG")) |value| { - if (strings.eqlComptime(value, "1")) { - return true; - } - } - return false; + return bun.env_var.RUNNER_DEBUG.get(); } pub fn enableBuffering() void { @@ -826,10 +804,10 @@ fn ScopedLogger(comptime tagname: []const u8, comptime visibility: Visibility) t fn evaluateIsVisible() void { if (bun.getenvZAnyCase("BUN_DEBUG_" ++ tagname)) |val| { really_disable.store(strings.eqlComptime(val, "0"), .monotonic); - } else if (bun.getenvZAnyCase("BUN_DEBUG_ALL")) |val| { - really_disable.store(strings.eqlComptime(val, "0"), .monotonic); - } else if (bun.getenvZAnyCase("BUN_DEBUG_QUIET_LOGS")) |val| { - really_disable.store(really_disable.load(.monotonic) or !strings.eqlComptime(val, "0"), .monotonic); + } else if (bun.env_var.BUN_DEBUG_ALL.get()) |val| { + really_disable.store(val, .monotonic); + } else if (bun.env_var.BUN_DEBUG_QUIET_LOGS.get()) |val| { + really_disable.store(really_disable.load(.monotonic) or !val, .monotonic); } else { for (bun.argv) |arg| { if (strings.eqlCaseInsensitiveASCII(arg, comptime "--debug-" ++ tagname, true)) { @@ -1266,7 +1244,7 @@ extern "c" fn getpid() c_int; pub fn initScopedDebugWriterAtStartup() void { bun.debugAssert(source_set); - if (bun.getenvZ("BUN_DEBUG")) |path| { + if (bun.env_var.BUN_DEBUG.get()) |path| { if (path.len > 0 and !strings.eql(path, "0") and !strings.eql(path, "false")) { if (std.fs.path.dirname(path)) |dir| { std.fs.cwd().makePath(dir) catch {}; diff --git a/src/patch.zig b/src/patch.zig index 2143a8b4f8..95923d05ad 100644 --- a/src/patch.zig +++ b/src/patch.zig @@ -1267,7 +1267,7 @@ pub fn spawnOpts( "XDG_CONFIG_HOME", "USERPROFILE", }; - const PATH = bun.getenvZ("PATH"); + const PATH = bun.env_var.PATH.get(); const envp_buf = bun.handleOom(bun.default_allocator.allocSentinel(?[*:0]const u8, env_arr.len + @as(usize, if (PATH != null) 1 else 0), null)); for (0..env_arr.len) |i| { envp_buf[i] = env_arr[i].ptr; @@ -1392,7 +1392,7 @@ pub fn gitDiffInternal( child_proc.stderr_behavior = .Pipe; var map = std.process.EnvMap.init(allocator); defer map.deinit(); - if (bun.getenvZ("PATH")) |v| try map.put("PATH", v); + if (bun.env_var.PATH.get()) |v| try map.put("PATH", v); try map.put("GIT_CONFIG_NOSYSTEM", "1"); try map.put("HOME", ""); try map.put("XDG_CONFIG_HOME", ""); diff --git a/src/perf.zig b/src/perf.zig index 04a608040f..ba270fabe9 100644 --- a/src/perf.zig +++ b/src/perf.zig @@ -19,13 +19,13 @@ pub const Ctx = union(enum) { var is_enabled_once = std.once(isEnabledOnce); var is_enabled = std.atomic.Value(bool).init(false); fn isEnabledOnMacOSOnce() void { - if (bun.getenvZ("DYLD_ROOT_PATH") != null or bun.getRuntimeFeatureFlag(.BUN_INSTRUMENTS)) { + if (bun.env_var.DYLD_ROOT_PATH.get() != null or bun.feature_flag.BUN_INSTRUMENTS.get()) { is_enabled.store(true, .seq_cst); } } fn isEnabledOnLinuxOnce() void { - if (bun.getRuntimeFeatureFlag(.BUN_TRACE)) { + if (bun.feature_flag.BUN_TRACE.get()) { is_enabled.store(true, .seq_cst); } } diff --git a/src/shell/Builtin.zig b/src/shell/Builtin.zig index 8578485590..aaa5671c8d 100644 --- a/src/shell/Builtin.zig +++ b/src/shell/Builtin.zig @@ -112,7 +112,7 @@ pub const Kind = enum { } fn forceEnableOnPosix() bool { - return bun.getRuntimeFeatureFlag(.BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS); + return bun.feature_flag.BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS.get(); } pub fn fromStr(str: []const u8) ?Builtin.Kind { diff --git a/src/sql/mysql/MySQLRequestQueue.zig b/src/sql/mysql/MySQLRequestQueue.zig index 15d87a306d..1606e70637 100644 --- a/src/sql/mysql/MySQLRequestQueue.zig +++ b/src/sql/mysql/MySQLRequestQueue.zig @@ -32,7 +32,7 @@ pub inline fn markAsPrepared(this: *@This()) void { } } pub inline fn canPipeline(this: *@This(), connection: *MySQLConnection) bool { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING.get()) { @branchHint(.unlikely); return false; } diff --git a/src/sql/postgres/DebugSocketMonitorReader.zig b/src/sql/postgres/DebugSocketMonitorReader.zig index 1af82ce043..d8444ffd89 100644 --- a/src/sql/postgres/DebugSocketMonitorReader.zig +++ b/src/sql/postgres/DebugSocketMonitorReader.zig @@ -3,7 +3,7 @@ pub var enabled = false; pub var check = std.once(load); pub fn load() void { - if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR_READER")) |monitor| { + if (bun.env_var.BUN_POSTGRES_SOCKET_MONITOR_READER.get()) |monitor| { enabled = true; file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { enabled = false; diff --git a/src/sql/postgres/DebugSocketMonitorWriter.zig b/src/sql/postgres/DebugSocketMonitorWriter.zig index c721cdd2ac..8301d17a2b 100644 --- a/src/sql/postgres/DebugSocketMonitorWriter.zig +++ b/src/sql/postgres/DebugSocketMonitorWriter.zig @@ -7,7 +7,7 @@ pub fn write(data: []const u8) void { } pub fn load() void { - if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR")) |monitor| { + if (bun.env_var.BUN_POSTGRES_SOCKET_MONITOR.get()) |monitor| { enabled = true; file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch { enabled = false; diff --git a/src/sql/postgres/PostgresSQLConnection.zig b/src/sql/postgres/PostgresSQLConnection.zig index 4f4787de42..6ed3e6c030 100644 --- a/src/sql/postgres/PostgresSQLConnection.zig +++ b/src/sql/postgres/PostgresSQLConnection.zig @@ -984,7 +984,7 @@ pub fn hasQueryRunning(this: *PostgresSQLConnection) bool { } pub fn canPipeline(this: *PostgresSQLConnection) bool { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING.get()) { @branchHint(.unlikely); return false; } diff --git a/src/tracy.zig b/src/tracy.zig index 9963250c04..475bcf8a8d 100644 --- a/src/tracy.zig +++ b/src/tracy.zig @@ -528,7 +528,7 @@ fn dlsym(comptime Type: type, comptime symbol: [:0]const u8) ?Type { const RLTD: std.c.RTLD = if (bun.Environment.isMac) @bitCast(@as(i32, -2)) else if (bun.Environment.isLinux) .{} else {}; - if (bun.getenvZ("BUN_TRACY_PATH")) |path| { + if (bun.env_var.BUN_TRACY_PATH.get()) |path| { const handle = bun.sys.dlopen(&(std.posix.toPosixPath(path) catch unreachable), RLTD); if (handle != null) { Handle.handle = handle; diff --git a/src/transpiler.zig b/src/transpiler.zig index 668ee1a978..2ea1d4df14 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -902,7 +902,7 @@ pub const Transpiler = struct { comptime format: js_printer.Format, handler: js_printer.SourceMapHandler, ) !usize { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS)) { + if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS.get()) { return transpiler.printWithSourceMapMaybe( result.ast, &result.source, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index bb24dbc771..02ad4e9ef3 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1583,7 +1583,7 @@ fn SocketHandler(comptime ssl: bool) type { const Options = struct { pub fn fromJS(globalObject: *jsc.JSGlobalObject, options_obj: jsc.JSValue) !valkey.Options { var this = valkey.Options{ - .enable_auto_pipelining = !bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_REDIS_AUTO_PIPELINING), + .enable_auto_pipelining = !bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_REDIS_AUTO_PIPELINING.get(), }; if (try options_obj.getOptionalInt(globalObject, "idleTimeout", u32)) |idle_timeout| { diff --git a/src/watcher/INotifyWatcher.zig b/src/watcher/INotifyWatcher.zig index b4996fa5b2..96d79fa681 100644 --- a/src/watcher/INotifyWatcher.zig +++ b/src/watcher/INotifyWatcher.zig @@ -94,9 +94,7 @@ pub fn init(this: *INotifyWatcher, _: []const u8) !void { bun.assert(!this.loaded); this.loaded = true; - if (bun.getenvZ("BUN_INOTIFY_COALESCE_INTERVAL")) |env| { - this.coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000; - } + this.coalesce_interval = std.math.cast(isize, bun.env_var.BUN_INOTIFY_COALESCE_INTERVAL.get()) orelse 100_000; // TODO: convert to bun.sys.Error this.fd = .fromNative(try std.posix.inotify_init1(IN.CLOEXEC)); diff --git a/src/watcher/WatcherTrace.zig b/src/watcher/WatcherTrace.zig index d2beeb1e4e..cd01ba2969 100644 --- a/src/watcher/WatcherTrace.zig +++ b/src/watcher/WatcherTrace.zig @@ -6,7 +6,7 @@ var trace_file: ?bun.sys.File = null; pub fn init() void { if (trace_file != null) return; - if (bun.getenvZ("BUN_WATCHER_TRACE")) |trace_path| { + if (bun.env_var.BUN_WATCHER_TRACE.get()) |trace_path| { if (trace_path.len > 0) { const flags = bun.O.WRONLY | bun.O.CREAT | bun.O.APPEND; const mode = 0o644; diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index 9edd2b793a..7cac923618 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -36,7 +36,7 @@ "std.enums.tagName(": 2, "std.fs.Dir": 164, "std.fs.File": 62, - "std.fs.cwd": 103, + "std.fs.cwd": 102, "std.log": 1, "std.mem.indexOfAny(u8": 0, "std.unicode": 27, From ab1395d38ebc3a90a7898f9af8c812e339403221 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Fri, 24 Oct 2025 11:11:20 -0800 Subject: [PATCH 236/391] zig: env_var: fix output port (#24026) --- src/output.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/output.zig b/src/output.zig index cb6ecd4a41..e5dc464b2b 100644 --- a/src/output.zig +++ b/src/output.zig @@ -805,9 +805,9 @@ fn ScopedLogger(comptime tagname: []const u8, comptime visibility: Visibility) t if (bun.getenvZAnyCase("BUN_DEBUG_" ++ tagname)) |val| { really_disable.store(strings.eqlComptime(val, "0"), .monotonic); } else if (bun.env_var.BUN_DEBUG_ALL.get()) |val| { - really_disable.store(val, .monotonic); + really_disable.store(!val, .monotonic); } else if (bun.env_var.BUN_DEBUG_QUIET_LOGS.get()) |val| { - really_disable.store(really_disable.load(.monotonic) or !val, .monotonic); + really_disable.store(really_disable.load(.monotonic) or val, .monotonic); } else { for (bun.argv) |arg| { if (strings.eqlCaseInsensitiveASCII(arg, comptime "--debug-" ++ tagname, true)) { From 0dd6aa47ea149e949c72646ba916e5723850da44 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Oct 2025 14:14:15 -0700 Subject: [PATCH 237/391] Replace panic with debug warn Closes https://github.com/oven-sh/bun/pull/24025 --- src/env_var.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/env_var.zig b/src/env_var.zig index 99b35005f3..49d034f755 100644 --- a/src/env_var.zig +++ b/src/env_var.zig @@ -342,8 +342,8 @@ const kind = struct { deser: struct { /// Control how deserializing and deserialization errors are handled. error_handling: enum { - /// panic on deserialization errors. - panic, + /// debug_warn on deserialization errors. + debug_warn, /// Ignore deserialization errors and treat the variable as not set. not_set, /// Fallback to default. @@ -356,7 +356,7 @@ const kind = struct { /// Note: Most values are considered truthy, except for "", "0", "false", "no", /// and "off". truthy_cast, - } = .panic, + } = .debug_warn, /// Control what empty strings are treated as. empty_string_as: union(enum) { @@ -444,8 +444,10 @@ const kind = struct { "fallback to default on {s}, but no default is set."; switch (ip.opts.deser.error_handling) { - .panic => { - bun.Output.panic(fmt, .{ ip.var_name, raw_env }); + .debug_warn => { + bun.Output.debugWarn(fmt, .{ ip.var_name, raw_env }); + self.value.store(not_set_sentinel, .monotonic); + return null; }, .not_set => { self.value.store(not_set_sentinel, .monotonic); From afd125fc12ac6a9b3163f28d8f0f4e5d809eb820 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 24 Oct 2025 14:43:05 -0700 Subject: [PATCH 238/391] docs(env_var): document silent error handling behavior (#24043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What does this PR do? This PR adds documentation comments to `src/env_var.zig` that explain the silent error handling behavior for environment variable deserialization, based on the documentation from the closed PR #24036. The comments clarify: 1. **Module-level documentation**: Environment variables may fail to parse silently. When they do, the default behavior is to show a debug warning and treat them as not set. This is intentional to avoid panics from environment variable pollution. 2. **Inline documentation**: Deserialization errors cannot panic. Users needing more robust configuration mechanisms should consider alternatives to environment variables. This documentation complements the behavior change introduced in commit 0dd6aa47ea which replaced panic with debug_warn. ### How did you verify your code works? Ran `bun bd` successfully - the build completed without errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/env_var.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/env_var.zig b/src/env_var.zig index 49d034f755..cdd75eda12 100644 --- a/src/env_var.zig +++ b/src/env_var.zig @@ -8,6 +8,12 @@ //! If default values are provided, the .get() method is guaranteed not to return a nullable type, //! whereas if no default is provided, the .get() method will return an optional type. //! +//! Note that environment variables may fail to parse silently. If they do fail to parse, the +//! default is to show a debug warning and treat them as not set. This behavior can be customized, +//! but environment variables are not meant to be a robust configuration mechanism. If you do think +//! your feature needs more customization, consider using other means. The reason we have decided +//! upon this behavior is to avoid panics due to environment variable pollution. +//! //! TODO(markovejnovic): It would be neat if this library supported loading floats as //! well as strings, integers and booleans, but for now this will do. //! @@ -341,6 +347,9 @@ const kind = struct { default: ?ValueType = null, deser: struct { /// Control how deserializing and deserialization errors are handled. + /// + /// Note that deserialization errors cannot panic. If you need more robust means of + /// handling inputs, consider not using environment variables. error_handling: enum { /// debug_warn on deserialization errors. debug_warn, From a3f18b9e0e2f0624846109bc82f21e9ec2ba553a Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 24 Oct 2025 19:07:40 -0700 Subject: [PATCH 239/391] feat(test): implement onTestFinished hook for bun:test (#24038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements `onTestFinished()` for `bun:test`, which runs after all `afterEach` hooks have completed. ## Implementation - Added `onTestFinished` export to the test module in `jest.zig` - Modified `genericHook` in `bun_test.zig` to handle `onTestFinished` as a special case that: - Can only be called inside a test (not in describe blocks or preload) - Appends hooks at the very end of the execution sequence - Added comprehensive tests covering basic ordering, multiple callbacks, async callbacks, and interaction with other hooks ## Execution Order When called inside a test: 1. Test body executes 2. `afterAll` hooks (if added inside the test) 3. `afterEach` hooks 4. `onTestFinished` hooks ✨ ## Test Plan - ✅ All new tests pass with `bun bd test` - ✅ Tests correctly fail with `USE_SYSTEM_BUN=1` (feature not in released version) - ✅ Verifies correct ordering with `afterEach`, `afterAll`, and multiple `onTestFinished` calls - ✅ Tests async `onTestFinished` callbacks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: pfg --- docs/cli/test.md | 13 +- packages/bun-types/test.d.ts | 22 ++++ src/bun.js/test/bun_test.zig | 58 +++++---- src/bun.js/test/jest.zig | 3 +- .../js/bun/test/test-on-test-finished.test.ts | 116 ++++++++++++++++++ 5 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 test/js/bun/test/test-on-test-finished.test.ts diff --git a/docs/cli/test.md b/docs/cli/test.md index f266166a59..476b7c5a39 100644 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -257,12 +257,13 @@ $ bun test --watch Bun supports the following lifecycle hooks: -| Hook | Description | -| ------------ | --------------------------- | -| `beforeAll` | Runs once before all tests. | -| `beforeEach` | Runs before each test. | -| `afterEach` | Runs after each test. | -| `afterAll` | Runs once after all tests. | +| Hook | Description | +| ---------------- | -------------------------------------------------------- | +| `beforeAll` | Runs once before all tests. | +| `beforeEach` | Runs before each test. | +| `afterEach` | Runs after each test. | +| `afterAll` | Runs once after all tests. | +| `onTestFinished` | Runs after a test finishes, including after `afterEach`. | These hooks can be defined inside test files, or in a separate file that is preloaded with the `--preload` flag. diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index ca5aa18aea..e37c1b0fc7 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -358,6 +358,28 @@ declare module "bun:test" { fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), options?: HookOptions, ): void; + /** + * Runs a function after a test finishes, including after all afterEach hooks. + * + * This is useful for cleanup tasks that need to run at the very end of a test, + * after all other hooks have completed. + * + * Can only be called inside a test, not in describe blocks. + * + * @example + * test("my test", () => { + * onTestFinished(() => { + * // This runs after all afterEach hooks + * console.log("Test finished!"); + * }); + * }); + * + * @param fn the function to run + */ + export function onTestFinished( + fn: (() => void | Promise) | ((done: (err?: unknown) => void) => void), + options?: HookOptions, + ): void; /** * Sets the default timeout for all tests in the current file. If a test specifies a timeout, it will * override this value. The default timeout is 5000ms (5 seconds). diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 61bae4e157..34fd701ccb 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -56,6 +56,9 @@ pub const js_fns = struct { .timeout = args.options.timeout, }; const bunTest = bunTestRoot.getActiveFileUnlessInPreload(globalThis.bunVM()) orelse { + if (tag == .onTestFinished) { + return globalThis.throw("Cannot call {s}() in preload. It can only be called inside a test.", .{@tagName(tag)}); + } group.log("genericHook in preload", .{}); _ = try bunTestRoot.hook_scope.appendHook(bunTestRoot.gpa, tag, args.callback, cfg, .{}, .preload); @@ -64,36 +67,49 @@ pub const js_fns = struct { switch (bunTest.phase) { .collection => { + if (tag == .onTestFinished) { + return globalThis.throw("Cannot call {s}() outside of a test. It can only be called inside a test.", .{@tagName(tag)}); + } _ = try bunTest.collection.active_scope.appendHook(bunTest.gpa, tag, args.callback, cfg, .{}, .collection); return .js_undefined; }, .execution => { - if (tag == .afterAll or tag == .afterEach) { - // allowed - const active = bunTest.getCurrentStateData(); - const sequence, _ = bunTest.execution.getCurrentAndValidExecutionSequence(active) orelse { - return globalThis.throw("Cannot call {s}() here. It cannot be called inside a concurrent test. Call it inside describe() instead.", .{@tagName(tag)}); - }; - var append_point = sequence.active_entry; + const active = bunTest.getCurrentStateData(); + const sequence, _ = bunTest.execution.getCurrentAndValidExecutionSequence(active) orelse { + const message = if (tag == .onTestFinished) + "Cannot call {s}() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent." + else + "Cannot call {s}() here. It cannot be called inside a concurrent test. Call it inside describe() instead."; + return globalThis.throw(message, .{@tagName(tag)}); + }; - var iter = append_point; - const before_test_entry = while (iter) |entry| : (iter = entry.next) { - if (entry == sequence.test_entry) break true; - } else false; + const append_point = switch (tag) { + .afterAll, .afterEach => blk: { + var iter = sequence.active_entry; + while (iter) |entry| : (iter = entry.next) { + if (entry == sequence.test_entry) break :blk sequence.test_entry.?; + } - if (before_test_entry) append_point = sequence.test_entry; + break :blk sequence.active_entry orelse return globalThis.throw("Cannot call {s}() here. Call it inside describe() instead.", .{@tagName(tag)}); + }, + .onTestFinished => blk: { + // Find the last entry in the sequence + var last_entry = sequence.active_entry orelse return globalThis.throw("Cannot call {s}() here. Call it inside a test instead.", .{@tagName(tag)}); + while (last_entry.next) |next_entry| { + last_entry = next_entry; + } + break :blk last_entry; + }, + else => return globalThis.throw("Cannot call {s}() inside a test. Call it inside describe() instead.", .{@tagName(tag)}), + }; - const append_point_value = append_point orelse return globalThis.throw("Cannot call {s}() here. Call it inside describe() instead.", .{@tagName(tag)}); + const new_item = ExecutionEntry.create(bunTest.gpa, null, args.callback, cfg, null, .{}, .execution); + new_item.next = append_point.next; + append_point.next = new_item; + bun.handleOom(bunTest.extra_execution_entries.append(new_item)); - const new_item = ExecutionEntry.create(bunTest.gpa, null, args.callback, cfg, null, .{}, .execution); - new_item.next = append_point_value.next; - append_point_value.next = new_item; - bun.handleOom(bunTest.extra_execution_entries.append(new_item)); - - return .js_undefined; - } - return globalThis.throw("Cannot call {s}() inside a test. Call it inside describe() instead.", .{@tagName(tag)}); + return .js_undefined; }, .done => return globalThis.throw("Cannot call {s}() after the test run has completed", .{@tagName(tag)}), } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 0f34d3daa5..6639cd5e5a 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -163,7 +163,7 @@ pub const Jest = struct { } pub fn createTestModule(globalObject: *JSGlobalObject) bun.JSError!JSValue { - const module = JSValue.createEmptyObject(globalObject, 19); + const module = JSValue.createEmptyObject(globalObject, 20); const test_scope_functions = try bun_test.ScopeFunctions.createBound(globalObject, .@"test", .zero, .{}, bun_test.ScopeFunctions.strings.@"test"); module.put(globalObject, ZigString.static("test"), test_scope_functions); @@ -183,6 +183,7 @@ pub const Jest = struct { module.put(globalObject, ZigString.static("beforeAll"), jsc.host_fn.NewFunction(globalObject, ZigString.static("beforeAll"), 1, bun_test.js_fns.genericHook(.beforeAll).hookFn, false)); module.put(globalObject, ZigString.static("afterAll"), jsc.host_fn.NewFunction(globalObject, ZigString.static("afterAll"), 1, bun_test.js_fns.genericHook(.afterAll).hookFn, false)); module.put(globalObject, ZigString.static("afterEach"), jsc.host_fn.NewFunction(globalObject, ZigString.static("afterEach"), 1, bun_test.js_fns.genericHook(.afterEach).hookFn, false)); + module.put(globalObject, ZigString.static("onTestFinished"), jsc.host_fn.NewFunction(globalObject, ZigString.static("onTestFinished"), 1, bun_test.js_fns.genericHook(.onTestFinished).hookFn, false)); module.put(globalObject, ZigString.static("setDefaultTimeout"), jsc.host_fn.NewFunction(globalObject, ZigString.static("setDefaultTimeout"), 1, jsSetDefaultTimeout, false)); module.put(globalObject, ZigString.static("expect"), Expect.js.getConstructor(globalObject)); module.put(globalObject, ZigString.static("expectTypeOf"), ExpectTypeOf.js.getConstructor(globalObject)); diff --git a/test/js/bun/test/test-on-test-finished.test.ts b/test/js/bun/test/test-on-test-finished.test.ts new file mode 100644 index 0000000000..e97a24a2e4 --- /dev/null +++ b/test/js/bun/test/test-on-test-finished.test.ts @@ -0,0 +1,116 @@ +import { afterAll, afterEach, describe, expect, onTestFinished, test } from "bun:test"; + +// Test the basic ordering of onTestFinished +describe("onTestFinished ordering", () => { + const output: string[] = []; + + afterEach(() => { + output.push("afterEach"); + }); + + test("test 1", () => { + afterAll(() => { + output.push("inner afterAll"); + }); + onTestFinished(() => { + output.push("onTestFinished"); + }); + output.push("test 1"); + }); + + test("test 2", () => { + // After test 2 starts, verify the order from test 1 + expect(output).toEqual(["test 1", "inner afterAll", "afterEach", "onTestFinished"]); + }); +}); + +// Test multiple onTestFinished calls +describe("multiple onTestFinished", () => { + const output: string[] = []; + + afterEach(() => { + output.push("afterEach"); + }); + + test("test with multiple onTestFinished", () => { + onTestFinished(() => { + output.push("onTestFinished 1"); + }); + onTestFinished(() => { + output.push("onTestFinished 2"); + }); + output.push("test"); + }); + + test("verify order", () => { + expect(output).toEqual(["test", "afterEach", "onTestFinished 1", "onTestFinished 2"]); + }); +}); + +// Test onTestFinished with async callbacks +describe("async onTestFinished", () => { + const output: string[] = []; + + afterEach(() => { + output.push("afterEach"); + }); + + test("async onTestFinished", async () => { + onTestFinished(async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + output.push("onTestFinished async"); + }); + output.push("test"); + }); + + test("verify async order", () => { + expect(output).toEqual(["test", "afterEach", "onTestFinished async"]); + }); +}); + +// Test that onTestFinished throws proper error in concurrent tests +describe("onTestFinished errors", () => { + test.concurrent("cannot be called in concurrent test 1", () => { + expect(() => { + onTestFinished(() => { + console.log("should not run"); + }); + }).toThrow( + "Cannot call onTestFinished() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent.", + ); + }); + + test.concurrent("cannot be called in concurrent test 2", () => { + expect(() => { + onTestFinished(() => { + console.log("should not run"); + }); + }).toThrow( + "Cannot call onTestFinished() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent.", + ); + }); +}); + +// Test onTestFinished with afterEach and afterAll together +describe("onTestFinished with all hooks", () => { + const output: string[] = []; + + afterEach(() => { + output.push("afterEach"); + }); + + test("test with all hooks", () => { + afterAll(() => { + output.push("inner afterAll"); + }); + onTestFinished(() => { + output.push("onTestFinished"); + }); + output.push("test"); + }); + + test("verify complete order", () => { + // Expected order: test body, inner afterAll, afterEach, onTestFinished + expect(output).toEqual(["test", "inner afterAll", "afterEach", "onTestFinished"]); + }); +}); From 5a7b8240912e4ac5abb2a288b2bf28000d7c5f01 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 24 Oct 2025 19:27:14 -0700 Subject: [PATCH 240/391] fix(css): process color-scheme rules inside @layer blocks (#24034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #20689 Previously, `@layer` blocks were not being processed through the CSS minifier, which meant that `color-scheme` properties inside `@layer` blocks would not get the required `--buncss-light`/`--buncss-dark` variable injections needed for browsers that don't support the `light-dark()` function. ## Changes - Implemented proper minification for `LayerBlockRule` in `src/css/rules/rules.zig:218-221` - Added recursive call to `minify()` on nested rules, matching the behavior of other at-rules like `@media` and `@supports` - Added comprehensive tests for `color-scheme` inside `@layer` blocks ## Test Plan Added three new test cases in `test/js/bun/css/css.test.ts`: 1. Simple `@layer` with `color-scheme: dark` 2. Named layers (`@layer shm.colors`) with multiple rules 3. Anonymous `@layer` with `color-scheme: light dark` (generates media query) All tests pass: ```bash bun bd test test/js/bun/css/css.test.ts -t "color-scheme" ``` ## Before ```css /* Input */ @layer shm.colors { body.theme-dark { color-scheme: dark; } } /* Output (broken - no variables) */ @layer shm.colors { body.theme-dark { color-scheme: dark; } } ``` ## After ```css /* Input */ @layer shm.colors { body.theme-dark { color-scheme: dark; } } /* Output (fixed - variables injected) */ @layer shm.colors { body.theme-dark { --buncss-light: ; --buncss-dark: initial; color-scheme: dark; } } ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/css/rules/rules.zig | 4 +-- test/internal/ban-limits.json | 2 +- test/js/bun/css/css.test.ts | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/css/rules/rules.zig b/src/css/rules/rules.zig index bd2578c20b..8860619a59 100644 --- a/src/css/rules/rules.zig +++ b/src/css/rules/rules.zig @@ -216,8 +216,8 @@ pub fn CssRuleList(comptime AtRule: type) type { debug("TODO: ContainerRule", .{}); }, .layer_block => |*lay| { - _ = lay; // autofix - debug("TODO: LayerBlockRule", .{}); + try lay.rules.minify(context, parent_is_unused); + if (lay.rules.v.items.len == 0) continue; }, .layer_statement => |*lay| { _ = lay; // autofix diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index 7cac923618..b77728edff 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -9,7 +9,7 @@ ".jsBoolean(true)": 0, ".stdDir()": 41, ".stdFile()": 16, - "// autofix": 165, + "// autofix": 164, ": [^=]+= undefined,$": 255, "== alloc.ptr": 0, "== allocator.ptr": 0, diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index f147451099..5eac899ae4 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7308,6 +7308,68 @@ describe("css tests", () => { `, { chrome: Some(90 << 16) }, ); + + // Test color-scheme inside @layer blocks (issue #20689) + prefix_test( + `@layer colors { + .foo { color-scheme: dark; } + }`, + `@layer colors { + .foo { + --buncss-light: ; + --buncss-dark: initial; + color-scheme: dark; + } + } + `, + { chrome: Some(90 << 16) }, + ); + prefix_test( + `@layer shm.colors { + body.theme-dark { + color-scheme: dark; + } + body.theme-light { + color-scheme: light; + } + }`, + `@layer shm.colors { + body.theme-dark { + --buncss-light: ; + --buncss-dark: initial; + color-scheme: dark; + } + + body.theme-light { + --buncss-light: initial; + --buncss-dark: ; + color-scheme: light; + } + } + `, + { chrome: Some(90 << 16) }, + ); + prefix_test( + `@layer { + .foo { color-scheme: light dark; } + }`, + `@layer { + .foo { + --buncss-light: initial; + --buncss-dark: ; + color-scheme: light dark; + } + + @media (prefers-color-scheme: dark) { + .foo { + --buncss-light: ; + --buncss-dark: initial; + } + } + } + `, + { chrome: Some(90 << 16) }, + ); }); describe("edge cases", () => { From cfe561a0834458d183db36faf1cd6ca24e16fa20 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 24 Oct 2025 19:30:43 -0700 Subject: [PATCH 241/391] fix: allow lifecycle hooks to accept options as second parameter (#24039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #23133 This PR fixes a bug where lifecycle hooks (`beforeAll`, `beforeEach`, `afterAll`, `afterEach`) would throw an error when called with a function and options object: ```typescript beforeAll(() => { console.log("beforeAll") }, { timeout: 10_000 }) ``` Previously, this would throw: `error: beforeAll() expects a function as the second argument` ## Root Cause The issue was in `ScopeFunctions.parseArguments()` at `src/bun.js/test/ScopeFunctions.zig:342`. When parsing two arguments, it always treated them as `(description, callback)` instead of checking if they could be `(callback, options)`. ## Solution Updated the two-argument parsing logic to check if the first argument is a function and the second is not a function. In that case, treat them as `(callback, options)` instead of `(description, callback)`. ## Changes - Modified `src/bun.js/test/ScopeFunctions.zig` to handle `(callback, options)` case - Added regression test at `test/regression/issue/23133.test.ts` ## Testing ✅ Verified the fix works with the reproduction case from the issue ✅ Added comprehensive regression test covering all lifecycle hooks with both object and numeric timeout options ✅ All existing jest-hooks tests still pass ✅ Test fails with `USE_SYSTEM_BUN=1` and passes with the fixed build 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: pfg --- src/bun.js/test/ScopeFunctions.zig | 9 +++-- src/bun.js/test/bun_test.zig | 2 +- test/regression/issue/23133.test.ts | 54 +++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 test/regression/issue/23133.test.ts diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig index 4d7b1c9a4a..7f4c6438ef 100644 --- a/src/bun.js/test/ScopeFunctions.zig +++ b/src/bun.js/test/ScopeFunctions.zig @@ -296,6 +296,7 @@ const ParseArgumentsResult = struct { } }; pub const CallbackMode = enum { require, allow }; +pub const FunctionKind = enum { test_or_describe, hook }; fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, description: jsc.JSValue, signature: Signature) bun.JSError![]const u8 { if (description == .zero) { @@ -329,7 +330,7 @@ fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, descr return globalThis.throwPretty("{s}() expects first argument to be a named class, named function, number, or string", .{signature}); } -pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode }) bun.JSError!ParseArgumentsResult { +pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode, kind: FunctionKind = .test_or_describe }) bun.JSError!ParseArgumentsResult { var a1, var a2, var a3 = callframe.argumentsAsArray(3); const len: enum { three, two, one, zero } = if (!a3.isUndefinedOrNull()) .three else if (!a2.isUndefinedOrNull()) .two else if (!a1.isUndefinedOrNull()) .one else .zero; @@ -338,8 +339,9 @@ pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame // description, callback(fn), options(!fn) // description, options(!fn), callback(fn) .three => if (a2.isFunction()) .{ .description = a1, .callback = a2, .options = a3 } else .{ .description = a1, .callback = a3, .options = a2 }, + // callback(fn), options(!fn) // description, callback(fn) - .two => .{ .description = a1, .callback = a2 }, + .two => if (a1.isFunction() and !a2.isFunction()) .{ .callback = a1, .options = a2 } else .{ .description = a1, .callback = a2 }, // description // callback(fn) .one => if (a1.isFunction()) .{ .callback = a1 } else .{ .description = a1 }, @@ -352,7 +354,8 @@ pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame } else if (callback.isFunction()) blk: { break :blk callback.withAsyncContextIfNeeded(globalThis); } else { - return globalThis.throw("{s} expects a function as the second argument", .{signature}); + const ordinal = if (cfg.kind == .hook) "first" else "second"; + return globalThis.throw("{s} expects a function as the {s} argument", .{ signature, ordinal }); }; var result: ParseArgumentsResult = .{ diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 34fd701ccb..02a4c90078 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -44,7 +44,7 @@ pub const js_fns = struct { defer group.end(); errdefer group.log("ended in error", .{}); - var args = try ScopeFunctions.parseArguments(globalThis, callFrame, .{ .str = @tagName(tag) ++ "()" }, bun.default_allocator, .{ .callback = .require }); + var args = try ScopeFunctions.parseArguments(globalThis, callFrame, .{ .str = @tagName(tag) ++ "()" }, bun.default_allocator, .{ .callback = .require, .kind = .hook }); defer args.deinit(bun.default_allocator); const has_done_parameter = if (args.callback) |callback| try callback.getLength(globalThis) > 0 else false; diff --git a/test/regression/issue/23133.test.ts b/test/regression/issue/23133.test.ts new file mode 100644 index 0000000000..e4b20eee7c --- /dev/null +++ b/test/regression/issue/23133.test.ts @@ -0,0 +1,54 @@ +// https://github.com/oven-sh/bun/issues/23133 +// Passing HookOptions to lifecycle hooks should work +import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test"; + +const logs: string[] = []; + +// Test beforeAll with object timeout option +beforeAll( + () => { + logs.push("beforeAll with object timeout"); + }, + { timeout: 10_000 }, +); + +// Test beforeAll with numeric timeout option +beforeAll(() => { + logs.push("beforeAll with numeric timeout"); +}, 5000); + +// Test beforeEach with timeout option +beforeEach( + () => { + logs.push("beforeEach"); + }, + { timeout: 10_000 }, +); + +// Test afterEach with timeout option +afterEach( + () => { + logs.push("afterEach"); + }, + { timeout: 10_000 }, +); + +// Test afterAll with timeout option +afterAll( + () => { + logs.push("afterAll"); + }, + { timeout: 10_000 }, +); + +test("lifecycle hooks accept timeout options", () => { + expect(logs).toContain("beforeAll with object timeout"); + expect(logs).toContain("beforeAll with numeric timeout"); + expect(logs).toContain("beforeEach"); +}); + +test("beforeEach runs before each test", () => { + // beforeEach should have run twice now (once for each test) + const beforeEachCount = logs.filter(l => l === "beforeEach").length; + expect(beforeEachCount).toBe(2); +}); From f4b6396eac9a5d33fedd7dc1f45a854dee4ca653 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 25 Oct 2025 13:36:33 +0900 Subject: [PATCH 242/391] Fix unhandled exception in JSC__JSPromise__wrap when resolving promise (#23961) ### What does this PR do? Previously, `JSC__JSPromise__wrap` would call `JSC::JSPromise::resolvedPromise(globalObject, result)` without checking if an exception was thrown during promise resolution. This could happen in certain edge cases, such as when the result value is a thenable that triggers stack overflow, or when the promise resolution mechanism itself encounters an error. When such exceptions occurred, they would escape back to the Zig code, causing the CatchScope assertion to fail with "ASSERTION FAILED: Unexpected exception observed on thread" instead of being properly handled. This PR adds an exception check immediately after calling `JSC::JSPromise::resolvedPromise()` and before the `RELEASE_AND_RETURN` macro. If an exception is detected, the function now clears it and returns a rejected promise with the exception value, ensuring consistent error handling behavior. This matches the pattern already used earlier in the function for the initial function call exception handling. ### How did you verify your code works? new and existing tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/bindings/bindings.cpp | 9 ++++++++- test/js/web/fetch/response.test.ts | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index db24591323..a3153d67e1 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3397,7 +3397,14 @@ JSC::EncodedJSValue JSC__JSPromise__wrap(JSC::JSGlobalObject* globalObject, void RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, err))); } - RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::resolvedPromise(globalObject, result))); + JSValue resolved = JSC::JSPromise::resolvedPromise(globalObject, result); + if (scope.exception()) [[unlikely]] { + auto* exception = scope.exception(); + scope.clearException(); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, exception->value()))); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(resolved)); } [[ZIG_EXPORT(check_slow)]] void JSC__JSPromise__reject(JSC::JSPromise* arg0, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue JSValue2) diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts index c5a249f6d1..cdbdfd42a9 100644 --- a/test/js/web/fetch/response.test.ts +++ b/test/js/web/fetch/response.test.ts @@ -49,7 +49,7 @@ describe("2-arg form", () => { test("print size", () => { expect(normalizeBunSnapshot(Bun.inspect(new Response(Bun.file(import.meta.filename)))), import.meta.dir) .toMatchInlineSnapshot(` - "Response (3.82 KB) { + "Response (4.15 KB) { ok: true, url: "", status: 200, @@ -109,3 +109,17 @@ test("new Response(123, { method: 456 }) does not throw", () => { // @ts-expect-error expect(() => new Response("123", { method: 456 })).not.toThrow(); }); + +test("handle stack overflow", () => { + function f0(a1, a2) { + const v4 = new Response(); + // @ts-ignore + const v5 = v4.text(a2, a2, v4, f0, f0); + a1(a1); // Recursive call causes stack overflow + return v5; + } + expect(() => { + // @ts-ignore + f0(f0); + }).toThrow("Maximum call stack size exceeded."); +}); From 0fba69d50cb704bbb32bcd3fe4e38c69707763ac Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Oct 2025 23:42:20 -0700 Subject: [PATCH 243/391] Add some internal deprecation @compileError messages --- src/CLAUDE.md | 1 - src/bun.zig | 3 +++ src/main.zig | 47 +++++++++++++++++++++++++---------------------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 21b296e7f1..7b394aa69f 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -8,5 +8,4 @@ Syntax reminders: Conventions: - Prefer `@import` at the **bottom** of the file, but the auto formatter will move them so you don't need to worry about it. -- Prefer `@import("bun")`. Not `@import("root").bun` or `@import("../bun.zig")`. - You must be patient with the build. diff --git a/src/bun.zig b/src/bun.zig index f1f13acbef..a4c57dcebf 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3770,3 +3770,6 @@ const CopyFile = @import("./copy_file.zig"); const builtin = @import("builtin"); const std = @import("std"); const Allocator = std.mem.Allocator; + +// Claude thinks its bun.JSC when we renamed it to bun.jsc months ago. +pub const JSC = @compileError("Deprecated: Use @import(\"bun\").jsc instead"); diff --git a/src/main.zig b/src/main.zig index d07a0630c6..cc3856e4f7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,4 @@ -pub const panic = bun.crash_handler.panic; +pub const panic = _bun.crash_handler.panic; pub const std_options = std.Options{ .enable_segfault_handler = false, }; @@ -6,7 +6,7 @@ pub const std_options = std.Options{ pub const io_mode = .blocking; comptime { - bun.assert(builtin.target.cpu.arch.endian() == .little); + _bun.assert(builtin.target.cpu.arch.endian() == .little); } extern fn bun_warn_avx_missing(url: [*:0]const u8) void; @@ -15,7 +15,7 @@ pub extern "c" var _environ: ?*anyopaque; pub extern "c" var environ: ?*anyopaque; pub fn main() void { - bun.crash_handler.init(); + _bun.crash_handler.init(); if (Environment.isPosix) { var act: std.posix.Sigaction = .{ @@ -28,38 +28,38 @@ pub fn main() void { } if (Environment.isDebug) { - bun.debug_allocator_data.backing = .init; + _bun.debug_allocator_data.backing = .init; } // This should appear before we make any calls at all to libuv. // So it's safest to put it very early in the main function. if (Environment.isWindows) { - _ = bun.windows.libuv.uv_replace_allocator( - &bun.mimalloc.mi_malloc, - &bun.mimalloc.mi_realloc, - &bun.mimalloc.mi_calloc, - &bun.mimalloc.mi_free, + _ = _bun.windows.libuv.uv_replace_allocator( + &_bun.mimalloc.mi_malloc, + &_bun.mimalloc.mi_realloc, + &_bun.mimalloc.mi_calloc, + &_bun.mimalloc.mi_free, ); - bun.handleOom(bun.windows.env.convertEnvToWTF8()); + _bun.handleOom(_bun.windows.env.convertEnvToWTF8()); environ = @ptrCast(std.os.environ.ptr); _environ = @ptrCast(std.os.environ.ptr); } - bun.start_time = std.time.nanoTimestamp(); - bun.initArgv(bun.default_allocator) catch |err| { + _bun.start_time = std.time.nanoTimestamp(); + _bun.initArgv(_bun.default_allocator) catch |err| { Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)}); }; Output.Source.Stdio.init(); defer Output.flush(); if (Environment.isX64 and Environment.enableSIMD and Environment.isPosix) { - bun_warn_avx_missing(bun.cli.UpgradeCommand.Bun__githubBaselineURL.ptr); + bun_warn_avx_missing(_bun.cli.UpgradeCommand.Bun__githubBaselineURL.ptr); } - bun.StackCheck.configureThread(); + _bun.StackCheck.configureThread(); - bun.cli.Cli.start(bun.default_allocator); - bun.Global.exit(0); + _bun.cli.Cli.start(_bun.default_allocator); + _bun.Global.exit(0); } pub export fn Bun__panic(msg: [*]const u8, len: usize) noreturn { @@ -71,22 +71,25 @@ pub fn copyForwards(comptime T: type, dest: []T, source: []const T) void { if (source.len == 0) { return; } - bun.copy(T, dest[0..source.len], source); + _bun.copy(T, dest[0..source.len], source); } pub fn copyBackwards(comptime T: type, dest: []T, source: []const T) void { if (source.len == 0) { return; } - bun.copy(T, dest[0..source.len], source); + _bun.copy(T, dest[0..source.len], source); } pub fn eqlBytes(src: []const u8, dest: []const u8) bool { - return bun.c.memcmp(src.ptr, dest.ptr, src.len) == 0; + return _bun.c.memcmp(src.ptr, dest.ptr, src.len) == 0; } // -- End Zig Standard Library Additions -- const builtin = @import("builtin"); const std = @import("std"); -const bun = @import("bun"); -const Environment = bun.Environment; -const Output = bun.Output; +// Claude thinks its @import("root").bun when it's @import("bun"). +const bun = @compileError("Deprecated: Use @import(\"bun\") instead"); + +const _bun = @import("bun"); +const Environment = _bun.Environment; +const Output = _bun.Output; From d2c284242037520f239903565a8b4021bb881526 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Oct 2025 00:05:28 -0700 Subject: [PATCH 244/391] Autoformat --- src/bun.zig | 6 +++--- src/main.zig | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bun.zig b/src/bun.zig index a4c57dcebf..a1230783c9 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3766,10 +3766,10 @@ pub fn getUseSystemCA(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFra return jsc.JSValue.jsBoolean(Arguments.Bun__Node__UseSystemCA); } +// Claude thinks its bun.JSC when we renamed it to bun.jsc months ago. +pub const JSC = @compileError("Deprecated: Use @import(\"bun\").jsc instead"); + const CopyFile = @import("./copy_file.zig"); const builtin = @import("builtin"); const std = @import("std"); const Allocator = std.mem.Allocator; - -// Claude thinks its bun.JSC when we renamed it to bun.jsc months ago. -pub const JSC = @compileError("Deprecated: Use @import(\"bun\").jsc instead"); diff --git a/src/main.zig b/src/main.zig index cc3856e4f7..f0ee3cd83a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -84,12 +84,12 @@ pub fn eqlBytes(src: []const u8, dest: []const u8) bool { } // -- End Zig Standard Library Additions -- -const builtin = @import("builtin"); -const std = @import("std"); - // Claude thinks its @import("root").bun when it's @import("bun"). const bun = @compileError("Deprecated: Use @import(\"bun\") instead"); +const builtin = @import("builtin"); +const std = @import("std"); + const _bun = @import("bun"); const Environment = _bun.Environment; const Output = _bun.Output; From fb1fbe62e6151ea6e0e9430f714dbb0c30adca6c Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sat, 25 Oct 2025 14:52:34 -0800 Subject: [PATCH 245/391] ci: update alpine linux to 3.22 (#24052) [publish images] --- .buildkite/ci.mjs | 14 +++++++------- dockerhub/alpine/Dockerfile | 4 ++-- package.json | 2 +- scripts/bootstrap.sh | 3 +-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 6d27bb7e65..5d3423b0a3 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -108,9 +108,9 @@ const buildPlatforms = [ { os: "linux", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] }, { os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, { os: "linux", arch: "x64", profile: "asan", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.21" }, - { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.21" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.21" }, + { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.22" }, + { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.22" }, + { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.22" }, { os: "windows", arch: "x64", release: "2019" }, { os: "windows", arch: "x64", baseline: true, release: "2019" }, ]; @@ -133,9 +133,9 @@ const testPlatforms = [ { os: "linux", arch: "x64", distro: "ubuntu", release: "24.04", tier: "latest" }, { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "25.04", tier: "latest" }, { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "24.04", tier: "latest" }, - { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.21", tier: "latest" }, - { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.21", tier: "latest" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.21", tier: "latest" }, + { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.22", tier: "latest" }, + { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.22", tier: "latest" }, + { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.22", tier: "latest" }, { os: "windows", arch: "x64", release: "2019", tier: "oldest" }, { os: "windows", arch: "x64", release: "2019", baseline: true, tier: "oldest" }, ]; @@ -343,7 +343,7 @@ function getZigPlatform() { arch: "aarch64", abi: "musl", distro: "alpine", - release: "3.21", + release: "3.22", }; } diff --git a/dockerhub/alpine/Dockerfile b/dockerhub/alpine/Dockerfile index 8d1ecbaddd..4d5a01876f 100644 --- a/dockerhub/alpine/Dockerfile +++ b/dockerhub/alpine/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.20 AS build +FROM alpine:3.22 AS build # https://github.com/oven-sh/bun/releases ARG BUN_VERSION=latest @@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \ && rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \ && chmod +x /usr/local/bin/bun -FROM alpine:3.20 +FROM alpine:3.22 # Disable the runtime transpiler cache by default inside Docker containers. # On ephemeral containers, the cache is not useful diff --git a/package.json b/package.json index bc4df314a6..c0fcee4b5f 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "clean:zig": "rm -rf build/debug/cache/zig build/debug/CMakeCache.txt 'build/debug/*.o' .zig-cache zig-out || true", "machine:linux:ubuntu": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=ubuntu --release=25.04", "machine:linux:debian": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=debian --release=12", - "machine:linux:alpine": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=alpine --release=3.21", + "machine:linux:alpine": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=alpine --release=3.22", "machine:linux:amazonlinux": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=amazonlinux --release=2023", "machine:windows:2019": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=windows --release=2019", "sync-webkit-source": "bun ./scripts/sync-webkit-source.ts" diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 3537285e05..ebda5460ea 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1060,12 +1060,11 @@ install_llvm() { install_packages "llvm@$(llvm_version)" ;; apk) - # alpine doesn't have a lld19 package on 3.21 atm so use bare one for now install_packages \ "llvm$(llvm_version)" \ "clang$(llvm_version)" \ "scudo-malloc" \ - "lld" \ + "lld$(llvm_version)" \ "llvm$(llvm_version)-dev" # Ensures llvm-symbolizer is installed ;; esac From a2b262ed69402238ec1623b716ea9cb6fade7861 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sat, 25 Oct 2025 14:53:02 -0800 Subject: [PATCH 246/391] ci: update bun version to 1.3.1 (#24053) [publish images] --- scripts/bootstrap.ps1 | 2 +- scripts/bootstrap.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 index 9b3cf40315..f5ddf5026d 100755 --- a/scripts/bootstrap.ps1 +++ b/scripts/bootstrap.ps1 @@ -244,7 +244,7 @@ function Install-NodeJs { } function Install-Bun { - Install-Package bun -Version "1.2.17" + Install-Package bun -Version "1.3.1" } function Install-Cygwin { diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index ebda5460ea..62cd622cc6 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -907,7 +907,7 @@ setup_node_gyp_cache() { } bun_version_exact() { - print "1.2.17" + print "1.3.1" } install_bun() { From 3367fa6ae360ec7b3e38cf413e248fc0ec598327 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 25 Oct 2025 20:43:02 -0700 Subject: [PATCH 247/391] Refactor: Extract ModuleLoader components into separate files (#24083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Split `ModuleLoader.zig` into smaller, more focused modules for better code organization and maintainability: - `AsyncModule` → `src/bun.js/AsyncModule.zig` (lines 69-806) - `RuntimeTranspilerStore` → `src/bun.js/RuntimeTranspilerStore.zig` (lines 2028-2606) - `HardcodedModule` → `src/bun.js/HardcodedModule.zig` (lines 2618-3040) ## Changes - Extracted three large components from `ModuleLoader.zig` into separate files - Updated imports in all affected files - Made necessary functions/constants public (`dumpSource`, `dumpSourceString`, `setBreakPointOnFirstLine`, `bun_aliases`) - Updated `ModuleLoader.zig` to import the new modules ## Testing - Build passes successfully (`bun bd`) - Basic module loading verified with smoke tests - Existing resolve tests continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/AsyncModule.zig | 781 +++++++++++ src/bun.js/HardcodedModule.zig | 431 ++++++ src/bun.js/ModuleLoader.zig | 1759 +------------------------ src/bun.js/RuntimeTranspilerStore.zig | 626 +++++++++ 4 files changed, 1846 insertions(+), 1751 deletions(-) create mode 100644 src/bun.js/AsyncModule.zig create mode 100644 src/bun.js/HardcodedModule.zig create mode 100644 src/bun.js/RuntimeTranspilerStore.zig diff --git a/src/bun.js/AsyncModule.zig b/src/bun.js/AsyncModule.zig new file mode 100644 index 0000000000..7cc369fc3a --- /dev/null +++ b/src/bun.js/AsyncModule.zig @@ -0,0 +1,781 @@ +const debug = Output.scoped(.AsyncModule, .hidden); + +const string = []const u8; + +pub const AsyncModule = struct { + // This is all the state used by the printer to print the module + parse_result: ParseResult, + promise: jsc.Strong.Optional = .empty, + path: Fs.Path, + specifier: string = "", + referrer: string = "", + string_buf: []u8 = &[_]u8{}, + fd: ?StoredFileDescriptorType = null, + package_json: ?*PackageJSON = null, + loader: api.Loader, + hash: u32 = std.math.maxInt(u32), + globalThis: *JSGlobalObject = undefined, + arena: *bun.ArenaAllocator, + + // This is the specific state for making it async + poll_ref: Async.KeepAlive = .{}, + any_task: jsc.AnyTask = undefined, + + pub const Id = u32; + + const PackageDownloadError = struct { + name: []const u8, + resolution: Install.Resolution, + err: anyerror, + url: []const u8, + }; + + const PackageResolveError = struct { + name: []const u8, + err: anyerror, + url: []const u8, + version: Dependency.Version, + }; + + pub const Queue = struct { + map: Map = .{}, + scheduled: u32 = 0, + concurrent_task_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + + const DeferredDependencyError = struct { + dependency: Dependency, + root_dependency_id: Install.DependencyID, + err: anyerror, + }; + + pub const Map = std.ArrayListUnmanaged(AsyncModule); + + pub fn enqueue(this: *Queue, globalObject: *JSGlobalObject, opts: anytype) void { + debug("enqueue: {s}", .{opts.specifier}); + var module = AsyncModule.init(opts, globalObject) catch unreachable; + module.poll_ref.ref(this.vm()); + + this.map.append(this.vm().allocator, module) catch unreachable; + this.vm().packageManager().drainDependencyList(); + } + + pub fn onDependencyError(ctx: *anyopaque, dependency: Dependency, root_dependency_id: Install.DependencyID, err: anyerror) void { + var this = bun.cast(*Queue, ctx); + debug("onDependencyError: {s}", .{this.vm().packageManager().lockfile.str(&dependency.name)}); + + var modules: []AsyncModule = this.map.items; + var i: usize = 0; + outer: for (modules) |module_| { + var module = module_; + const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); + for (root_dependency_ids, 0..) |dep, dep_i| { + if (dep != root_dependency_id) continue; + module.resolveError( + this.vm(), + module.parse_result.pending_imports.items(.import_record_id)[dep_i], + .{ + .name = this.vm().packageManager().lockfile.str(&dependency.name), + .err = err, + .url = "", + .version = dependency.version, + }, + ) catch unreachable; + continue :outer; + } + + modules[i] = module; + i += 1; + } + this.map.items.len = i; + } + pub fn onWakeHandler(ctx: *anyopaque, _: *PackageManager) void { + debug("onWake", .{}); + var this = bun.cast(*Queue, ctx); + this.vm().enqueueTaskConcurrent(jsc.ConcurrentTask.createFrom(this)); + } + + pub fn onPoll(this: *Queue) void { + debug("onPoll", .{}); + this.runTasks(); + this.pollModules(); + } + + pub fn runTasks(this: *Queue) void { + var pm = this.vm().packageManager(); + + if (Output.enable_ansi_colors_stderr) { + pm.startProgressBarIfNone(); + pm.runTasks( + *Queue, + this, + .{ + .onExtract = {}, + .onResolve = onResolve, + .onPackageManifestError = onPackageManifestError, + .onPackageDownloadError = onPackageDownloadError, + .progress_bar = true, + }, + true, + PackageManager.Options.LogLevel.default, + ) catch unreachable; + } else { + pm.runTasks( + *Queue, + this, + .{ + .onExtract = {}, + .onResolve = onResolve, + .onPackageManifestError = onPackageManifestError, + .onPackageDownloadError = onPackageDownloadError, + }, + true, + PackageManager.Options.LogLevel.default_no_progress, + ) catch unreachable; + } + } + + pub fn onResolve(_: *Queue) void { + debug("onResolve", .{}); + } + + pub fn onPackageManifestError( + this: *Queue, + name: []const u8, + err: anyerror, + url: []const u8, + ) void { + debug("onPackageManifestError: {s}", .{name}); + + var modules: []AsyncModule = this.map.items; + var i: usize = 0; + outer: for (modules) |module_| { + var module = module_; + const tags = module.parse_result.pending_imports.items(.tag); + for (tags, 0..) |tag, tag_i| { + if (tag == .resolve) { + const esms = module.parse_result.pending_imports.items(.esm); + const esm = esms[tag_i]; + const string_bufs = module.parse_result.pending_imports.items(.string_buf); + + if (!strings.eql(esm.name.slice(string_bufs[tag_i]), name)) continue; + + const versions = module.parse_result.pending_imports.items(.dependency); + + module.resolveError( + this.vm(), + module.parse_result.pending_imports.items(.import_record_id)[tag_i], + .{ + .name = name, + .err = err, + .url = url, + .version = versions[tag_i], + }, + ) catch unreachable; + continue :outer; + } + } + + modules[i] = module; + i += 1; + } + this.map.items.len = i; + } + + pub fn onPackageDownloadError( + this: *Queue, + package_id: Install.PackageID, + name: []const u8, + resolution: *const Install.Resolution, + err: anyerror, + url: []const u8, + ) void { + debug("onPackageDownloadError: {s}", .{name}); + + const resolution_ids = this.vm().packageManager().lockfile.buffers.resolutions.items; + var modules: []AsyncModule = this.map.items; + var i: usize = 0; + outer: for (modules) |module_| { + var module = module_; + const record_ids = module.parse_result.pending_imports.items(.import_record_id); + const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); + for (root_dependency_ids, 0..) |dependency_id, import_id| { + if (resolution_ids[dependency_id] != package_id) continue; + module.downloadError( + this.vm(), + record_ids[import_id], + .{ + .name = name, + .resolution = resolution.*, + .err = err, + .url = url, + }, + ) catch unreachable; + continue :outer; + } + + modules[i] = module; + i += 1; + } + this.map.items.len = i; + } + + pub fn pollModules(this: *Queue) void { + var pm = this.vm().packageManager(); + if (pm.pending_tasks.load(.monotonic) > 0) return; + + var modules: []AsyncModule = this.map.items; + var i: usize = 0; + + for (modules) |mod| { + var module = mod; + var tags = module.parse_result.pending_imports.items(.tag); + const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); + // var esms = module.parse_result.pending_imports.items(.esm); + // var versions = module.parse_result.pending_imports.items(.dependency); + var done_count: usize = 0; + for (tags, 0..) |tag, tag_i| { + const root_id = root_dependency_ids[tag_i]; + const resolution_ids = pm.lockfile.buffers.resolutions.items; + if (root_id >= resolution_ids.len) continue; + const package_id = resolution_ids[root_id]; + + switch (tag) { + .resolve => { + if (package_id == Install.invalid_package_id) { + continue; + } + + // if we get here, the package has already been resolved. + tags[tag_i] = .download; + }, + .download => { + if (package_id == Install.invalid_package_id) { + unreachable; + } + }, + .done => { + done_count += 1; + continue; + }, + } + + if (package_id == Install.invalid_package_id) { + continue; + } + + const package = pm.lockfile.packages.get(package_id); + bun.assert(package.resolution.tag != .root); + + var name_and_version_hash: ?u64 = null; + var patchfile_hash: ?u64 = null; + switch (pm.determinePreinstallState(package, pm.lockfile, &name_and_version_hash, &patchfile_hash)) { + .done => { + // we are only truly done if all the dependencies are done. + const current_tasks = pm.total_tasks; + // so if enqueuing all the dependencies produces no new tasks, we are done. + pm.enqueueDependencyList(package.dependencies); + if (current_tasks == pm.total_tasks) { + tags[tag_i] = .done; + done_count += 1; + } + }, + .extracting => { + // we are extracting the package + // we need to wait for the next poll + continue; + }, + .extract => {}, + else => {}, + } + } + + if (done_count == tags.len) { + module.done(this.vm()); + } else { + modules[i] = module; + i += 1; + } + } + this.map.items.len = i; + if (i == 0) { + // ensure we always end the progress bar + this.vm().packageManager().endProgressBar(); + } + } + + pub fn vm(this: *Queue) *VirtualMachine { + return @alignCast(@fieldParentPtr("modules", this)); + } + + comptime { + // Ensure VirtualMachine has a field named "modules" of the correct type + // If this fails, the @fieldParentPtr in vm() above needs to be updated + const VM = @import("./VirtualMachine.zig"); + if (!@hasField(VM, "modules")) { + @compileError("VirtualMachine must have a 'modules' field for AsyncModule.Queue.vm() to work"); + } + } + }; + + pub fn init(opts: anytype, globalObject: *JSGlobalObject) !AsyncModule { + // var stmt_blocks = js_ast.Stmt.Data.toOwnedSlice(); + // var expr_blocks = js_ast.Expr.Data.toOwnedSlice(); + const this_promise = JSValue.createInternalPromise(globalObject); + const promise = jsc.Strong.Optional.create(this_promise, globalObject); + + var buf = bun.StringBuilder{}; + buf.count(opts.referrer); + buf.count(opts.specifier); + buf.count(opts.path.text); + + try buf.allocate(bun.default_allocator); + opts.promise_ptr.?.* = this_promise.asInternalPromise().?; + const referrer = buf.append(opts.referrer); + const specifier = buf.append(opts.specifier); + const path = Fs.Path.init(buf.append(opts.path.text)); + + return AsyncModule{ + .parse_result = opts.parse_result, + .promise = promise, + .path = path, + .specifier = specifier, + .referrer = referrer, + .fd = opts.fd, + .package_json = opts.package_json, + .loader = opts.loader.toAPI(), + .string_buf = buf.allocatedSlice(), + // .stmt_blocks = stmt_blocks, + // .expr_blocks = expr_blocks, + .globalThis = globalObject, + .arena = opts.arena, + }; + } + + pub fn done(this: *AsyncModule, jsc_vm: *VirtualMachine) void { + var clone = jsc_vm.allocator.create(AsyncModule) catch unreachable; + clone.* = this.*; + jsc_vm.modules.scheduled += 1; + clone.any_task = jsc.AnyTask.New(AsyncModule, onDone).init(clone); + jsc_vm.enqueueTask(jsc.Task.init(&clone.any_task)); + } + + pub fn onDone(this: *AsyncModule) void { + jsc.markBinding(@src()); + var jsc_vm = this.globalThis.bunVM(); + jsc_vm.modules.scheduled -= 1; + if (jsc_vm.modules.scheduled == 0) { + jsc_vm.packageManager().endProgressBar(); + } + var log = logger.Log.init(jsc_vm.allocator); + defer log.deinit(); + var errorable: jsc.ErrorableResolvedSource = undefined; + this.poll_ref.unref(jsc_vm); + outer: { + errorable = jsc.ErrorableResolvedSource.ok(this.resumeLoadingModule(&log) catch |err| { + switch (err) { + error.JSError => { + errorable = .err(error.JSError, this.globalThis.takeError(error.JSError)); + break :outer; + }, + else => { + VirtualMachine.processFetchLog( + this.globalThis, + bun.String.init(this.specifier), + bun.String.init(this.referrer), + &log, + &errorable, + err, + ); + break :outer; + }, + } + }); + } + + var spec = bun.String.init(ZigString.init(this.specifier).withEncoding()); + var ref = bun.String.init(ZigString.init(this.referrer).withEncoding()); + bun.jsc.fromJSHostCallGeneric(this.globalThis, @src(), Bun__onFulfillAsyncModule, .{ + this.globalThis, + this.promise.get().?, + &errorable, + &spec, + &ref, + }) catch {}; + this.deinit(); + jsc_vm.allocator.destroy(this); + } + + pub fn fulfill( + globalThis: *JSGlobalObject, + promise: JSValue, + resolved_source: *ResolvedSource, + err: ?anyerror, + specifier_: bun.String, + referrer_: bun.String, + log: *logger.Log, + ) bun.JSError!void { + jsc.markBinding(@src()); + var specifier = specifier_; + var referrer = referrer_; + var scope: jsc.CatchScope = undefined; + scope.init(globalThis, @src()); + defer { + specifier.deref(); + referrer.deref(); + scope.deinit(); + } + + var errorable: jsc.ErrorableResolvedSource = undefined; + if (err) |e| { + defer { + if (resolved_source.source_code_needs_deref) { + resolved_source.source_code_needs_deref = false; + resolved_source.source_code.deref(); + } + } + + if (e == error.JSError) { + errorable = jsc.ErrorableResolvedSource.err(error.JSError, globalThis.takeError(error.JSError)); + } else { + VirtualMachine.processFetchLog( + globalThis, + specifier, + referrer, + log, + &errorable, + e, + ); + } + } else { + errorable = jsc.ErrorableResolvedSource.ok(resolved_source.*); + } + log.deinit(); + + debug("fulfill: {any}", .{specifier}); + + try bun.jsc.fromJSHostCallGeneric(globalThis, @src(), Bun__onFulfillAsyncModule, .{ + globalThis, + promise, + &errorable, + &specifier, + &referrer, + }); + } + + pub fn resolveError(this: *AsyncModule, vm: *VirtualMachine, import_record_id: u32, result: PackageResolveError) !void { + const globalThis = this.globalThis; + + const msg: []u8 = try switch (result.err) { + error.PackageManifestHTTP400 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 400 while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.PackageManifestHTTP401 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 401 while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.PackageManifestHTTP402 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 402 while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.PackageManifestHTTP403 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 403 while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.PackageManifestHTTP404 => std.fmt.allocPrint( + bun.default_allocator, + "Package '{s}' was not found", + .{result.name}, + ), + error.PackageManifestHTTP4xx => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 4xx while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.PackageManifestHTTP5xx => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 5xx while resolving package '{s}' at '{s}'", + .{ result.name, result.url }, + ), + error.DistTagNotFound, error.NoMatchingVersion => brk: { + const prefix: []const u8 = if (result.err == error.NoMatchingVersion and result.version.tag == .npm and result.version.value.npm.version.isExact()) + "Version not found" + else if (result.version.tag == .npm and !result.version.value.npm.version.isExact()) + "No matching version found" + else + "No match found"; + + break :brk std.fmt.allocPrint( + bun.default_allocator, + "{s} '{s}' for package '{s}' (but package exists)", + .{ prefix, vm.packageManager().lockfile.str(&result.version.literal), result.name }, + ); + }, + else => |err| std.fmt.allocPrint( + bun.default_allocator, + "{s} resolving package '{s}' at '{s}'", + .{ bun.asByteSlice(@errorName(err)), result.name, result.url }, + ), + }; + defer bun.default_allocator.free(msg); + + const name: []const u8 = switch (result.err) { + error.NoMatchingVersion => "PackageVersionNotFound", + error.DistTagNotFound => "PackageTagNotFound", + error.PackageManifestHTTP403 => "PackageForbidden", + error.PackageManifestHTTP404 => "PackageNotFound", + else => "PackageResolveError", + }; + + var error_instance = ZigString.init(msg).withEncoding().toErrorInstance(globalThis); + if (result.url.len > 0) + error_instance.put(globalThis, ZigString.static("url"), ZigString.init(result.url).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("name"), ZigString.init(name).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("pkg"), ZigString.init(result.name).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("specifier"), ZigString.init(this.specifier).withEncoding().toJS(globalThis)); + const location = logger.rangeData(&this.parse_result.source, this.parse_result.ast.import_records.at(import_record_id).range, "").location.?; + error_instance.put(globalThis, ZigString.static("sourceURL"), ZigString.init(this.parse_result.source.path.text).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("line"), JSValue.jsNumber(location.line)); + if (location.line_text) |line_text| { + error_instance.put(globalThis, ZigString.static("lineText"), ZigString.init(line_text).withEncoding().toJS(globalThis)); + } + error_instance.put(globalThis, ZigString.static("column"), JSValue.jsNumber(location.column)); + if (this.referrer.len > 0 and !strings.eqlComptime(this.referrer, "undefined")) { + error_instance.put(globalThis, ZigString.static("referrer"), ZigString.init(this.referrer).withEncoding().toJS(globalThis)); + } + + const promise_value = this.promise.swap(); + var promise = promise_value.asInternalPromise().?; + promise_value.ensureStillAlive(); + this.poll_ref.unref(vm); + this.deinit(); + promise.rejectAsHandled(globalThis, error_instance); + } + pub fn downloadError(this: *AsyncModule, vm: *VirtualMachine, import_record_id: u32, result: PackageDownloadError) !void { + const globalThis = this.globalThis; + + const msg_args = .{ + result.name, + result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), + }; + + const msg: []u8 = try switch (result.err) { + error.TarballHTTP400 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 400 downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP401 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 401 downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP402 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 402 downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP403 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 403 downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP404 => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 404 downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP4xx => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 4xx downloading package '{s}@{any}'", + msg_args, + ), + error.TarballHTTP5xx => std.fmt.allocPrint( + bun.default_allocator, + "HTTP 5xx downloading package '{s}@{any}'", + msg_args, + ), + error.TarballFailedToExtract => std.fmt.allocPrint( + bun.default_allocator, + "Failed to extract tarball for package '{s}@{any}'", + msg_args, + ), + else => |err| std.fmt.allocPrint( + bun.default_allocator, + "{s} downloading package '{s}@{any}'", + .{ + bun.asByteSlice(@errorName(err)), + result.name, + result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), + }, + ), + }; + defer bun.default_allocator.free(msg); + + const name: []const u8 = switch (result.err) { + error.TarballFailedToExtract => "PackageExtractionError", + error.TarballHTTP403 => "TarballForbiddenError", + error.TarballHTTP404 => "TarballNotFoundError", + else => "TarballDownloadError", + }; + + var error_instance = ZigString.init(msg).withEncoding().toErrorInstance(globalThis); + if (result.url.len > 0) + error_instance.put(globalThis, ZigString.static("url"), ZigString.init(result.url).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("name"), ZigString.init(name).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("pkg"), ZigString.init(result.name).withEncoding().toJS(globalThis)); + if (this.specifier.len > 0 and !strings.eqlComptime(this.specifier, "undefined")) { + error_instance.put(globalThis, ZigString.static("referrer"), ZigString.init(this.specifier).withEncoding().toJS(globalThis)); + } + + const location = logger.rangeData(&this.parse_result.source, this.parse_result.ast.import_records.at(import_record_id).range, "").location.?; + error_instance.put(globalThis, ZigString.static("specifier"), ZigString.init( + this.parse_result.ast.import_records.at(import_record_id).path.text, + ).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("sourceURL"), ZigString.init(this.parse_result.source.path.text).withEncoding().toJS(globalThis)); + error_instance.put(globalThis, ZigString.static("line"), JSValue.jsNumber(location.line)); + if (location.line_text) |line_text| { + error_instance.put(globalThis, ZigString.static("lineText"), ZigString.init(line_text).withEncoding().toJS(globalThis)); + } + error_instance.put(globalThis, ZigString.static("column"), JSValue.jsNumber(location.column)); + + const promise_value = this.promise.swap(); + var promise = promise_value.asInternalPromise().?; + promise_value.ensureStillAlive(); + this.poll_ref.unref(vm); + this.deinit(); + promise.rejectAsHandled(globalThis, error_instance); + } + + pub fn resumeLoadingModule(this: *AsyncModule, log: *logger.Log) !ResolvedSource { + debug("resumeLoadingModule: {s}", .{this.specifier}); + var parse_result = this.parse_result; + const path = this.path; + var jsc_vm = VirtualMachine.get(); + const specifier = this.specifier; + const old_log = jsc_vm.log; + + jsc_vm.transpiler.linker.log = log; + jsc_vm.transpiler.log = log; + jsc_vm.transpiler.resolver.log = log; + jsc_vm.packageManager().log = log; + defer { + jsc_vm.transpiler.linker.log = old_log; + jsc_vm.transpiler.log = old_log; + jsc_vm.transpiler.resolver.log = old_log; + jsc_vm.packageManager().log = old_log; + } + + // We _must_ link because: + // - node_modules bundle won't be properly + try jsc_vm.transpiler.linker.link( + path, + &parse_result, + jsc_vm.origin, + .absolute_path, + false, + true, + ); + this.parse_result = parse_result; + + var printer = VirtualMachine.source_code_printer.?.*; + printer.ctx.reset(); + + { + var mapper = jsc_vm.sourceMapHandler(&printer); + defer VirtualMachine.source_code_printer.?.* = printer; + _ = try jsc_vm.transpiler.printWithSourceMap( + parse_result, + @TypeOf(&printer), + &printer, + .esm_ascii, + mapper.get(), + ); + } + + if (comptime Environment.dump_source) { + dumpSource(jsc_vm, specifier, &printer); + } + + if (jsc_vm.isWatcherEnabled()) { + var resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, bun.String.init(specifier), path.text, null, false); + + if (parse_result.input_fd) |fd_| { + if (std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + _ = jsc_vm.bun_watcher.addFile( + fd_, + path.text, + this.hash, + options.Loader.fromAPI(this.loader), + .invalid, + this.package_json, + true, + ); + } + } + + resolved_source.is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + + return resolved_source; + } + + return ResolvedSource{ + .allocator = null, + .source_code = bun.String.cloneLatin1(printer.ctx.getWritten()), + .specifier = String.init(specifier), + .source_url = String.init(path.text), + .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + }; + } + + pub fn deinit(this: *AsyncModule) void { + this.promise.deinit(); + this.parse_result.deinit(); + this.arena.deinit(); + this.globalThis.bunVM().allocator.destroy(this.arena); + // bun.default_allocator.free(this.stmt_blocks); + // bun.default_allocator.free(this.expr_blocks); + + bun.default_allocator.free(this.string_buf); + } + + extern "c" fn Bun__onFulfillAsyncModule( + globalObject: *JSGlobalObject, + promiseValue: JSValue, + res: *jsc.ErrorableResolvedSource, + specifier: *bun.String, + referrer: *bun.String, + ) void; +}; + +const Dependency = @import("../install/dependency.zig"); +const Fs = @import("../fs.zig"); +const options = @import("../options.zig"); +const std = @import("std"); +const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; +const dumpSource = @import("./RuntimeTranspilerStore.zig").dumpSource; + +const Install = @import("../install/install.zig"); +const PackageManager = @import("../install/install.zig").PackageManager; + +const bun = @import("bun"); +const Async = bun.Async; +const Environment = bun.Environment; +const Output = bun.Output; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const String = bun.String; +const logger = bun.logger; +const strings = bun.strings; +const ParseResult = bun.transpiler.ParseResult; +const api = bun.schema.api; + +const jsc = bun.jsc; +const JSGlobalObject = bun.jsc.JSGlobalObject; +const JSValue = bun.jsc.JSValue; +const ResolvedSource = bun.jsc.ResolvedSource; +const VirtualMachine = bun.jsc.VirtualMachine; +const ZigString = bun.jsc.ZigString; diff --git a/src/bun.js/HardcodedModule.zig b/src/bun.js/HardcodedModule.zig new file mode 100644 index 0000000000..698b400fb7 --- /dev/null +++ b/src/bun.js/HardcodedModule.zig @@ -0,0 +1,431 @@ +const string = []const u8; + +pub const HardcodedModule = enum { + bun, + @"abort-controller", + @"bun:app", + @"bun:ffi", + @"bun:jsc", + @"bun:main", + @"bun:test", + @"bun:wrap", + @"bun:sqlite", + @"node:assert", + @"node:assert/strict", + @"node:async_hooks", + @"node:buffer", + @"node:child_process", + @"node:console", + @"node:constants", + @"node:crypto", + @"node:dns", + @"node:dns/promises", + @"node:domain", + @"node:events", + @"node:fs", + @"node:fs/promises", + @"node:http", + @"node:https", + @"node:module", + @"node:net", + @"node:os", + @"node:path", + @"node:path/posix", + @"node:path/win32", + @"node:perf_hooks", + @"node:process", + @"node:querystring", + @"node:readline", + @"node:readline/promises", + @"node:stream", + @"node:stream/consumers", + @"node:stream/promises", + @"node:stream/web", + @"node:string_decoder", + @"node:test", + @"node:timers", + @"node:timers/promises", + @"node:tls", + @"node:tty", + @"node:url", + @"node:util", + @"node:util/types", + @"node:vm", + @"node:wasi", + @"node:zlib", + @"node:worker_threads", + @"node:punycode", + undici, + ws, + @"isomorphic-fetch", + @"node-fetch", + vercel_fetch, + @"utf-8-validate", + @"node:v8", + @"node:trace_events", + @"node:repl", + @"node:inspector", + @"node:http2", + @"node:diagnostics_channel", + @"node:dgram", + @"node:cluster", + @"node:_stream_duplex", + @"node:_stream_passthrough", + @"node:_stream_readable", + @"node:_stream_transform", + @"node:_stream_wrap", + @"node:_stream_writable", + @"node:_tls_common", + @"node:_http_agent", + @"node:_http_client", + @"node:_http_common", + @"node:_http_incoming", + @"node:_http_outgoing", + @"node:_http_server", + /// This is gated behind '--expose-internals' + @"bun:internal-for-testing", + + /// The module loader first uses `Aliases` to get a single string during + /// resolution, then maps that single string to the actual module. + /// Do not include aliases here; Those go in `Aliases`. + pub const map = bun.ComptimeStringMap(HardcodedModule, [_]struct { []const u8, HardcodedModule }{ + // Bun + .{ "bun", .bun }, + .{ "bun:app", .@"bun:app" }, + .{ "bun:ffi", .@"bun:ffi" }, + .{ "bun:jsc", .@"bun:jsc" }, + .{ "bun:main", .@"bun:main" }, + .{ "bun:test", .@"bun:test" }, + .{ "bun:sqlite", .@"bun:sqlite" }, + .{ "bun:wrap", .@"bun:wrap" }, + .{ "bun:internal-for-testing", .@"bun:internal-for-testing" }, + // Node.js + .{ "node:assert", .@"node:assert" }, + .{ "node:assert/strict", .@"node:assert/strict" }, + .{ "node:async_hooks", .@"node:async_hooks" }, + .{ "node:buffer", .@"node:buffer" }, + .{ "node:child_process", .@"node:child_process" }, + .{ "node:cluster", .@"node:cluster" }, + .{ "node:console", .@"node:console" }, + .{ "node:constants", .@"node:constants" }, + .{ "node:crypto", .@"node:crypto" }, + .{ "node:dgram", .@"node:dgram" }, + .{ "node:diagnostics_channel", .@"node:diagnostics_channel" }, + .{ "node:dns", .@"node:dns" }, + .{ "node:dns/promises", .@"node:dns/promises" }, + .{ "node:domain", .@"node:domain" }, + .{ "node:events", .@"node:events" }, + .{ "node:fs", .@"node:fs" }, + .{ "node:fs/promises", .@"node:fs/promises" }, + .{ "node:http", .@"node:http" }, + .{ "node:http2", .@"node:http2" }, + .{ "node:https", .@"node:https" }, + .{ "node:inspector", .@"node:inspector" }, + .{ "node:module", .@"node:module" }, + .{ "node:net", .@"node:net" }, + .{ "node:readline", .@"node:readline" }, + .{ "node:test", .@"node:test" }, + .{ "node:os", .@"node:os" }, + .{ "node:path", .@"node:path" }, + .{ "node:path/posix", .@"node:path/posix" }, + .{ "node:path/win32", .@"node:path/win32" }, + .{ "node:perf_hooks", .@"node:perf_hooks" }, + .{ "node:process", .@"node:process" }, + .{ "node:punycode", .@"node:punycode" }, + .{ "node:querystring", .@"node:querystring" }, + .{ "node:readline/promises", .@"node:readline/promises" }, + .{ "node:repl", .@"node:repl" }, + .{ "node:stream", .@"node:stream" }, + .{ "node:stream/consumers", .@"node:stream/consumers" }, + .{ "node:stream/promises", .@"node:stream/promises" }, + .{ "node:stream/web", .@"node:stream/web" }, + .{ "node:string_decoder", .@"node:string_decoder" }, + .{ "node:timers", .@"node:timers" }, + .{ "node:timers/promises", .@"node:timers/promises" }, + .{ "node:tls", .@"node:tls" }, + .{ "node:trace_events", .@"node:trace_events" }, + .{ "node:tty", .@"node:tty" }, + .{ "node:url", .@"node:url" }, + .{ "node:util", .@"node:util" }, + .{ "node:util/types", .@"node:util/types" }, + .{ "node:v8", .@"node:v8" }, + .{ "node:vm", .@"node:vm" }, + .{ "node:wasi", .@"node:wasi" }, + .{ "node:worker_threads", .@"node:worker_threads" }, + .{ "node:zlib", .@"node:zlib" }, + .{ "node:_stream_duplex", .@"node:_stream_duplex" }, + .{ "node:_stream_passthrough", .@"node:_stream_passthrough" }, + .{ "node:_stream_readable", .@"node:_stream_readable" }, + .{ "node:_stream_transform", .@"node:_stream_transform" }, + .{ "node:_stream_wrap", .@"node:_stream_wrap" }, + .{ "node:_stream_writable", .@"node:_stream_writable" }, + .{ "node:_tls_common", .@"node:_tls_common" }, + .{ "node:_http_agent", .@"node:_http_agent" }, + .{ "node:_http_client", .@"node:_http_client" }, + .{ "node:_http_common", .@"node:_http_common" }, + .{ "node:_http_incoming", .@"node:_http_incoming" }, + .{ "node:_http_outgoing", .@"node:_http_outgoing" }, + .{ "node:_http_server", .@"node:_http_server" }, + + .{ "node-fetch", HardcodedModule.@"node-fetch" }, + .{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" }, + .{ "undici", HardcodedModule.undici }, + .{ "ws", HardcodedModule.ws }, + .{ "@vercel/fetch", HardcodedModule.vercel_fetch }, + .{ "utf-8-validate", HardcodedModule.@"utf-8-validate" }, + .{ "abort-controller", HardcodedModule.@"abort-controller" }, + }); + + /// Contains the list of built-in modules from the perspective of the module + /// loader. This logic is duplicated for `isBuiltinModule` and the like. + pub const Alias = struct { + path: [:0]const u8, + tag: ImportRecord.Tag = .builtin, + node_builtin: bool = false, + node_only_prefix: bool = false, + + fn nodeEntry(comptime path: [:0]const u8) struct { string, Alias } { + return .{ + path, + .{ + .path = if (path.len > 5 and std.mem.eql(u8, path[0..5], "node:")) path else "node:" ++ path, + .node_builtin = true, + }, + }; + } + fn nodeEntryOnlyPrefix(comptime path: [:0]const u8) struct { string, Alias } { + return .{ + path, + .{ + .path = if (path.len > 5 and std.mem.eql(u8, path[0..5], "node:")) path else "node:" ++ path, + .node_builtin = true, + .node_only_prefix = true, + }, + }; + } + fn entry(comptime path: [:0]const u8) struct { string, Alias } { + return .{ path, .{ .path = path } }; + } + + // Applied to both --target=bun and --target=node + const common_alias_kvs = [_]struct { string, Alias }{ + nodeEntry("node:assert"), + nodeEntry("node:assert/strict"), + nodeEntry("node:async_hooks"), + nodeEntry("node:buffer"), + nodeEntry("node:child_process"), + nodeEntry("node:cluster"), + nodeEntry("node:console"), + nodeEntry("node:constants"), + nodeEntry("node:crypto"), + nodeEntry("node:dgram"), + nodeEntry("node:diagnostics_channel"), + nodeEntry("node:dns"), + nodeEntry("node:dns/promises"), + nodeEntry("node:domain"), + nodeEntry("node:events"), + nodeEntry("node:fs"), + nodeEntry("node:fs/promises"), + nodeEntry("node:http"), + nodeEntry("node:http2"), + nodeEntry("node:https"), + nodeEntry("node:inspector"), + nodeEntry("node:module"), + nodeEntry("node:net"), + nodeEntry("node:os"), + nodeEntry("node:path"), + nodeEntry("node:path/posix"), + nodeEntry("node:path/win32"), + nodeEntry("node:perf_hooks"), + nodeEntry("node:process"), + nodeEntry("node:punycode"), + nodeEntry("node:querystring"), + nodeEntry("node:readline"), + nodeEntry("node:readline/promises"), + nodeEntry("node:repl"), + nodeEntry("node:stream"), + nodeEntry("node:stream/consumers"), + nodeEntry("node:stream/promises"), + nodeEntry("node:stream/web"), + nodeEntry("node:string_decoder"), + nodeEntry("node:timers"), + nodeEntry("node:timers/promises"), + nodeEntry("node:tls"), + nodeEntry("node:trace_events"), + nodeEntry("node:tty"), + nodeEntry("node:url"), + nodeEntry("node:util"), + nodeEntry("node:util/types"), + nodeEntry("node:v8"), + nodeEntry("node:vm"), + nodeEntry("node:wasi"), + nodeEntry("node:worker_threads"), + nodeEntry("node:zlib"), + // New Node.js builtins only resolve from the prefixed one. + nodeEntryOnlyPrefix("node:test"), + + nodeEntry("assert"), + nodeEntry("assert/strict"), + nodeEntry("async_hooks"), + nodeEntry("buffer"), + nodeEntry("child_process"), + nodeEntry("cluster"), + nodeEntry("console"), + nodeEntry("constants"), + nodeEntry("crypto"), + nodeEntry("dgram"), + nodeEntry("diagnostics_channel"), + nodeEntry("dns"), + nodeEntry("dns/promises"), + nodeEntry("domain"), + nodeEntry("events"), + nodeEntry("fs"), + nodeEntry("fs/promises"), + nodeEntry("http"), + nodeEntry("http2"), + nodeEntry("https"), + nodeEntry("inspector"), + nodeEntry("module"), + nodeEntry("net"), + nodeEntry("os"), + nodeEntry("path"), + nodeEntry("path/posix"), + nodeEntry("path/win32"), + nodeEntry("perf_hooks"), + nodeEntry("process"), + nodeEntry("punycode"), + nodeEntry("querystring"), + nodeEntry("readline"), + nodeEntry("readline/promises"), + nodeEntry("repl"), + nodeEntry("stream"), + nodeEntry("stream/consumers"), + nodeEntry("stream/promises"), + nodeEntry("stream/web"), + nodeEntry("string_decoder"), + nodeEntry("timers"), + nodeEntry("timers/promises"), + nodeEntry("tls"), + nodeEntry("trace_events"), + nodeEntry("tty"), + nodeEntry("url"), + nodeEntry("util"), + nodeEntry("util/types"), + nodeEntry("v8"), + nodeEntry("vm"), + nodeEntry("wasi"), + nodeEntry("worker_threads"), + nodeEntry("zlib"), + + nodeEntry("node:_http_agent"), + nodeEntry("node:_http_client"), + nodeEntry("node:_http_common"), + nodeEntry("node:_http_incoming"), + nodeEntry("node:_http_outgoing"), + nodeEntry("node:_http_server"), + + nodeEntry("_http_agent"), + nodeEntry("_http_client"), + nodeEntry("_http_common"), + nodeEntry("_http_incoming"), + nodeEntry("_http_outgoing"), + nodeEntry("_http_server"), + + // sys is a deprecated alias for util + .{ "sys", .{ .path = "node:util", .node_builtin = true } }, + .{ "node:sys", .{ .path = "node:util", .node_builtin = true } }, + + // These are returned in builtinModules, but probably not many + // packages use them so we will just alias them. + .{ "node:_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } }, + .{ "node:_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } }, + .{ "node:_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } }, + .{ "node:_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } }, + .{ "node:_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } }, + .{ "node:_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } }, + .{ "node:_tls_wrap", .{ .path = "node:tls", .node_builtin = true } }, + .{ "node:_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } }, + .{ "_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } }, + .{ "_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } }, + .{ "_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } }, + .{ "_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } }, + .{ "_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } }, + .{ "_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } }, + .{ "_tls_wrap", .{ .path = "node:tls", .node_builtin = true } }, + .{ "_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } }, + }; + + const bun_extra_alias_kvs = [_]struct { string, Alias }{ + .{ "bun", .{ .path = "bun", .tag = .bun } }, + .{ "bun:test", .{ .path = "bun:test" } }, + .{ "bun:app", .{ .path = "bun:app" } }, + .{ "bun:ffi", .{ .path = "bun:ffi" } }, + .{ "bun:jsc", .{ .path = "bun:jsc" } }, + .{ "bun:sqlite", .{ .path = "bun:sqlite" } }, + .{ "bun:wrap", .{ .path = "bun:wrap" } }, + .{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } }, + .{ "ffi", .{ .path = "bun:ffi" } }, + + // inspector/promises is not implemented, it is an alias of inspector + .{ "node:inspector/promises", .{ .path = "node:inspector", .node_builtin = true } }, + .{ "inspector/promises", .{ .path = "node:inspector", .node_builtin = true } }, + + // Thirdparty packages we override + .{ "@vercel/fetch", .{ .path = "@vercel/fetch" } }, + .{ "isomorphic-fetch", .{ .path = "isomorphic-fetch" } }, + .{ "node-fetch", .{ .path = "node-fetch" } }, + .{ "undici", .{ .path = "undici" } }, + .{ "utf-8-validate", .{ .path = "utf-8-validate" } }, + .{ "ws", .{ .path = "ws" } }, + .{ "ws/lib/websocket", .{ .path = "ws" } }, + + // Polyfills we force to native + .{ "abort-controller", .{ .path = "abort-controller" } }, + .{ "abort-controller/polyfill", .{ .path = "abort-controller" } }, + + // To force Next.js to not use bundled dependencies. + .{ "next/dist/compiled/ws", .{ .path = "ws" } }, + .{ "next/dist/compiled/node-fetch", .{ .path = "node-fetch" } }, + .{ "next/dist/compiled/undici", .{ .path = "undici" } }, + }; + + const bun_test_extra_alias_kvs = [_]struct { string, Alias }{ + .{ "@jest/globals", .{ .path = "bun:test" } }, + .{ "vitest", .{ .path = "bun:test" } }, + }; + + const node_extra_alias_kvs = [_]struct { string, Alias }{ + nodeEntry("node:inspector/promises"), + nodeEntry("inspector/promises"), + }; + + const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_extra_alias_kvs); + pub const bun_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs); + const bun_test_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs ++ bun_test_extra_alias_kvs); + + const Cfg = struct { rewrite_jest_for_tests: bool = false }; + pub fn has(name: []const u8, target: options.Target, cfg: Cfg) bool { + return get(name, target, cfg) != null; + } + + pub fn get(name: []const u8, target: options.Target, cfg: Cfg) ?Alias { + if (target.isBun()) { + if (cfg.rewrite_jest_for_tests) { + return bun_test_aliases.get(name); + } else { + return bun_aliases.get(name); + } + } else if (target.isNode()) { + return node_aliases.get(name); + } + return null; + } + }; +}; + +const bun = @import("bun"); +const options = @import("../options.zig"); +const std = @import("std"); + +const ast = @import("../import_record.zig"); +const ImportRecord = ast.ImportRecord; diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index d1ce74545a..1cfaaedc6a 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -1,6 +1,9 @@ const ModuleLoader = @This(); pub const node_fallbacks = @import("../node_fallbacks.zig"); +pub const AsyncModule = @import("./AsyncModule.zig").AsyncModule; +pub const RuntimeTranspilerStore = @import("./RuntimeTranspilerStore.zig").RuntimeTranspilerStore; +pub const HardcodedModule = @import("./HardcodedModule.zig").HardcodedModule; transpile_source_code_arena: ?*bun.ArenaAllocator = null, eval_source: ?*logger.Source = null, @@ -66,745 +69,6 @@ pub fn resolveEmbeddedFile(vm: *VirtualMachine, input_path: []const u8, extname: return bun.path.joinAbs(bun.fs.FileSystem.instance.fs.tmpdirPath(), .auto, tmpfilename); } -pub const AsyncModule = struct { - // This is all the state used by the printer to print the module - parse_result: ParseResult, - promise: jsc.Strong.Optional = .empty, - path: Fs.Path, - specifier: string = "", - referrer: string = "", - string_buf: []u8 = &[_]u8{}, - fd: ?StoredFileDescriptorType = null, - package_json: ?*PackageJSON = null, - loader: api.Loader, - hash: u32 = std.math.maxInt(u32), - globalThis: *JSGlobalObject = undefined, - arena: *bun.ArenaAllocator, - - // This is the specific state for making it async - poll_ref: Async.KeepAlive = .{}, - any_task: jsc.AnyTask = undefined, - - pub const Id = u32; - - const PackageDownloadError = struct { - name: []const u8, - resolution: Install.Resolution, - err: anyerror, - url: []const u8, - }; - - const PackageResolveError = struct { - name: []const u8, - err: anyerror, - url: []const u8, - version: Dependency.Version, - }; - - pub const Queue = struct { - map: Map = .{}, - scheduled: u32 = 0, - concurrent_task_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - - const DeferredDependencyError = struct { - dependency: Dependency, - root_dependency_id: Install.DependencyID, - err: anyerror, - }; - - pub const Map = std.ArrayListUnmanaged(AsyncModule); - - pub fn enqueue(this: *Queue, globalObject: *JSGlobalObject, opts: anytype) void { - debug("enqueue: {s}", .{opts.specifier}); - var module = AsyncModule.init(opts, globalObject) catch unreachable; - module.poll_ref.ref(this.vm()); - - this.map.append(this.vm().allocator, module) catch unreachable; - this.vm().packageManager().drainDependencyList(); - } - - pub fn onDependencyError(ctx: *anyopaque, dependency: Dependency, root_dependency_id: Install.DependencyID, err: anyerror) void { - var this = bun.cast(*Queue, ctx); - debug("onDependencyError: {s}", .{this.vm().packageManager().lockfile.str(&dependency.name)}); - - var modules: []AsyncModule = this.map.items; - var i: usize = 0; - outer: for (modules) |module_| { - var module = module_; - const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); - for (root_dependency_ids, 0..) |dep, dep_i| { - if (dep != root_dependency_id) continue; - module.resolveError( - this.vm(), - module.parse_result.pending_imports.items(.import_record_id)[dep_i], - .{ - .name = this.vm().packageManager().lockfile.str(&dependency.name), - .err = err, - .url = "", - .version = dependency.version, - }, - ) catch unreachable; - continue :outer; - } - - modules[i] = module; - i += 1; - } - this.map.items.len = i; - } - pub fn onWakeHandler(ctx: *anyopaque, _: *PackageManager) void { - debug("onWake", .{}); - var this = bun.cast(*Queue, ctx); - this.vm().enqueueTaskConcurrent(jsc.ConcurrentTask.createFrom(this)); - } - - pub fn onPoll(this: *Queue) void { - debug("onPoll", .{}); - this.runTasks(); - this.pollModules(); - } - - pub fn runTasks(this: *Queue) void { - var pm = this.vm().packageManager(); - - if (Output.enable_ansi_colors_stderr) { - pm.startProgressBarIfNone(); - pm.runTasks( - *Queue, - this, - .{ - .onExtract = {}, - .onResolve = onResolve, - .onPackageManifestError = onPackageManifestError, - .onPackageDownloadError = onPackageDownloadError, - .progress_bar = true, - }, - true, - PackageManager.Options.LogLevel.default, - ) catch unreachable; - } else { - pm.runTasks( - *Queue, - this, - .{ - .onExtract = {}, - .onResolve = onResolve, - .onPackageManifestError = onPackageManifestError, - .onPackageDownloadError = onPackageDownloadError, - }, - true, - PackageManager.Options.LogLevel.default_no_progress, - ) catch unreachable; - } - } - - pub fn onResolve(_: *Queue) void { - debug("onResolve", .{}); - } - - pub fn onPackageManifestError( - this: *Queue, - name: []const u8, - err: anyerror, - url: []const u8, - ) void { - debug("onPackageManifestError: {s}", .{name}); - - var modules: []AsyncModule = this.map.items; - var i: usize = 0; - outer: for (modules) |module_| { - var module = module_; - const tags = module.parse_result.pending_imports.items(.tag); - for (tags, 0..) |tag, tag_i| { - if (tag == .resolve) { - const esms = module.parse_result.pending_imports.items(.esm); - const esm = esms[tag_i]; - const string_bufs = module.parse_result.pending_imports.items(.string_buf); - - if (!strings.eql(esm.name.slice(string_bufs[tag_i]), name)) continue; - - const versions = module.parse_result.pending_imports.items(.dependency); - - module.resolveError( - this.vm(), - module.parse_result.pending_imports.items(.import_record_id)[tag_i], - .{ - .name = name, - .err = err, - .url = url, - .version = versions[tag_i], - }, - ) catch unreachable; - continue :outer; - } - } - - modules[i] = module; - i += 1; - } - this.map.items.len = i; - } - - pub fn onPackageDownloadError( - this: *Queue, - package_id: Install.PackageID, - name: []const u8, - resolution: *const Install.Resolution, - err: anyerror, - url: []const u8, - ) void { - debug("onPackageDownloadError: {s}", .{name}); - - const resolution_ids = this.vm().packageManager().lockfile.buffers.resolutions.items; - var modules: []AsyncModule = this.map.items; - var i: usize = 0; - outer: for (modules) |module_| { - var module = module_; - const record_ids = module.parse_result.pending_imports.items(.import_record_id); - const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); - for (root_dependency_ids, 0..) |dependency_id, import_id| { - if (resolution_ids[dependency_id] != package_id) continue; - module.downloadError( - this.vm(), - record_ids[import_id], - .{ - .name = name, - .resolution = resolution.*, - .err = err, - .url = url, - }, - ) catch unreachable; - continue :outer; - } - - modules[i] = module; - i += 1; - } - this.map.items.len = i; - } - - pub fn pollModules(this: *Queue) void { - var pm = this.vm().packageManager(); - if (pm.pending_tasks.load(.monotonic) > 0) return; - - var modules: []AsyncModule = this.map.items; - var i: usize = 0; - - for (modules) |mod| { - var module = mod; - var tags = module.parse_result.pending_imports.items(.tag); - const root_dependency_ids = module.parse_result.pending_imports.items(.root_dependency_id); - // var esms = module.parse_result.pending_imports.items(.esm); - // var versions = module.parse_result.pending_imports.items(.dependency); - var done_count: usize = 0; - for (tags, 0..) |tag, tag_i| { - const root_id = root_dependency_ids[tag_i]; - const resolution_ids = pm.lockfile.buffers.resolutions.items; - if (root_id >= resolution_ids.len) continue; - const package_id = resolution_ids[root_id]; - - switch (tag) { - .resolve => { - if (package_id == Install.invalid_package_id) { - continue; - } - - // if we get here, the package has already been resolved. - tags[tag_i] = .download; - }, - .download => { - if (package_id == Install.invalid_package_id) { - unreachable; - } - }, - .done => { - done_count += 1; - continue; - }, - } - - if (package_id == Install.invalid_package_id) { - continue; - } - - const package = pm.lockfile.packages.get(package_id); - bun.assert(package.resolution.tag != .root); - - var name_and_version_hash: ?u64 = null; - var patchfile_hash: ?u64 = null; - switch (pm.determinePreinstallState(package, pm.lockfile, &name_and_version_hash, &patchfile_hash)) { - .done => { - // we are only truly done if all the dependencies are done. - const current_tasks = pm.total_tasks; - // so if enqueuing all the dependencies produces no new tasks, we are done. - pm.enqueueDependencyList(package.dependencies); - if (current_tasks == pm.total_tasks) { - tags[tag_i] = .done; - done_count += 1; - } - }, - .extracting => { - // we are extracting the package - // we need to wait for the next poll - continue; - }, - .extract => {}, - else => {}, - } - } - - if (done_count == tags.len) { - module.done(this.vm()); - } else { - modules[i] = module; - i += 1; - } - } - this.map.items.len = i; - if (i == 0) { - // ensure we always end the progress bar - this.vm().packageManager().endProgressBar(); - } - } - - pub fn vm(this: *Queue) *VirtualMachine { - return @alignCast(@fieldParentPtr("modules", this)); - } - }; - - pub fn init(opts: anytype, globalObject: *JSGlobalObject) !AsyncModule { - // var stmt_blocks = js_ast.Stmt.Data.toOwnedSlice(); - // var expr_blocks = js_ast.Expr.Data.toOwnedSlice(); - const this_promise = JSValue.createInternalPromise(globalObject); - const promise = jsc.Strong.Optional.create(this_promise, globalObject); - - var buf = bun.StringBuilder{}; - buf.count(opts.referrer); - buf.count(opts.specifier); - buf.count(opts.path.text); - - try buf.allocate(bun.default_allocator); - opts.promise_ptr.?.* = this_promise.asInternalPromise().?; - const referrer = buf.append(opts.referrer); - const specifier = buf.append(opts.specifier); - const path = Fs.Path.init(buf.append(opts.path.text)); - - return AsyncModule{ - .parse_result = opts.parse_result, - .promise = promise, - .path = path, - .specifier = specifier, - .referrer = referrer, - .fd = opts.fd, - .package_json = opts.package_json, - .loader = opts.loader.toAPI(), - .string_buf = buf.allocatedSlice(), - // .stmt_blocks = stmt_blocks, - // .expr_blocks = expr_blocks, - .globalThis = globalObject, - .arena = opts.arena, - }; - } - - pub fn done(this: *AsyncModule, jsc_vm: *VirtualMachine) void { - var clone = jsc_vm.allocator.create(AsyncModule) catch unreachable; - clone.* = this.*; - jsc_vm.modules.scheduled += 1; - clone.any_task = jsc.AnyTask.New(AsyncModule, onDone).init(clone); - jsc_vm.enqueueTask(jsc.Task.init(&clone.any_task)); - } - - pub fn onDone(this: *AsyncModule) void { - jsc.markBinding(@src()); - var jsc_vm = this.globalThis.bunVM(); - jsc_vm.modules.scheduled -= 1; - if (jsc_vm.modules.scheduled == 0) { - jsc_vm.packageManager().endProgressBar(); - } - var log = logger.Log.init(jsc_vm.allocator); - defer log.deinit(); - var errorable: jsc.ErrorableResolvedSource = undefined; - this.poll_ref.unref(jsc_vm); - outer: { - errorable = jsc.ErrorableResolvedSource.ok(this.resumeLoadingModule(&log) catch |err| { - switch (err) { - error.JSError => { - errorable = .err(error.JSError, this.globalThis.takeError(error.JSError)); - break :outer; - }, - else => { - VirtualMachine.processFetchLog( - this.globalThis, - bun.String.init(this.specifier), - bun.String.init(this.referrer), - &log, - &errorable, - err, - ); - break :outer; - }, - } - }); - } - - var spec = bun.String.init(ZigString.init(this.specifier).withEncoding()); - var ref = bun.String.init(ZigString.init(this.referrer).withEncoding()); - bun.jsc.fromJSHostCallGeneric(this.globalThis, @src(), Bun__onFulfillAsyncModule, .{ - this.globalThis, - this.promise.get().?, - &errorable, - &spec, - &ref, - }) catch {}; - this.deinit(); - jsc_vm.allocator.destroy(this); - } - - pub fn fulfill( - globalThis: *JSGlobalObject, - promise: JSValue, - resolved_source: *ResolvedSource, - err: ?anyerror, - specifier_: bun.String, - referrer_: bun.String, - log: *logger.Log, - ) bun.JSError!void { - jsc.markBinding(@src()); - var specifier = specifier_; - var referrer = referrer_; - var scope: jsc.CatchScope = undefined; - scope.init(globalThis, @src()); - defer { - specifier.deref(); - referrer.deref(); - scope.deinit(); - } - - var errorable: jsc.ErrorableResolvedSource = undefined; - if (err) |e| { - defer { - if (resolved_source.source_code_needs_deref) { - resolved_source.source_code_needs_deref = false; - resolved_source.source_code.deref(); - } - } - - if (e == error.JSError) { - errorable = jsc.ErrorableResolvedSource.err(error.JSError, globalThis.takeError(error.JSError)); - } else { - VirtualMachine.processFetchLog( - globalThis, - specifier, - referrer, - log, - &errorable, - e, - ); - } - } else { - errorable = jsc.ErrorableResolvedSource.ok(resolved_source.*); - } - log.deinit(); - - debug("fulfill: {any}", .{specifier}); - - try bun.jsc.fromJSHostCallGeneric(globalThis, @src(), Bun__onFulfillAsyncModule, .{ - globalThis, - promise, - &errorable, - &specifier, - &referrer, - }); - } - - pub fn resolveError(this: *AsyncModule, vm: *VirtualMachine, import_record_id: u32, result: PackageResolveError) !void { - const globalThis = this.globalThis; - - const msg: []u8 = try switch (result.err) { - error.PackageManifestHTTP400 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 400 while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.PackageManifestHTTP401 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 401 while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.PackageManifestHTTP402 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 402 while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.PackageManifestHTTP403 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 403 while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.PackageManifestHTTP404 => std.fmt.allocPrint( - bun.default_allocator, - "Package '{s}' was not found", - .{result.name}, - ), - error.PackageManifestHTTP4xx => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 4xx while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.PackageManifestHTTP5xx => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 5xx while resolving package '{s}' at '{s}'", - .{ result.name, result.url }, - ), - error.DistTagNotFound, error.NoMatchingVersion => brk: { - const prefix: []const u8 = if (result.err == error.NoMatchingVersion and result.version.tag == .npm and result.version.value.npm.version.isExact()) - "Version not found" - else if (result.version.tag == .npm and !result.version.value.npm.version.isExact()) - "No matching version found" - else - "No match found"; - - break :brk std.fmt.allocPrint( - bun.default_allocator, - "{s} '{s}' for package '{s}' (but package exists)", - .{ prefix, vm.packageManager().lockfile.str(&result.version.literal), result.name }, - ); - }, - else => |err| std.fmt.allocPrint( - bun.default_allocator, - "{s} resolving package '{s}' at '{s}'", - .{ bun.asByteSlice(@errorName(err)), result.name, result.url }, - ), - }; - - const name: []const u8 = switch (result.err) { - error.NoMatchingVersion => "PackageVersionNotFound", - error.DistTagNotFound => "PackageTagNotFound", - error.PackageManifestHTTP403 => "PackageForbidden", - error.PackageManifestHTTP404 => "PackageNotFound", - else => "PackageResolveError", - }; - - var error_instance = ZigString.init(msg).withEncoding().toErrorInstance(globalThis); - if (result.url.len > 0) - error_instance.put(globalThis, ZigString.static("url"), ZigString.init(result.url).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("name"), ZigString.init(name).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("pkg"), ZigString.init(result.name).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("specifier"), ZigString.init(this.specifier).withEncoding().toJS(globalThis)); - const location = logger.rangeData(&this.parse_result.source, this.parse_result.ast.import_records.at(import_record_id).range, "").location.?; - error_instance.put(globalThis, ZigString.static("sourceURL"), ZigString.init(this.parse_result.source.path.text).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("line"), JSValue.jsNumber(location.line)); - if (location.line_text) |line_text| { - error_instance.put(globalThis, ZigString.static("lineText"), ZigString.init(line_text).withEncoding().toJS(globalThis)); - } - error_instance.put(globalThis, ZigString.static("column"), JSValue.jsNumber(location.column)); - if (this.referrer.len > 0 and !strings.eqlComptime(this.referrer, "undefined")) { - error_instance.put(globalThis, ZigString.static("referrer"), ZigString.init(this.referrer).withEncoding().toJS(globalThis)); - } - - const promise_value = this.promise.swap(); - var promise = promise_value.asInternalPromise().?; - promise_value.ensureStillAlive(); - this.poll_ref.unref(vm); - this.deinit(); - promise.rejectAsHandled(globalThis, error_instance); - } - pub fn downloadError(this: *AsyncModule, vm: *VirtualMachine, import_record_id: u32, result: PackageDownloadError) !void { - const globalThis = this.globalThis; - - const msg_args = .{ - result.name, - result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), - }; - - const msg: []u8 = try switch (result.err) { - error.TarballHTTP400 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 400 downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP401 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 401 downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP402 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 402 downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP403 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 403 downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP404 => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 404 downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP4xx => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 4xx downloading package '{s}@{any}'", - msg_args, - ), - error.TarballHTTP5xx => std.fmt.allocPrint( - bun.default_allocator, - "HTTP 5xx downloading package '{s}@{any}'", - msg_args, - ), - error.TarballFailedToExtract => std.fmt.allocPrint( - bun.default_allocator, - "Failed to extract tarball for package '{s}@{any}'", - msg_args, - ), - else => |err| std.fmt.allocPrint( - bun.default_allocator, - "{s} downloading package '{s}@{any}'", - .{ - bun.asByteSlice(@errorName(err)), - result.name, - result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), - }, - ), - }; - - const name: []const u8 = switch (result.err) { - error.TarballFailedToExtract => "PackageExtractionError", - error.TarballHTTP403 => "TarballForbiddenError", - error.TarballHTTP404 => "TarballNotFoundError", - else => "TarballDownloadError", - }; - - var error_instance = ZigString.init(msg).withEncoding().toErrorInstance(globalThis); - if (result.url.len > 0) - error_instance.put(globalThis, ZigString.static("url"), ZigString.init(result.url).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("name"), ZigString.init(name).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("pkg"), ZigString.init(result.name).withEncoding().toJS(globalThis)); - if (this.specifier.len > 0 and !strings.eqlComptime(this.specifier, "undefined")) { - error_instance.put(globalThis, ZigString.static("referrer"), ZigString.init(this.specifier).withEncoding().toJS(globalThis)); - } - - const location = logger.rangeData(&this.parse_result.source, this.parse_result.ast.import_records.at(import_record_id).range, "").location.?; - error_instance.put(globalThis, ZigString.static("specifier"), ZigString.init( - this.parse_result.ast.import_records.at(import_record_id).path.text, - ).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("sourceURL"), ZigString.init(this.parse_result.source.path.text).withEncoding().toJS(globalThis)); - error_instance.put(globalThis, ZigString.static("line"), JSValue.jsNumber(location.line)); - if (location.line_text) |line_text| { - error_instance.put(globalThis, ZigString.static("lineText"), ZigString.init(line_text).withEncoding().toJS(globalThis)); - } - error_instance.put(globalThis, ZigString.static("column"), JSValue.jsNumber(location.column)); - - const promise_value = this.promise.swap(); - var promise = promise_value.asInternalPromise().?; - promise_value.ensureStillAlive(); - this.poll_ref.unref(vm); - this.deinit(); - promise.rejectAsHandled(globalThis, error_instance); - } - - pub fn resumeLoadingModule(this: *AsyncModule, log: *logger.Log) !ResolvedSource { - debug("resumeLoadingModule: {s}", .{this.specifier}); - var parse_result = this.parse_result; - const path = this.path; - var jsc_vm = VirtualMachine.get(); - const specifier = this.specifier; - const old_log = jsc_vm.log; - - jsc_vm.transpiler.linker.log = log; - jsc_vm.transpiler.log = log; - jsc_vm.transpiler.resolver.log = log; - jsc_vm.packageManager().log = log; - defer { - jsc_vm.transpiler.linker.log = old_log; - jsc_vm.transpiler.log = old_log; - jsc_vm.transpiler.resolver.log = old_log; - jsc_vm.packageManager().log = old_log; - } - - // We _must_ link because: - // - node_modules bundle won't be properly - try jsc_vm.transpiler.linker.link( - path, - &parse_result, - jsc_vm.origin, - .absolute_path, - false, - true, - ); - this.parse_result = parse_result; - - var printer = VirtualMachine.source_code_printer.?.*; - printer.ctx.reset(); - - { - var mapper = jsc_vm.sourceMapHandler(&printer); - defer VirtualMachine.source_code_printer.?.* = printer; - _ = try jsc_vm.transpiler.printWithSourceMap( - parse_result, - @TypeOf(&printer), - &printer, - .esm_ascii, - mapper.get(), - ); - } - - if (comptime Environment.dump_source) { - dumpSource(jsc_vm, specifier, &printer); - } - - if (jsc_vm.isWatcherEnabled()) { - var resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, bun.String.init(specifier), path.text, null, false); - - if (parse_result.input_fd) |fd_| { - if (std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { - _ = jsc_vm.bun_watcher.addFile( - fd_, - path.text, - this.hash, - options.Loader.fromAPI(this.loader), - .invalid, - this.package_json, - true, - ); - } - } - - resolved_source.is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; - - return resolved_source; - } - - return ResolvedSource{ - .allocator = null, - .source_code = bun.String.cloneLatin1(printer.ctx.getWritten()), - .specifier = String.init(specifier), - .source_url = String.init(path.text), - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, - }; - } - - pub fn deinit(this: *AsyncModule) void { - this.promise.deinit(); - this.parse_result.deinit(); - this.arena.deinit(); - this.globalThis.bunVM().allocator.destroy(this.arena); - // bun.default_allocator.free(this.stmt_blocks); - // bun.default_allocator.free(this.expr_blocks); - - bun.default_allocator.free(this.string_buf); - } - - extern "c" fn Bun__onFulfillAsyncModule( - globalObject: *JSGlobalObject, - promiseValue: JSValue, - res: *jsc.ErrorableResolvedSource, - specifier: *bun.String, - referrer: *bun.String, - ) void; -}; - pub export fn Bun__getDefaultLoader(global: *JSGlobalObject, str: *const bun.String) api.Loader { var jsc_vm = global.bunVM(); const filename = str.toUTF8(jsc_vm.allocator); @@ -2025,586 +1289,6 @@ inline fn jsSyntheticModule(name: ResolvedSource.Tag, specifier: String) Resolve /// /// This can technically fail if concurrent access across processes happens, or permission issues. /// Errors here should always be ignored. -fn dumpSource(vm: *VirtualMachine, specifier: string, printer: anytype) void { - dumpSourceString(vm, specifier, printer.ctx.getWritten()); -} - -fn dumpSourceString(vm: *VirtualMachine, specifier: string, written: []const u8) void { - dumpSourceStringFailiable(vm, specifier, written) catch |e| { - Output.debugWarn("Failed to dump source string: {}", .{e}); - }; -} - -fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written: []const u8) !void { - if (!Environment.isDebug) return; - if (bun.feature_flag.BUN_DEBUG_NO_DUMP.get()) return; - - const BunDebugHolder = struct { - pub var dir: ?std.fs.Dir = null; - pub var lock: bun.Mutex = .{}; - }; - - BunDebugHolder.lock.lock(); - defer BunDebugHolder.lock.unlock(); - - const dir = BunDebugHolder.dir orelse dir: { - const base_name = switch (Environment.os) { - else => "/tmp/bun-debug-src/", - .windows => brk: { - const temp = bun.fs.FileSystem.RealFS.platformTempDir(); - var win_temp_buffer: bun.PathBuffer = undefined; - @memcpy(win_temp_buffer[0..temp.len], temp); - const suffix = "\\bun-debug-src"; - @memcpy(win_temp_buffer[temp.len .. temp.len + suffix.len], suffix); - win_temp_buffer[temp.len + suffix.len] = 0; - break :brk win_temp_buffer[0 .. temp.len + suffix.len :0]; - }, - }; - const dir = try std.fs.cwd().makeOpenPath(base_name, .{}); - BunDebugHolder.dir = dir; - break :dir dir; - }; - - if (std.fs.path.dirname(specifier)) |dir_path| { - const root_len = switch (Environment.os) { - else => "/".len, - .windows => bun.path.windowsFilesystemRoot(dir_path).len, - }; - var parent = try dir.makeOpenPath(dir_path[root_len..], .{}); - defer parent.close(); - parent.writeFile(.{ - .sub_path = std.fs.path.basename(specifier), - .data = written, - }) catch |e| { - Output.debugWarn("Failed to dump source string: writeFile {}", .{e}); - return; - }; - if (vm.source_mappings.get(specifier)) |mappings| { - defer mappings.deref(); - const map_path = bun.handleOom(std.mem.concat(bun.default_allocator, u8, &.{ std.fs.path.basename(specifier), ".map" })); - defer bun.default_allocator.free(map_path); - const file = try parent.createFile(map_path, .{}); - defer file.close(); - - const source_file = parent.readFileAlloc( - bun.default_allocator, - specifier, - std.math.maxInt(u64), - ) catch ""; - defer bun.default_allocator.free(source_file); - - var bufw = std.io.bufferedWriter(file.writer()); - const w = bufw.writer(); - try w.print( - \\{{ - \\ "version": 3, - \\ "file": {}, - \\ "sourceRoot": "", - \\ "sources": [{}], - \\ "sourcesContent": [{}], - \\ "names": [], - \\ "mappings": "{}" - \\}} - , .{ - bun.fmt.formatJSONStringUTF8(std.fs.path.basename(specifier), .{}), - bun.fmt.formatJSONStringUTF8(specifier, .{}), - bun.fmt.formatJSONStringUTF8(source_file, .{}), - mappings.formatVLQs(), - }); - try bufw.flush(); - } - } else { - dir.writeFile(.{ - .sub_path = std.fs.path.basename(specifier), - .data = written, - }) catch return; - } -} - -fn setBreakPointOnFirstLine() bool { - const s = struct { - var set_break_point: bool = true; - }; - const ret = s.set_break_point; - s.set_break_point = false; - return ret; -} - -pub const RuntimeTranspilerStore = struct { - generation_number: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - store: TranspilerJob.Store, - enabled: bool = true, - queue: Queue = Queue{}, - - pub const Queue = bun.UnboundedQueue(TranspilerJob, .next); - - pub fn init() RuntimeTranspilerStore { - return RuntimeTranspilerStore{ - .store = TranspilerJob.Store.init(bun.typedAllocator(TranspilerJob)), - }; - } - - pub fn runFromJSThread(this: *RuntimeTranspilerStore, event_loop: *jsc.EventLoop, global: *jsc.JSGlobalObject, vm: *jsc.VirtualMachine) void { - var batch = this.queue.popBatch(); - const jsc_vm = vm.jsc_vm; - var iter = batch.iterator(); - if (iter.next()) |job| { - // we run just one job first to see if there are more - job.runFromJSThread() catch |err| global.reportUncaughtExceptionFromError(err); - } else { - return; - } - while (iter.next()) |job| { - // if there are more, we need to drain the microtasks from the previous run - event_loop.drainMicrotasksWithGlobal(global, jsc_vm) catch return; - job.runFromJSThread() catch |err| global.reportUncaughtExceptionFromError(err); - } - - // immediately after this is called, the microtasks will be drained again. - } - - pub fn transpile( - this: *RuntimeTranspilerStore, - vm: *VirtualMachine, - globalObject: *JSGlobalObject, - input_specifier: bun.String, - path: Fs.Path, - referrer: bun.String, - loader: bun.options.Loader, - package_json: ?*const PackageJSON, - ) *anyopaque { - var job: *TranspilerJob = this.store.get(); - const owned_path = Fs.Path.init(bun.default_allocator.dupe(u8, path.text) catch unreachable); - const promise = jsc.JSInternalPromise.create(globalObject); - - // NOTE: DirInfo should already be cached since module loading happens - // after module resolution, so this should be cheap - var resolved_source = ResolvedSource{}; - if (package_json) |pkg| { - switch (pkg.module_type) { - .cjs => { - resolved_source.tag = .package_json_type_commonjs; - resolved_source.is_commonjs_module = true; - }, - .esm => resolved_source.tag = .package_json_type_module, - .unknown => {}, - } - } - - job.* = TranspilerJob{ - .non_threadsafe_input_specifier = input_specifier, - .path = owned_path, - .globalThis = globalObject, - .non_threadsafe_referrer = referrer, - .vm = vm, - .log = logger.Log.init(bun.default_allocator), - .loader = loader, - .promise = .create(JSValue.fromCell(promise), globalObject), - .poll_ref = .{}, - .fetcher = TranspilerJob.Fetcher{ - .file = {}, - }, - .resolved_source = resolved_source, - }; - if (comptime Environment.allow_assert) - debug("transpile({s}, {s}, async)", .{ path.text, @tagName(job.loader) }); - job.schedule(); - return promise; - } - - pub const TranspilerJob = struct { - path: Fs.Path, - non_threadsafe_input_specifier: String, - non_threadsafe_referrer: String, - loader: options.Loader, - promise: jsc.Strong.Optional = .empty, - vm: *VirtualMachine, - globalThis: *JSGlobalObject, - fetcher: Fetcher, - poll_ref: Async.KeepAlive = .{}, - generation_number: u32 = 0, - log: logger.Log, - parse_error: ?anyerror = null, - resolved_source: ResolvedSource = ResolvedSource{}, - work_task: jsc.WorkPoolTask = .{ .callback = runFromWorkerThread }, - next: ?*TranspilerJob = null, - - pub const Store = bun.HiveArray(TranspilerJob, if (bun.heap_breakdown.enabled) 0 else 64).Fallback; - - pub const Fetcher = union(enum) { - virtual_module: bun.String, - file: void, - - pub fn deinit(this: *@This()) void { - if (this.* == .virtual_module) { - this.virtual_module.deref(); - } - } - }; - - pub fn deinit(this: *TranspilerJob) void { - bun.default_allocator.free(this.path.text); - - this.poll_ref.disable(); - this.fetcher.deinit(); - this.loader = options.Loader.file; - this.non_threadsafe_input_specifier.deref(); - this.non_threadsafe_referrer.deref(); - this.path = Fs.Path.empty; - this.log.deinit(); - this.promise.deinit(); - this.globalThis = undefined; - } - - threadlocal var ast_memory_store: ?*js_ast.ASTMemoryAllocator = null; - threadlocal var source_code_printer: ?*js_printer.BufferPrinter = null; - - pub fn dispatchToMainThread(this: *TranspilerJob) void { - this.vm.transpiler_store.queue.push(this); - this.vm.eventLoop().enqueueTaskConcurrent(jsc.ConcurrentTask.createFrom(&this.vm.transpiler_store)); - } - - pub fn runFromJSThread(this: *TranspilerJob) bun.JSError!void { - var vm = this.vm; - const promise = this.promise.swap(); - const globalThis = this.globalThis; - this.poll_ref.unref(vm); - - const referrer = this.non_threadsafe_referrer; - this.non_threadsafe_referrer = String.empty; - var log = this.log; - this.log = logger.Log.init(bun.default_allocator); - var resolved_source = this.resolved_source; - const specifier = brk: { - if (this.parse_error != null) { - break :brk bun.String.cloneUTF8(this.path.text); - } - - const out = this.non_threadsafe_input_specifier; - this.non_threadsafe_input_specifier = String.empty; - - bun.debugAssert(resolved_source.source_url.isEmpty()); - bun.debugAssert(resolved_source.specifier.isEmpty()); - resolved_source.source_url = out.createIfDifferent(this.path.text); - resolved_source.specifier = out.dupeRef(); - break :brk out; - }; - - const parse_error = this.parse_error; - - this.promise.deinit(); - this.deinit(); - - _ = vm.transpiler_store.store.put(this); - - try ModuleLoader.AsyncModule.fulfill(globalThis, promise, &resolved_source, parse_error, specifier, referrer, &log); - } - - pub fn schedule(this: *TranspilerJob) void { - this.poll_ref.ref(this.vm); - jsc.WorkPool.schedule(&this.work_task); - } - - pub fn runFromWorkerThread(work_task: *jsc.WorkPoolTask) void { - @as(*TranspilerJob, @fieldParentPtr("work_task", work_task)).run(); - } - - pub fn run(this: *TranspilerJob) void { - var arena = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - defer this.dispatchToMainThread(); - if (this.generation_number != this.vm.transpiler_store.generation_number.load(.monotonic)) { - this.parse_error = error.TranspilerJobGenerationMismatch; - return; - } - - if (ast_memory_store == null) { - ast_memory_store = bun.handleOom(bun.default_allocator.create(js_ast.ASTMemoryAllocator)); - ast_memory_store.?.* = js_ast.ASTMemoryAllocator{ - .allocator = allocator, - .previous = null, - }; - } - - var ast_scope = ast_memory_store.?.enter(allocator); - defer ast_scope.exit(); - - const path = this.path; - const specifier = this.path.text; - const loader = this.loader; - - var cache = jsc.RuntimeTranspilerCache{ - .output_code_allocator = allocator, - .sourcemap_allocator = bun.default_allocator, - }; - var log = logger.Log.init(allocator); - defer { - this.log = logger.Log.init(bun.default_allocator); - bun.handleOom(log.cloneToWithRecycled(&this.log, true)); - } - var vm = this.vm; - var transpiler: bun.Transpiler = undefined; - transpiler = vm.transpiler; - transpiler.setAllocator(allocator); - transpiler.setLog(&log); - transpiler.resolver.opts = transpiler.options; - transpiler.macro_context = null; - transpiler.linker.resolver = &transpiler.resolver; - - var fd: ?StoredFileDescriptorType = null; - var package_json: ?*PackageJSON = null; - const hash = bun.Watcher.getHash(path.text); - - switch (vm.bun_watcher) { - .hot, .watch => { - if (vm.bun_watcher.indexOf(hash)) |index| { - const watcher_fd = vm.bun_watcher.watchlist().items(.fd)[index]; - fd = if (watcher_fd.stdioTag() == null) watcher_fd else null; - package_json = vm.bun_watcher.watchlist().items(.package_json)[index]; - } - }, - else => {}, - } - - // this should be a cheap lookup because 24 bytes == 8 * 3 so it's read 3 machine words - const is_node_override = strings.hasPrefixComptime(specifier, node_fallbacks.import_path); - - const macro_remappings = if (vm.macro_mode or !vm.has_any_macro_remappings or is_node_override) - MacroRemap{} - else - transpiler.options.macro_remap; - - var fallback_source: logger.Source = undefined; - - // Usually, we want to close the input file automatically. - // - // If we're re-using the file descriptor from the fs watcher - // Do not close it because that will break the kqueue-based watcher - // - var should_close_input_file_fd = fd == null; - - var input_file_fd: StoredFileDescriptorType = .invalid; - - const is_main = vm.main.len == path.text.len and - vm.main_hash == hash and - strings.eqlLong(vm.main, path.text, false); - - const module_type: ModuleType = switch (this.resolved_source.tag) { - .package_json_type_commonjs => .cjs, - .package_json_type_module => .esm, - else => .unknown, - }; - - var parse_options = Transpiler.ParseOptions{ - .allocator = allocator, - .path = path, - .loader = loader, - .dirname_fd = .invalid, - .file_descriptor = fd, - .file_fd_ptr = &input_file_fd, - .file_hash = hash, - .macro_remappings = macro_remappings, - .jsx = transpiler.options.jsx, - .emit_decorator_metadata = transpiler.options.emit_decorator_metadata, - .virtual_source = null, - .dont_bundle_twice = true, - .allow_commonjs = true, - .inject_jest_globals = transpiler.options.rewrite_jest_for_tests, - .set_breakpoint_on_first_line = vm.debugger != null and - vm.debugger.?.set_breakpoint_on_first_line and - is_main and - setBreakPointOnFirstLine(), - .runtime_transpiler_cache = if (!jsc.RuntimeTranspilerCache.is_disabled) &cache else null, - .remove_cjs_module_wrapper = is_main and vm.module_loader.eval_source != null, - .module_type = module_type, - .allow_bytecode_cache = true, - }; - - defer { - if (should_close_input_file_fd and input_file_fd.isValid()) { - input_file_fd.close(); - input_file_fd = .invalid; - } - } - - if (is_node_override) { - if (node_fallbacks.contentsFromPath(specifier)) |code| { - const fallback_path = Fs.Path.initWithNamespace(specifier, "node"); - fallback_source = logger.Source{ .path = fallback_path, .contents = code }; - parse_options.virtual_source = &fallback_source; - } - } - - var parse_result: bun.transpiler.ParseResult = transpiler.parseMaybeReturnFileOnlyAllowSharedBuffer( - parse_options, - null, - false, - false, - ) orelse { - if (vm.isWatcherEnabled()) { - if (input_file_fd.isValid()) { - if (!is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { - should_close_input_file_fd = false; - _ = vm.bun_watcher.addFile( - input_file_fd, - path.text, - hash, - loader, - .invalid, - package_json, - true, - ); - } - } - } - - this.parse_error = error.ParseError; - - return; - }; - - if (vm.isWatcherEnabled()) { - if (input_file_fd.isValid()) { - if (!is_node_override and - std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) - { - should_close_input_file_fd = false; - _ = vm.bun_watcher.addFile( - input_file_fd, - path.text, - hash, - loader, - .invalid, - package_json, - true, - ); - } - } - } - - if (cache.entry) |*entry| { - vm.source_mappings.putMappings(&parse_result.source, .{ - .list = .{ .items = @constCast(entry.sourcemap), .capacity = entry.sourcemap.len }, - .allocator = bun.default_allocator, - }) catch {}; - - if (comptime Environment.dump_source) { - dumpSourceString(vm, specifier, entry.output_code.byteSlice()); - } - - this.resolved_source = ResolvedSource{ - .allocator = null, - .source_code = switch (entry.output_code) { - .string => entry.output_code.string, - .utf8 => brk: { - const result = bun.String.cloneUTF8(entry.output_code.utf8); - cache.output_code_allocator.free(entry.output_code.utf8); - entry.output_code.utf8 = ""; - break :brk result; - }, - }, - .is_commonjs_module = entry.metadata.module_type == .cjs, - .tag = this.resolved_source.tag, - }; - - return; - } - - if (parse_result.already_bundled != .none) { - const bytecode_slice = parse_result.already_bundled.bytecodeSlice(); - this.resolved_source = ResolvedSource{ - .allocator = null, - .source_code = bun.String.cloneLatin1(parse_result.source.contents), - .already_bundled = true, - .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, - .bytecode_cache_size = bytecode_slice.len, - .is_commonjs_module = parse_result.already_bundled.isCommonJS(), - .tag = this.resolved_source.tag, - }; - this.resolved_source.source_code.ensureHash(); - return; - } - - for (parse_result.ast.import_records.slice()) |*import_record_| { - var import_record: *bun.ImportRecord = import_record_; - - if (jsc.ModuleLoader.HardcodedModule.Alias.get(import_record.path.text, transpiler.options.target, .{ .rewrite_jest_for_tests = transpiler.options.rewrite_jest_for_tests })) |replacement| { - import_record.path.text = replacement.path; - import_record.tag = replacement.tag; - import_record.is_external_without_side_effects = true; - continue; - } - - if (strings.hasPrefixComptime(import_record.path.text, "bun:")) { - import_record.path = Fs.Path.init(import_record.path.text["bun:".len..]); - import_record.path.namespace = "bun"; - import_record.is_external_without_side_effects = true; - } - } - - if (source_code_printer == null) { - const writer = js_printer.BufferWriter.init(bun.default_allocator); - source_code_printer = bun.default_allocator.create(js_printer.BufferPrinter) catch unreachable; - source_code_printer.?.* = js_printer.BufferPrinter.init(writer); - source_code_printer.?.ctx.append_null_byte = false; - } - - var printer = source_code_printer.?.*; - printer.ctx.reset(); - - { - var mapper = vm.sourceMapHandler(&printer); - defer source_code_printer.?.* = printer; - _ = transpiler.printWithSourceMap( - parse_result, - @TypeOf(&printer), - &printer, - .esm_ascii, - mapper.get(), - ) catch |err| { - this.parse_error = err; - return; - }; - } - - if (comptime Environment.dump_source) { - dumpSource(this.vm, specifier, &printer); - } - - const source_code = brk: { - const written = printer.ctx.getWritten(); - - const result = cache.output_code orelse bun.String.cloneLatin1(written); - - if (written.len > 1024 * 1024 * 2 or vm.smol) { - printer.ctx.buffer.deinit(); - source_code_printer.?.* = printer; - } - - // In a benchmarking loading @babel/standalone 100 times: - // - // After ensureHash: - // 354.00 ms 4.2% 354.00 ms WTF::StringImpl::hashSlowCase() const - // - // Before ensureHash: - // 506.00 ms 6.1% 506.00 ms WTF::StringImpl::hashSlowCase() const - // - result.ensureHash(); - - break :brk result; - }; - this.resolved_source = ResolvedSource{ - .allocator = null, - .source_code = source_code, - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, - .tag = this.resolved_source.tag, - }; - } - }; -}; - pub const FetchFlags = enum { transpile, print_source, @@ -2615,430 +1299,6 @@ pub const FetchFlags = enum { } }; -pub const HardcodedModule = enum { - bun, - @"abort-controller", - @"bun:app", - @"bun:ffi", - @"bun:jsc", - @"bun:main", - @"bun:test", - @"bun:wrap", - @"bun:sqlite", - @"node:assert", - @"node:assert/strict", - @"node:async_hooks", - @"node:buffer", - @"node:child_process", - @"node:console", - @"node:constants", - @"node:crypto", - @"node:dns", - @"node:dns/promises", - @"node:domain", - @"node:events", - @"node:fs", - @"node:fs/promises", - @"node:http", - @"node:https", - @"node:module", - @"node:net", - @"node:os", - @"node:path", - @"node:path/posix", - @"node:path/win32", - @"node:perf_hooks", - @"node:process", - @"node:querystring", - @"node:readline", - @"node:readline/promises", - @"node:stream", - @"node:stream/consumers", - @"node:stream/promises", - @"node:stream/web", - @"node:string_decoder", - @"node:test", - @"node:timers", - @"node:timers/promises", - @"node:tls", - @"node:tty", - @"node:url", - @"node:util", - @"node:util/types", - @"node:vm", - @"node:wasi", - @"node:zlib", - @"node:worker_threads", - @"node:punycode", - undici, - ws, - @"isomorphic-fetch", - @"node-fetch", - vercel_fetch, - @"utf-8-validate", - @"node:v8", - @"node:trace_events", - @"node:repl", - @"node:inspector", - @"node:http2", - @"node:diagnostics_channel", - @"node:dgram", - @"node:cluster", - @"node:_stream_duplex", - @"node:_stream_passthrough", - @"node:_stream_readable", - @"node:_stream_transform", - @"node:_stream_wrap", - @"node:_stream_writable", - @"node:_tls_common", - @"node:_http_agent", - @"node:_http_client", - @"node:_http_common", - @"node:_http_incoming", - @"node:_http_outgoing", - @"node:_http_server", - /// This is gated behind '--expose-internals' - @"bun:internal-for-testing", - - /// The module loader first uses `Aliases` to get a single string during - /// resolution, then maps that single string to the actual module. - /// Do not include aliases here; Those go in `Aliases`. - pub const map = bun.ComptimeStringMap(HardcodedModule, [_]struct { []const u8, HardcodedModule }{ - // Bun - .{ "bun", .bun }, - .{ "bun:app", .@"bun:app" }, - .{ "bun:ffi", .@"bun:ffi" }, - .{ "bun:jsc", .@"bun:jsc" }, - .{ "bun:main", .@"bun:main" }, - .{ "bun:test", .@"bun:test" }, - .{ "bun:sqlite", .@"bun:sqlite" }, - .{ "bun:wrap", .@"bun:wrap" }, - .{ "bun:internal-for-testing", .@"bun:internal-for-testing" }, - // Node.js - .{ "node:assert", .@"node:assert" }, - .{ "node:assert/strict", .@"node:assert/strict" }, - .{ "node:async_hooks", .@"node:async_hooks" }, - .{ "node:buffer", .@"node:buffer" }, - .{ "node:child_process", .@"node:child_process" }, - .{ "node:cluster", .@"node:cluster" }, - .{ "node:console", .@"node:console" }, - .{ "node:constants", .@"node:constants" }, - .{ "node:crypto", .@"node:crypto" }, - .{ "node:dgram", .@"node:dgram" }, - .{ "node:diagnostics_channel", .@"node:diagnostics_channel" }, - .{ "node:dns", .@"node:dns" }, - .{ "node:dns/promises", .@"node:dns/promises" }, - .{ "node:domain", .@"node:domain" }, - .{ "node:events", .@"node:events" }, - .{ "node:fs", .@"node:fs" }, - .{ "node:fs/promises", .@"node:fs/promises" }, - .{ "node:http", .@"node:http" }, - .{ "node:http2", .@"node:http2" }, - .{ "node:https", .@"node:https" }, - .{ "node:inspector", .@"node:inspector" }, - .{ "node:module", .@"node:module" }, - .{ "node:net", .@"node:net" }, - .{ "node:readline", .@"node:readline" }, - .{ "node:test", .@"node:test" }, - .{ "node:os", .@"node:os" }, - .{ "node:path", .@"node:path" }, - .{ "node:path/posix", .@"node:path/posix" }, - .{ "node:path/win32", .@"node:path/win32" }, - .{ "node:perf_hooks", .@"node:perf_hooks" }, - .{ "node:process", .@"node:process" }, - .{ "node:punycode", .@"node:punycode" }, - .{ "node:querystring", .@"node:querystring" }, - .{ "node:readline", .@"node:readline" }, - .{ "node:readline/promises", .@"node:readline/promises" }, - .{ "node:repl", .@"node:repl" }, - .{ "node:stream", .@"node:stream" }, - .{ "node:stream/consumers", .@"node:stream/consumers" }, - .{ "node:stream/promises", .@"node:stream/promises" }, - .{ "node:stream/web", .@"node:stream/web" }, - .{ "node:string_decoder", .@"node:string_decoder" }, - .{ "node:timers", .@"node:timers" }, - .{ "node:timers/promises", .@"node:timers/promises" }, - .{ "node:tls", .@"node:tls" }, - .{ "node:trace_events", .@"node:trace_events" }, - .{ "node:tty", .@"node:tty" }, - .{ "node:url", .@"node:url" }, - .{ "node:util", .@"node:util" }, - .{ "node:util/types", .@"node:util/types" }, - .{ "node:v8", .@"node:v8" }, - .{ "node:vm", .@"node:vm" }, - .{ "node:wasi", .@"node:wasi" }, - .{ "node:worker_threads", .@"node:worker_threads" }, - .{ "node:zlib", .@"node:zlib" }, - .{ "node:_stream_duplex", .@"node:_stream_duplex" }, - .{ "node:_stream_passthrough", .@"node:_stream_passthrough" }, - .{ "node:_stream_readable", .@"node:_stream_readable" }, - .{ "node:_stream_transform", .@"node:_stream_transform" }, - .{ "node:_stream_wrap", .@"node:_stream_wrap" }, - .{ "node:_stream_writable", .@"node:_stream_writable" }, - .{ "node:_tls_common", .@"node:_tls_common" }, - .{ "node:_http_agent", .@"node:_http_agent" }, - .{ "node:_http_client", .@"node:_http_client" }, - .{ "node:_http_common", .@"node:_http_common" }, - .{ "node:_http_incoming", .@"node:_http_incoming" }, - .{ "node:_http_outgoing", .@"node:_http_outgoing" }, - .{ "node:_http_server", .@"node:_http_server" }, - - .{ "node-fetch", HardcodedModule.@"node-fetch" }, - .{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" }, - .{ "undici", HardcodedModule.undici }, - .{ "ws", HardcodedModule.ws }, - .{ "@vercel/fetch", HardcodedModule.vercel_fetch }, - .{ "utf-8-validate", HardcodedModule.@"utf-8-validate" }, - .{ "abort-controller", HardcodedModule.@"abort-controller" }, - }); - - /// Contains the list of built-in modules from the perspective of the module - /// loader. This logic is duplicated for `isBuiltinModule` and the like. - pub const Alias = struct { - path: [:0]const u8, - tag: ImportRecord.Tag = .builtin, - node_builtin: bool = false, - node_only_prefix: bool = false, - - fn nodeEntry(path: [:0]const u8) struct { string, Alias } { - return .{ - path, - .{ - .path = if (path.len > 5 and std.mem.eql(u8, path[0..5], "node:")) path else "node:" ++ path, - .node_builtin = true, - }, - }; - } - fn nodeEntryOnlyPrefix(path: [:0]const u8) struct { string, Alias } { - return .{ - path, - .{ - .path = if (path.len > 5 and std.mem.eql(u8, path[0..5], "node:")) path else "node:" ++ path, - .node_builtin = true, - .node_only_prefix = true, - }, - }; - } - fn entry(path: [:0]const u8) struct { string, Alias } { - return .{ path, .{ .path = path } }; - } - - // Applied to both --target=bun and --target=node - const common_alias_kvs = [_]struct { string, Alias }{ - nodeEntry("node:assert"), - nodeEntry("node:assert/strict"), - nodeEntry("node:async_hooks"), - nodeEntry("node:buffer"), - nodeEntry("node:child_process"), - nodeEntry("node:cluster"), - nodeEntry("node:console"), - nodeEntry("node:constants"), - nodeEntry("node:crypto"), - nodeEntry("node:dgram"), - nodeEntry("node:diagnostics_channel"), - nodeEntry("node:dns"), - nodeEntry("node:dns/promises"), - nodeEntry("node:domain"), - nodeEntry("node:events"), - nodeEntry("node:fs"), - nodeEntry("node:fs/promises"), - nodeEntry("node:http"), - nodeEntry("node:http2"), - nodeEntry("node:https"), - nodeEntry("node:inspector"), - nodeEntry("node:module"), - nodeEntry("node:net"), - nodeEntry("node:os"), - nodeEntry("node:path"), - nodeEntry("node:path/posix"), - nodeEntry("node:path/win32"), - nodeEntry("node:perf_hooks"), - nodeEntry("node:process"), - nodeEntry("node:punycode"), - nodeEntry("node:querystring"), - nodeEntry("node:readline"), - nodeEntry("node:readline/promises"), - nodeEntry("node:repl"), - nodeEntry("node:stream"), - nodeEntry("node:stream/consumers"), - nodeEntry("node:stream/promises"), - nodeEntry("node:stream/web"), - nodeEntry("node:string_decoder"), - nodeEntry("node:timers"), - nodeEntry("node:timers/promises"), - nodeEntry("node:tls"), - nodeEntry("node:trace_events"), - nodeEntry("node:tty"), - nodeEntry("node:url"), - nodeEntry("node:util"), - nodeEntry("node:util/types"), - nodeEntry("node:v8"), - nodeEntry("node:vm"), - nodeEntry("node:wasi"), - nodeEntry("node:worker_threads"), - nodeEntry("node:zlib"), - // New Node.js builtins only resolve from the prefixed one. - nodeEntryOnlyPrefix("node:test"), - - nodeEntry("assert"), - nodeEntry("assert/strict"), - nodeEntry("async_hooks"), - nodeEntry("buffer"), - nodeEntry("child_process"), - nodeEntry("cluster"), - nodeEntry("console"), - nodeEntry("constants"), - nodeEntry("crypto"), - nodeEntry("dgram"), - nodeEntry("diagnostics_channel"), - nodeEntry("dns"), - nodeEntry("dns/promises"), - nodeEntry("domain"), - nodeEntry("events"), - nodeEntry("fs"), - nodeEntry("fs/promises"), - nodeEntry("http"), - nodeEntry("http2"), - nodeEntry("https"), - nodeEntry("inspector"), - nodeEntry("module"), - nodeEntry("net"), - nodeEntry("os"), - nodeEntry("path"), - nodeEntry("path/posix"), - nodeEntry("path/win32"), - nodeEntry("perf_hooks"), - nodeEntry("process"), - nodeEntry("punycode"), - nodeEntry("querystring"), - nodeEntry("readline"), - nodeEntry("readline/promises"), - nodeEntry("repl"), - nodeEntry("stream"), - nodeEntry("stream/consumers"), - nodeEntry("stream/promises"), - nodeEntry("stream/web"), - nodeEntry("string_decoder"), - nodeEntry("timers"), - nodeEntry("timers/promises"), - nodeEntry("tls"), - nodeEntry("trace_events"), - nodeEntry("tty"), - nodeEntry("url"), - nodeEntry("util"), - nodeEntry("util/types"), - nodeEntry("v8"), - nodeEntry("vm"), - nodeEntry("wasi"), - nodeEntry("worker_threads"), - nodeEntry("zlib"), - - nodeEntry("node:_http_agent"), - nodeEntry("node:_http_client"), - nodeEntry("node:_http_common"), - nodeEntry("node:_http_incoming"), - nodeEntry("node:_http_outgoing"), - nodeEntry("node:_http_server"), - - nodeEntry("_http_agent"), - nodeEntry("_http_client"), - nodeEntry("_http_common"), - nodeEntry("_http_incoming"), - nodeEntry("_http_outgoing"), - nodeEntry("_http_server"), - - // sys is a deprecated alias for util - .{ "sys", .{ .path = "node:util", .node_builtin = true } }, - .{ "node:sys", .{ .path = "node:util", .node_builtin = true } }, - - // These are returned in builtinModules, but probably not many - // packages use them so we will just alias them. - .{ "node:_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } }, - .{ "node:_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } }, - .{ "node:_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } }, - .{ "node:_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } }, - .{ "node:_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } }, - .{ "node:_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } }, - .{ "node:_tls_wrap", .{ .path = "node:tls", .node_builtin = true } }, - .{ "node:_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } }, - .{ "_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } }, - .{ "_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } }, - .{ "_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } }, - .{ "_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } }, - .{ "_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } }, - .{ "_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } }, - .{ "_tls_wrap", .{ .path = "node:tls", .node_builtin = true } }, - .{ "_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } }, - }; - - const bun_extra_alias_kvs = [_]struct { string, Alias }{ - .{ "bun", .{ .path = "bun", .tag = .bun } }, - .{ "bun:test", .{ .path = "bun:test" } }, - .{ "bun:app", .{ .path = "bun:app" } }, - .{ "bun:ffi", .{ .path = "bun:ffi" } }, - .{ "bun:jsc", .{ .path = "bun:jsc" } }, - .{ "bun:sqlite", .{ .path = "bun:sqlite" } }, - .{ "bun:wrap", .{ .path = "bun:wrap" } }, - .{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } }, - .{ "ffi", .{ .path = "bun:ffi" } }, - - // inspector/promises is not implemented, it is an alias of inspector - .{ "node:inspector/promises", .{ .path = "node:inspector", .node_builtin = true } }, - .{ "inspector/promises", .{ .path = "node:inspector", .node_builtin = true } }, - - // Thirdparty packages we override - .{ "@vercel/fetch", .{ .path = "@vercel/fetch" } }, - .{ "isomorphic-fetch", .{ .path = "isomorphic-fetch" } }, - .{ "node-fetch", .{ .path = "node-fetch" } }, - .{ "undici", .{ .path = "undici" } }, - .{ "utf-8-validate", .{ .path = "utf-8-validate" } }, - .{ "ws", .{ .path = "ws" } }, - .{ "ws/lib/websocket", .{ .path = "ws" } }, - - // Polyfills we force to native - .{ "abort-controller", .{ .path = "abort-controller" } }, - .{ "abort-controller/polyfill", .{ .path = "abort-controller" } }, - - // To force Next.js to not use bundled dependencies. - .{ "next/dist/compiled/ws", .{ .path = "ws" } }, - .{ "next/dist/compiled/node-fetch", .{ .path = "node-fetch" } }, - .{ "next/dist/compiled/undici", .{ .path = "undici" } }, - }; - - const bun_test_extra_alias_kvs = [_]struct { string, Alias }{ - .{ "@jest/globals", .{ .path = "bun:test" } }, - .{ "vitest", .{ .path = "bun:test" } }, - }; - - const node_extra_alias_kvs = [_]struct { string, Alias }{ - nodeEntry("node:inspector/promises"), - nodeEntry("inspector/promises"), - }; - - const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_extra_alias_kvs); - const bun_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs); - const bun_test_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs ++ bun_test_extra_alias_kvs); - - const Cfg = struct { rewrite_jest_for_tests: bool = false }; - pub fn has(name: []const u8, target: options.Target, cfg: Cfg) bool { - return get(name, target, cfg) != null; - } - - pub fn get(name: []const u8, target: options.Target, cfg: Cfg) ?Alias { - if (target.isBun()) { - if (cfg.rewrite_jest_for_tests) { - return bun_test_aliases.get(name); - } else { - return bun_aliases.get(name); - } - } else if (target.isNode()) { - return node_aliases.get(name); - } - return null; - } - }; -}; - /// Support embedded .node files export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.String) bool { if (vm.standalone_module_graph == null) return false; @@ -3059,27 +1319,24 @@ const debug = Output.scoped(.ModuleLoader, .hidden); const string = []const u8; -const Dependency = @import("../install/dependency.zig"); const Fs = @import("../fs.zig"); const Runtime = @import("../runtime.zig"); +const ast = @import("../import_record.zig"); const node_module_module = @import("./bindings/NodeModuleModule.zig"); const std = @import("std"); const panic = std.debug.panic; -const ast = @import("../import_record.zig"); -const ImportRecord = ast.ImportRecord; - -const Install = @import("../install/install.zig"); -const PackageManager = @import("../install/install.zig").PackageManager; - const options = @import("../options.zig"); const ModuleType = options.ModuleType; const MacroRemap = @import("../resolver/package_json.zig").MacroMap; const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; +const dumpSource = @import("./RuntimeTranspilerStore.zig").dumpSource; +const dumpSourceString = @import("./RuntimeTranspilerStore.zig").dumpSourceString; +const setBreakPointOnFirstLine = @import("./RuntimeTranspilerStore.zig").setBreakPointOnFirstLine; + const bun = @import("bun"); -const Async = bun.Async; const Environment = bun.Environment; const MutableString = bun.MutableString; const Output = bun.Output; diff --git a/src/bun.js/RuntimeTranspilerStore.zig b/src/bun.js/RuntimeTranspilerStore.zig new file mode 100644 index 0000000000..695a3f10a1 --- /dev/null +++ b/src/bun.js/RuntimeTranspilerStore.zig @@ -0,0 +1,626 @@ +const debug = Output.scoped(.RuntimeTranspilerStore, .hidden); + +const string = []const u8; + +pub fn dumpSource(vm: *VirtualMachine, specifier: string, printer: anytype) void { + dumpSourceString(vm, specifier, printer.ctx.getWritten()); +} + +pub fn dumpSourceString(vm: *VirtualMachine, specifier: string, written: []const u8) void { + dumpSourceStringFailiable(vm, specifier, written) catch |e| { + Output.debugWarn("Failed to dump source string: {}", .{e}); + }; +} + +pub fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written: []const u8) !void { + if (!Environment.isDebug) return; + if (bun.feature_flag.BUN_DEBUG_NO_DUMP.get()) return; + + const BunDebugHolder = struct { + pub var dir: ?std.fs.Dir = null; + pub var lock: bun.Mutex = .{}; + }; + + BunDebugHolder.lock.lock(); + defer BunDebugHolder.lock.unlock(); + + const dir = BunDebugHolder.dir orelse dir: { + const base_name = switch (Environment.os) { + else => "/tmp/bun-debug-src/", + .windows => brk: { + const temp = bun.fs.FileSystem.RealFS.platformTempDir(); + var win_temp_buffer: bun.PathBuffer = undefined; + @memcpy(win_temp_buffer[0..temp.len], temp); + const suffix = "\\bun-debug-src"; + @memcpy(win_temp_buffer[temp.len .. temp.len + suffix.len], suffix); + win_temp_buffer[temp.len + suffix.len] = 0; + break :brk win_temp_buffer[0 .. temp.len + suffix.len :0]; + }, + }; + const dir = try std.fs.cwd().makeOpenPath(base_name, .{}); + BunDebugHolder.dir = dir; + break :dir dir; + }; + + if (std.fs.path.dirname(specifier)) |dir_path| { + const root_len = switch (Environment.os) { + else => "/".len, + .windows => bun.path.windowsFilesystemRoot(dir_path).len, + }; + var parent = try dir.makeOpenPath(dir_path[root_len..], .{}); + defer parent.close(); + parent.writeFile(.{ + .sub_path = std.fs.path.basename(specifier), + .data = written, + }) catch |e| { + Output.debugWarn("Failed to dump source string: writeFile {}", .{e}); + return; + }; + if (vm.source_mappings.get(specifier)) |mappings| { + defer mappings.deref(); + const map_path = bun.handleOom(std.mem.concat(bun.default_allocator, u8, &.{ std.fs.path.basename(specifier), ".map" })); + defer bun.default_allocator.free(map_path); + const file = try parent.createFile(map_path, .{}); + defer file.close(); + + const source_file = parent.readFileAlloc( + bun.default_allocator, + specifier, + std.math.maxInt(u64), + ) catch ""; + defer bun.default_allocator.free(source_file); + + var bufw = std.io.bufferedWriter(file.writer()); + const w = bufw.writer(); + try w.print( + \\{{ + \\ "version": 3, + \\ "file": {}, + \\ "sourceRoot": "", + \\ "sources": [{}], + \\ "sourcesContent": [{}], + \\ "names": [], + \\ "mappings": "{}" + \\}} + , .{ + bun.fmt.formatJSONStringUTF8(std.fs.path.basename(specifier), .{}), + bun.fmt.formatJSONStringUTF8(specifier, .{}), + bun.fmt.formatJSONStringUTF8(source_file, .{}), + mappings.formatVLQs(), + }); + try bufw.flush(); + } + } else { + dir.writeFile(.{ + .sub_path = std.fs.path.basename(specifier), + .data = written, + }) catch return; + } +} + +pub fn setBreakPointOnFirstLine() bool { + const s = struct { + var set_break_point: std.atomic.Value(bool) = std.atomic.Value(bool).init(true); + }; + return s.set_break_point.swap(false, .seq_cst); +} + +pub const RuntimeTranspilerStore = struct { + generation_number: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + store: TranspilerJob.Store, + enabled: bool = true, + queue: Queue = Queue{}, + + pub const Queue = bun.UnboundedQueue(TranspilerJob, .next); + + pub fn init() RuntimeTranspilerStore { + return RuntimeTranspilerStore{ + .store = TranspilerJob.Store.init(bun.typedAllocator(TranspilerJob)), + }; + } + + pub fn runFromJSThread(this: *RuntimeTranspilerStore, event_loop: *jsc.EventLoop, global: *jsc.JSGlobalObject, vm: *jsc.VirtualMachine) void { + var batch = this.queue.popBatch(); + const jsc_vm = vm.jsc_vm; + var iter = batch.iterator(); + if (iter.next()) |job| { + // we run just one job first to see if there are more + job.runFromJSThread() catch |err| global.reportUncaughtExceptionFromError(err); + } else { + return; + } + while (iter.next()) |job| { + // if there are more, we need to drain the microtasks from the previous run + event_loop.drainMicrotasksWithGlobal(global, jsc_vm) catch return; + job.runFromJSThread() catch |err| global.reportUncaughtExceptionFromError(err); + } + + // immediately after this is called, the microtasks will be drained again. + } + + pub fn transpile( + this: *RuntimeTranspilerStore, + vm: *VirtualMachine, + globalObject: *JSGlobalObject, + input_specifier: bun.String, + path: Fs.Path, + referrer: bun.String, + loader: bun.options.Loader, + package_json: ?*const PackageJSON, + ) *anyopaque { + var job: *TranspilerJob = this.store.get(); + const owned_path = Fs.Path.init(bun.default_allocator.dupe(u8, path.text) catch unreachable); + const promise = jsc.JSInternalPromise.create(globalObject); + + // NOTE: DirInfo should already be cached since module loading happens + // after module resolution, so this should be cheap + var resolved_source = ResolvedSource{}; + if (package_json) |pkg| { + switch (pkg.module_type) { + .cjs => { + resolved_source.tag = .package_json_type_commonjs; + resolved_source.is_commonjs_module = true; + }, + .esm => resolved_source.tag = .package_json_type_module, + .unknown => {}, + } + } + + job.* = TranspilerJob{ + .non_threadsafe_input_specifier = input_specifier, + .path = owned_path, + .globalThis = globalObject, + .non_threadsafe_referrer = referrer, + .vm = vm, + .log = logger.Log.init(bun.default_allocator), + .loader = loader, + .promise = .create(JSValue.fromCell(promise), globalObject), + .poll_ref = .{}, + .fetcher = TranspilerJob.Fetcher{ + .file = {}, + }, + .resolved_source = resolved_source, + .generation_number = this.generation_number.load(.seq_cst), + }; + if (comptime Environment.allow_assert) + debug("transpile({s}, {s}, async)", .{ path.text, @tagName(job.loader) }); + job.schedule(); + return promise; + } + + pub const TranspilerJob = struct { + path: Fs.Path, + non_threadsafe_input_specifier: String, + non_threadsafe_referrer: String, + loader: options.Loader, + promise: jsc.Strong.Optional = .empty, + vm: *VirtualMachine, + globalThis: *JSGlobalObject, + fetcher: Fetcher, + poll_ref: Async.KeepAlive = .{}, + generation_number: u32 = 0, + log: logger.Log, + parse_error: ?anyerror = null, + resolved_source: ResolvedSource = ResolvedSource{}, + work_task: jsc.WorkPoolTask = .{ .callback = runFromWorkerThread }, + next: ?*TranspilerJob = null, + + pub const Store = bun.HiveArray(TranspilerJob, if (bun.heap_breakdown.enabled) 0 else 64).Fallback; + + pub const Fetcher = union(enum) { + virtual_module: bun.String, + file: void, + + pub fn deinit(this: *@This()) void { + if (this.* == .virtual_module) { + this.virtual_module.deref(); + } + } + }; + + pub fn deinit(this: *TranspilerJob) void { + bun.default_allocator.free(this.path.text); + + this.poll_ref.disable(); + this.fetcher.deinit(); + this.loader = options.Loader.file; + this.non_threadsafe_input_specifier.deref(); + this.non_threadsafe_referrer.deref(); + this.path = Fs.Path.empty; + this.log.deinit(); + this.promise.deinit(); + this.globalThis = undefined; + } + + threadlocal var ast_memory_store: ?*js_ast.ASTMemoryAllocator = null; + threadlocal var source_code_printer: ?*js_printer.BufferPrinter = null; + + pub fn dispatchToMainThread(this: *TranspilerJob) void { + this.vm.transpiler_store.queue.push(this); + this.vm.eventLoop().enqueueTaskConcurrent(jsc.ConcurrentTask.createFrom(&this.vm.transpiler_store)); + } + + pub fn runFromJSThread(this: *TranspilerJob) bun.JSError!void { + var vm = this.vm; + const promise = this.promise.swap(); + const globalThis = this.globalThis; + this.poll_ref.unref(vm); + + const referrer = this.non_threadsafe_referrer; + this.non_threadsafe_referrer = String.empty; + var log = this.log; + this.log = logger.Log.init(bun.default_allocator); + var resolved_source = this.resolved_source; + const specifier = brk: { + if (this.parse_error != null) { + break :brk bun.String.cloneUTF8(this.path.text); + } + + const out = this.non_threadsafe_input_specifier; + this.non_threadsafe_input_specifier = String.empty; + + bun.debugAssert(resolved_source.source_url.isEmpty()); + bun.debugAssert(resolved_source.specifier.isEmpty()); + resolved_source.source_url = out.createIfDifferent(this.path.text); + resolved_source.specifier = out.dupeRef(); + break :brk out; + }; + + const parse_error = this.parse_error; + + this.promise.deinit(); + this.deinit(); + + _ = vm.transpiler_store.store.put(this); + + try AsyncModule.fulfill(globalThis, promise, &resolved_source, parse_error, specifier, referrer, &log); + } + + pub fn schedule(this: *TranspilerJob) void { + this.poll_ref.ref(this.vm); + jsc.WorkPool.schedule(&this.work_task); + } + + pub fn runFromWorkerThread(work_task: *jsc.WorkPoolTask) void { + @as(*TranspilerJob, @fieldParentPtr("work_task", work_task)).run(); + } + + pub fn run(this: *TranspilerJob) void { + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + defer this.dispatchToMainThread(); + if (this.generation_number != this.vm.transpiler_store.generation_number.load(.monotonic)) { + this.parse_error = error.TranspilerJobGenerationMismatch; + return; + } + + if (ast_memory_store == null) { + ast_memory_store = bun.handleOom(bun.default_allocator.create(js_ast.ASTMemoryAllocator)); + ast_memory_store.?.* = js_ast.ASTMemoryAllocator{ + .allocator = allocator, + .previous = null, + }; + } + + var ast_scope = ast_memory_store.?.enter(allocator); + defer ast_scope.exit(); + + const path = this.path; + const specifier = this.path.text; + const loader = this.loader; + + var cache = jsc.RuntimeTranspilerCache{ + .output_code_allocator = allocator, + .sourcemap_allocator = bun.default_allocator, + }; + var log = logger.Log.init(allocator); + defer { + this.log = logger.Log.init(bun.default_allocator); + bun.handleOom(log.cloneToWithRecycled(&this.log, true)); + } + var vm = this.vm; + var transpiler: bun.Transpiler = undefined; + transpiler = vm.transpiler; + transpiler.setAllocator(allocator); + transpiler.setLog(&log); + transpiler.resolver.opts = transpiler.options; + transpiler.macro_context = null; + transpiler.linker.resolver = &transpiler.resolver; + + var fd: ?StoredFileDescriptorType = null; + var package_json: ?*PackageJSON = null; + const hash = bun.Watcher.getHash(path.text); + + switch (vm.bun_watcher) { + .hot, .watch => { + if (vm.bun_watcher.indexOf(hash)) |index| { + const watcher_fd = vm.bun_watcher.watchlist().items(.fd)[index]; + fd = if (watcher_fd.stdioTag() == null) watcher_fd else null; + package_json = vm.bun_watcher.watchlist().items(.package_json)[index]; + } + }, + else => {}, + } + + // this should be a cheap lookup because 24 bytes == 8 * 3 so it's read 3 machine words + const is_node_override = strings.hasPrefixComptime(specifier, node_fallbacks.import_path); + + const macro_remappings = if (vm.macro_mode or !vm.has_any_macro_remappings or is_node_override) + MacroRemap{} + else + transpiler.options.macro_remap; + + var fallback_source: logger.Source = undefined; + + // Usually, we want to close the input file automatically. + // + // If we're re-using the file descriptor from the fs watcher + // Do not close it because that will break the kqueue-based watcher + // + var should_close_input_file_fd = fd == null; + + var input_file_fd: StoredFileDescriptorType = .invalid; + + const is_main = vm.main.len == path.text.len and + vm.main_hash == hash and + strings.eqlLong(vm.main, path.text, false); + + const module_type: ModuleType = switch (this.resolved_source.tag) { + .package_json_type_commonjs => .cjs, + .package_json_type_module => .esm, + else => .unknown, + }; + + var parse_options = Transpiler.ParseOptions{ + .allocator = allocator, + .path = path, + .loader = loader, + .dirname_fd = .invalid, + .file_descriptor = fd, + .file_fd_ptr = &input_file_fd, + .file_hash = hash, + .macro_remappings = macro_remappings, + .jsx = transpiler.options.jsx, + .emit_decorator_metadata = transpiler.options.emit_decorator_metadata, + .virtual_source = null, + .dont_bundle_twice = true, + .allow_commonjs = true, + .inject_jest_globals = transpiler.options.rewrite_jest_for_tests, + .set_breakpoint_on_first_line = vm.debugger != null and + vm.debugger.?.set_breakpoint_on_first_line and + is_main and + setBreakPointOnFirstLine(), + .runtime_transpiler_cache = if (!jsc.RuntimeTranspilerCache.is_disabled) &cache else null, + .remove_cjs_module_wrapper = is_main and vm.module_loader.eval_source != null, + .module_type = module_type, + .allow_bytecode_cache = true, + }; + + defer { + if (should_close_input_file_fd and input_file_fd.isValid()) { + input_file_fd.close(); + input_file_fd = .invalid; + } + } + + if (is_node_override) { + if (node_fallbacks.contentsFromPath(specifier)) |code| { + const fallback_path = Fs.Path.initWithNamespace(specifier, "node"); + fallback_source = logger.Source{ .path = fallback_path, .contents = code }; + parse_options.virtual_source = &fallback_source; + } + } + + var parse_result: bun.transpiler.ParseResult = transpiler.parseMaybeReturnFileOnlyAllowSharedBuffer( + parse_options, + null, + false, + false, + ) orelse { + if (vm.isWatcherEnabled()) { + if (input_file_fd.isValid()) { + if (!is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + should_close_input_file_fd = false; + _ = vm.bun_watcher.addFile( + input_file_fd, + path.text, + hash, + loader, + .invalid, + package_json, + true, + ); + } + } + } + + this.parse_error = error.ParseError; + + return; + }; + + if (vm.isWatcherEnabled()) { + if (input_file_fd.isValid()) { + if (!is_node_override and + std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) + { + should_close_input_file_fd = false; + _ = vm.bun_watcher.addFile( + input_file_fd, + path.text, + hash, + loader, + .invalid, + package_json, + true, + ); + } + } + } + + if (cache.entry) |*entry| { + vm.source_mappings.putMappings(&parse_result.source, .{ + .list = .{ .items = @constCast(entry.sourcemap), .capacity = entry.sourcemap.len }, + .allocator = bun.default_allocator, + }) catch {}; + + if (comptime Environment.dump_source) { + dumpSourceString(vm, specifier, entry.output_code.byteSlice()); + } + + this.resolved_source = ResolvedSource{ + .allocator = null, + .source_code = switch (entry.output_code) { + .string => entry.output_code.string, + .utf8 => brk: { + const result = bun.String.cloneUTF8(entry.output_code.utf8); + cache.output_code_allocator.free(entry.output_code.utf8); + entry.output_code.utf8 = ""; + break :brk result; + }, + }, + .is_commonjs_module = entry.metadata.module_type == .cjs, + .tag = this.resolved_source.tag, + }; + + return; + } + + if (parse_result.already_bundled != .none) { + const bytecode_slice = parse_result.already_bundled.bytecodeSlice(); + this.resolved_source = ResolvedSource{ + .allocator = null, + .source_code = bun.String.cloneLatin1(parse_result.source.contents), + .already_bundled = true, + .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, + .bytecode_cache_size = bytecode_slice.len, + .is_commonjs_module = parse_result.already_bundled.isCommonJS(), + .tag = this.resolved_source.tag, + }; + this.resolved_source.source_code.ensureHash(); + return; + } + + for (parse_result.ast.import_records.slice()) |*import_record_| { + var import_record: *bun.ImportRecord = import_record_; + + if (HardcodedModule.Alias.get(import_record.path.text, transpiler.options.target, .{ .rewrite_jest_for_tests = transpiler.options.rewrite_jest_for_tests })) |replacement| { + import_record.path.text = replacement.path; + import_record.tag = replacement.tag; + import_record.is_external_without_side_effects = true; + continue; + } + + if (strings.hasPrefixComptime(import_record.path.text, "bun:")) { + import_record.path = Fs.Path.init(import_record.path.text["bun:".len..]); + import_record.path.namespace = "bun"; + import_record.is_external_without_side_effects = true; + } + } + + if (source_code_printer == null) { + const writer = js_printer.BufferWriter.init(bun.default_allocator); + source_code_printer = bun.default_allocator.create(js_printer.BufferPrinter) catch unreachable; + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + } + + var printer = source_code_printer.?.*; + printer.ctx.reset(); + + // Cap buffer size to prevent unbounded growth + const max_buffer_cap = 512 * 1024; + if (printer.ctx.buffer.list.capacity > max_buffer_cap) { + printer.ctx.buffer.deinit(); + const writer = js_printer.BufferWriter.init(bun.default_allocator); + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + printer = source_code_printer.?.*; + } + + { + var mapper = vm.sourceMapHandler(&printer); + defer source_code_printer.?.* = printer; + _ = transpiler.printWithSourceMap( + parse_result, + @TypeOf(&printer), + &printer, + .esm_ascii, + mapper.get(), + ) catch |err| { + this.parse_error = err; + return; + }; + } + + if (comptime Environment.dump_source) { + dumpSource(this.vm, specifier, &printer); + } + + const source_code = brk: { + const written = printer.ctx.getWritten(); + + const result = cache.output_code orelse bun.String.cloneLatin1(written); + + if (written.len > 1024 * 1024 * 2 or vm.smol) { + printer.ctx.buffer.deinit(); + const writer = js_printer.BufferWriter.init(bun.default_allocator); + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + } else { + source_code_printer.?.* = printer; + } + + // In a benchmarking loading @babel/standalone 100 times: + // + // After ensureHash: + // 354.00 ms 4.2% 354.00 ms WTF::StringImpl::hashSlowCase() const + // + // Before ensureHash: + // 506.00 ms 6.1% 506.00 ms WTF::StringImpl::hashSlowCase() const + // + result.ensureHash(); + + break :brk result; + }; + this.resolved_source = ResolvedSource{ + .allocator = null, + .source_code = source_code, + .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + .tag = this.resolved_source.tag, + }; + } + }; +}; + +const Fs = @import("../fs.zig"); +const node_fallbacks = @import("../node_fallbacks.zig"); +const std = @import("std"); +const AsyncModule = @import("./AsyncModule.zig").AsyncModule; +const HardcodedModule = @import("./HardcodedModule.zig").HardcodedModule; + +const options = @import("../options.zig"); +const ModuleType = options.ModuleType; + +const MacroRemap = @import("../resolver/package_json.zig").MacroMap; +const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; + +const bun = @import("bun"); +const Async = bun.Async; +const Environment = bun.Environment; +const Output = bun.Output; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const String = bun.String; +const Transpiler = bun.Transpiler; +const js_ast = bun.ast; +const js_printer = bun.js_printer; +const logger = bun.logger; +const strings = bun.strings; + +const jsc = bun.jsc; +const JSGlobalObject = bun.jsc.JSGlobalObject; +const JSValue = bun.jsc.JSValue; +const ResolvedSource = bun.jsc.ResolvedSource; +const VirtualMachine = bun.jsc.VirtualMachine; From f58a0662367dcbc400d6aa2b56f82d326dbb550a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Oct 2025 21:34:24 -0700 Subject: [PATCH 248/391] Update CLAUDE.md --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5fa59d403c..986bff8ae9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,8 @@ test("my feature", async () => { - Use `normalizeBunSnapshot` to normalize snapshot output of the test. - NEVER write tests that check for no "panic" or "uncaught exception" or similar in the test output. That is NOT a valid test. - Use `tempDir` from `"harness"` to create a temporary directory. **Do not** use `tmpdirSync` or `fs.mkdtempSync` to create temporary directories. -- When spawning processes, tests should assert the output BEFORE asserting the exit code. This gives you a more useful error message on test failure. +- When spawning processes, tests should expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0). This gives you a more useful error message on test failure. +- **CRITICAL**: Do not write flaky tests. Do not use `setTimeout` in tests. Instead, `await` the condition to be met. You are not testing the TIME PASSING, you are testing the CONDITION. - **CRITICAL**: Verify your test fails with `USE_SYSTEM_BUN=1 bun test ` and passes with `bun bd test `. Your test is NOT VALID if it passes with `USE_SYSTEM_BUN=1`. ## Code Architecture From 4c00d8f0168b3e60350059251709768e41ced52c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:03:34 -0700 Subject: [PATCH 249/391] deps: update elysia to 1.4.13 (#24085) ## What does this PR do? Updates elysia to version 1.4.13 Compare: https://github.com/elysiajs/elysia/compare/1.4.12...1.4.13 Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-vendor.yml) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- test/vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/vendor.json b/test/vendor.json index 05ca430f3a..06a7d08a12 100644 --- a/test/vendor.json +++ b/test/vendor.json @@ -2,6 +2,6 @@ { "package": "elysia", "repository": "https://github.com/elysiajs/elysia", - "tag": "1.4.12" + "tag": "1.4.13" } ] From a75cef50798950a3801678761960226d1a7046db Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 26 Oct 2025 01:28:27 -0700 Subject: [PATCH 250/391] Add comprehensive documentation for JSRef (#24095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds detailed documentation explaining JSRef's intended usage - Includes a complete example showing common patterns - Explains the three states (weak, strong, finalized) - Provides guidelines on when to use strong vs weak references - References real examples from the codebase (ServerWebSocket, UDPSocket, MySQLConnection, ValkeyClient) ## Motivation JSRef is a critical type for managing JavaScript object references from native code, but it lacked comprehensive documentation explaining its usage patterns and lifecycle management. This makes it clearer how to properly use JSRef to: - Safely maintain references to JS objects from native code - Control whether references prevent garbage collection - Manage the upgrade/downgrade pattern based on object activity ## Test plan Documentation-only change, no functional changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/bindings/JSRef.zig | 91 +++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/src/bun.js/bindings/JSRef.zig b/src/bun.js/bindings/JSRef.zig index 08928aa73e..a90e0087a7 100644 --- a/src/bun.js/bindings/JSRef.zig +++ b/src/bun.js/bindings/JSRef.zig @@ -1,7 +1,92 @@ -/// Holds a reference to a JSValue. +/// Holds a reference to a JSValue with lifecycle management. +/// +/// JSRef is used to safely maintain a reference to a JavaScript object from native code, +/// with explicit control over whether the reference keeps the object alive during garbage collection. +/// +/// # Common Usage Pattern +/// +/// JSRef is typically used in native objects that need to maintain a reference to their +/// corresponding JavaScript wrapper object. The reference can be upgraded to "strong" when +/// the native object has pending work or active connections, and downgraded to "weak" when idle: +/// +/// ```zig +/// const MyNativeObject = struct { +/// this_value: jsc.JSRef = .empty(), +/// connection: SomeConnection, +/// +/// pub fn init(globalObject: *jsc.JSGlobalObject) *MyNativeObject { +/// const this = MyNativeObject.new(.{}); +/// const this_value = this.toJS(globalObject); +/// // Start with strong ref - object has pending work (initialization) +/// this.this_value = .initStrong(this_value, globalObject); +/// return this; +/// } +/// +/// fn updateReferenceType(this: *MyNativeObject) void { +/// if (this.connection.isActive()) { +/// // Keep object alive while connection is active +/// if (this.this_value.isNotEmpty() and this.this_value == .weak) { +/// this.this_value.upgrade(globalObject); +/// } +/// } else { +/// // Allow GC when connection is idle +/// if (this.this_value.isNotEmpty() and this.this_value == .strong) { +/// this.this_value.downgrade(); +/// } +/// } +/// } +/// +/// pub fn onMessage(this: *MyNativeObject) void { +/// // Safely retrieve the JSValue if still alive +/// const this_value = this.this_value.tryGet() orelse return; +/// // Use this_value... +/// } +/// +/// pub fn finalize(this: *MyNativeObject) void { +/// // Called when JS object is being garbage collected +/// this.this_value.finalize(); +/// this.cleanup(); +/// } +/// }; +/// ``` +/// +/// # States +/// +/// - **weak**: Holds a JSValue directly. Does NOT prevent garbage collection. +/// The JSValue may become invalid if the object is collected. +/// Use `tryGet()` to safely check if the value is still alive. +/// +/// - **strong**: Holds a Strong reference that prevents garbage collection. +/// The JavaScript object will stay alive as long as this reference exists. +/// Must call `deinit()` or `finalize()` to release. +/// +/// - **finalized**: The reference has been finalized (object was GC'd or explicitly cleaned up). +/// Indicates the JSValue is no longer valid. `tryGet()` returns null. +/// +/// # Key Methods +/// +/// - `initWeak()` / `initStrong()`: Create a new JSRef in weak or strong mode +/// - `tryGet()`: Safely retrieve the JSValue if still alive (returns null if finalized or empty) +/// - `upgrade()`: Convert weak → strong to prevent GC +/// - `downgrade()`: Convert strong → weak to allow GC (keeps the JSValue if still alive) +/// - `finalize()`: Mark as finalized and release resources (typically called from GC finalizer) +/// - `deinit()`: Release resources without marking as finalized +/// +/// # When to Use Strong vs Weak +/// +/// Use **strong** references when: +/// - The native object has active operations (network connections, pending requests, timers) +/// - You need to guarantee the JS object stays alive +/// - You'll call methods on the JS object from callbacks +/// +/// Use **weak** references when: +/// - The native object is idle with no pending work +/// - The JS object should be GC-able if no other references exist +/// - You want to allow natural garbage collection +/// +/// Common pattern: Start strong, downgrade to weak when idle, upgrade to strong when active. +/// See ServerWebSocket, UDPSocket, MySQLConnection, and ValkeyClient for examples. /// -/// This reference can be either weak (a JSValue) or may be strong, in which -/// case it prevents the garbage collector from collecting the value. pub const JSRef = union(enum) { weak: jsc.JSValue, strong: jsc.Strong.Optional, From b7ae21d0bcf27f4a56e4d52b185a1a1eb0923651 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 26 Oct 2025 14:29:27 -0700 Subject: [PATCH 251/391] Mark flaky test as TODO --- test/js/web/fetch/fetch.stream.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index bdbd646b65..c52a39252e 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -28,7 +28,9 @@ const empty = Buffer.alloc(0); describe.concurrent("fetch() with streaming", () => { [-1, 0, 20, 50, 100].forEach(timeout => { - it(`should be able to fail properly when reading from readable stream with timeout ${timeout}`, async () => { + // This test is flaky. + // Sometimes, we don't throw if signal.abort(). We need to fix that. + it.todo(`should be able to fail properly when reading from readable stream with timeout ${timeout}`, async () => { using server = Bun.serve({ port: 0, async fetch(req) { From b280e8d326c09277c4d000cee713d523cf2c1983 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 27 Oct 2025 02:37:05 -0700 Subject: [PATCH 252/391] Enable more sanitizers in CI (#24117) ### What does this PR do? We were only enabling UBSAN in debug builds. This was probably a mistake. ### How did you verify your code works? --- cmake/targets/BuildBun.cmake | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 113c61fbff..b5adbc4d43 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -944,7 +944,7 @@ if(NOT WIN32) if (NOT ABI STREQUAL "musl") target_compile_options(${bun} PUBLIC -fsanitize=null - -fsanitize-recover=all + -fno-sanitize-recover=all -fsanitize=bounds -fsanitize=return -fsanitize=nullability-arg @@ -999,6 +999,20 @@ if(NOT WIN32) ) if(ENABLE_ASAN) + target_compile_options(${bun} PUBLIC + -fsanitize=null + -fno-sanitize-recover=all + -fsanitize=bounds + -fsanitize=return + -fsanitize=nullability-arg + -fsanitize=nullability-assign + -fsanitize=nullability-return + -fsanitize=returns-nonnull-attribute + -fsanitize=unreachable + ) + target_link_libraries(${bun} PRIVATE + -fsanitize=null + ) target_compile_options(${bun} PUBLIC -fsanitize=address) target_link_libraries(${bun} PUBLIC -fsanitize=address) endif() From 1e849b905a5d8261ebcaa226a40feb0d8ca8f817 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 27 Oct 2025 11:26:09 -0800 Subject: [PATCH 253/391] zig: bun.sourcemap -> bun.SourceMap (#23477) --- src/StandaloneModuleGraph.zig | 2 +- src/bake/DevServer.zig | 2 +- src/bake/DevServer/IncrementalGraph.zig | 6 ++--- src/bake/DevServer/PackedMap.zig | 2 +- src/bake/DevServer/SourceMapStore.zig | 2 +- src/bun.js/SavedSourceMap.zig | 6 ++--- src/bun.js/VirtualMachine.zig | 2 +- .../bindings/generated_classes_list.zig | 2 +- src/bun.js/virtual_machine_exports.zig | 6 ++--- src/bun.zig | 4 ++-- src/bundler/Chunk.zig | 24 +++++++++---------- src/bundler/LinkerContext.zig | 20 ++++++++-------- src/bundler/LinkerGraph.zig | 2 +- src/bundler/bundle_v2.zig | 16 ++++++------- src/bundler/linker_context/computeChunks.zig | 12 +++++----- .../linker_context/postProcessCSSChunk.zig | 2 +- .../linker_context/postProcessJSChunk.zig | 2 +- .../linker_context/writeOutputFilesToDisk.zig | 1 - src/cli/test_command.zig | 20 ++++++++-------- src/js_printer.zig | 2 +- src/sourcemap/CodeCoverage.zig | 2 +- src/sourcemap/JSSourceMap.zig | 10 ++++---- 22 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 49c659e0dd..97cdca6bd0 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -1551,7 +1551,7 @@ const w = std.os.windows; const bun = @import("bun"); const Environment = bun.Environment; const Output = bun.Output; -const SourceMap = bun.sourcemap; +const SourceMap = bun.SourceMap; const StringPointer = bun.StringPointer; const Syscall = bun.sys; const macho = bun.macho; diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index e85e874890..73756e46ef 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -4664,7 +4664,7 @@ fn extractPathnameFromUrl(url: []const u8) []const u8 { const bun = @import("bun"); const Environment = bun.Environment; const Output = bun.Output; -const SourceMap = bun.sourcemap; +const SourceMap = bun.SourceMap; const Watcher = bun.Watcher; const assert = bun.assert; const bake = bun.bake; diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index 837cdf2925..6cb775c573 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -2034,6 +2034,9 @@ const DynamicBitSetUnmanaged = bun.bit_set.DynamicBitSetUnmanaged; const Log = bun.logger.Log; const useAllFields = bun.meta.useAllFields; +const SourceMap = bun.SourceMap; +const VLQ = SourceMap.VLQ; + const DevServer = bake.DevServer; const ChunkKind = DevServer.ChunkKind; const DevAllocator = DevServer.DevAllocator; @@ -2059,9 +2062,6 @@ const Chunk = bun.bundle_v2.Chunk; const Owned = bun.ptr.Owned; const Shared = bun.ptr.Shared; -const SourceMap = bun.sourcemap; -const VLQ = SourceMap.VLQ; - const std = @import("std"); const ArrayListUnmanaged = std.ArrayListUnmanaged; const AutoArrayHashMapUnmanaged = std.AutoArrayHashMapUnmanaged; diff --git a/src/bake/DevServer/PackedMap.zig b/src/bake/DevServer/PackedMap.zig index 1fc9f75105..0821237651 100644 --- a/src/bake/DevServer/PackedMap.zig +++ b/src/bake/DevServer/PackedMap.zig @@ -114,7 +114,7 @@ pub const Shared = union(enum) { const bun = @import("bun"); const Environment = bun.Environment; -const SourceMap = bun.sourcemap; +const SourceMap = bun.SourceMap; const assert = bun.assert; const assert_eql = bun.assert_eql; const Chunk = bun.bundle_v2.Chunk; diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index a2de1f35bc..cd2a470130 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -544,7 +544,7 @@ pub fn getParsedSourceMap(store: *Self, script_id: Key, arena: Allocator, gpa: A const bun = @import("bun"); const Environment = bun.Environment; const Output = bun.Output; -const SourceMap = bun.sourcemap; +const SourceMap = bun.SourceMap; const StringJoiner = bun.StringJoiner; const assert = bun.assert; const bake = bun.bake; diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index 7ad60b459e..64567ac553 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -384,8 +384,8 @@ const Output = bun.Output; const js_printer = bun.js_printer; const logger = bun.logger; -const SourceMap = bun.sourcemap; -const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; -const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; +const SourceMap = bun.SourceMap; +const BakeSourceProvider = bun.SourceMap.BakeSourceProvider; +const DevServerSourceProvider = bun.SourceMap.DevServerSourceProvider; const ParsedSourceMap = SourceMap.ParsedSourceMap; const SourceProviderMap = SourceMap.SourceProviderMap; diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 61f3fa5ae6..b5fe09398e 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -3711,7 +3711,7 @@ const Global = bun.Global; const MutableString = bun.MutableString; const Ordinal = bun.Ordinal; const Output = bun.Output; -const SourceMap = bun.sourcemap; +const SourceMap = bun.SourceMap; const String = bun.String; const Transpiler = bun.Transpiler; const Watcher = bun.Watcher; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 41705dbd11..f5e3655bc6 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -88,7 +88,7 @@ pub const Classes = struct { pub const RedisClient = api.Valkey; pub const BlockList = api.BlockList; pub const NativeZstd = api.NativeZstd; - pub const SourceMap = bun.sourcemap.JSSourceMap; + pub const SourceMap = bun.SourceMap.JSSourceMap; }; const bun = @import("bun"); diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 63606ad9ed..c7f01734bc 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -224,10 +224,10 @@ const std = @import("std"); const bun = @import("bun"); const PluginRunner = bun.transpiler.PluginRunner; +const BakeSourceProvider = bun.SourceMap.BakeSourceProvider; +const DevServerSourceProvider = bun.SourceMap.DevServerSourceProvider; + const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue; const VirtualMachine = jsc.VirtualMachine; - -const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; -const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; diff --git a/src/bun.zig b/src/bun.zig index a1230783c9..fb4b98ba84 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1488,8 +1488,8 @@ pub fn concat(comptime T: type, dest: []T, src: []const []const T) void { } pub const renamer = @import("./renamer.zig"); -// TODO: Rename to SourceMap as this is a struct. -pub const sourcemap = @import("./sourcemap/sourcemap.zig"); + +pub const SourceMap = @import("./sourcemap/sourcemap.zig"); /// Attempt to coerce some value into a byte slice. pub fn asByteSlice(buffer: anytype) []const u8 { diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig index cdf6eec74c..a7f880bd32 100644 --- a/src/bundler/Chunk.zig +++ b/src/bundler/Chunk.zig @@ -29,7 +29,7 @@ pub const Chunk = struct { has_html_chunk: bool = false, is_browser_chunk_from_server_build: bool = false, - output_source_map: sourcemap.SourceMapPieces, + output_source_map: SourceMap.SourceMapPieces, intermediate_output: IntermediateOutput = .{ .empty = {} }, isolated_hash: u64 = std.math.maxInt(u64), @@ -116,7 +116,7 @@ pub const Chunk = struct { pub const CodeResult = struct { buffer: []u8, - shifts: []sourcemap.SourceMapShifts, + shifts: []SourceMap.SourceMapShifts, }; pub fn getSize(this: *const IntermediateOutput) usize { @@ -181,12 +181,12 @@ pub const Chunk = struct { const entry_point_chunks_for_scb = linker_graph.files.items(.entry_point_chunk_index); var shift = if (enable_source_map_shifts) - sourcemap.SourceMapShifts{ + SourceMap.SourceMapShifts{ .after = .{}, .before = .{}, }; var shifts = if (enable_source_map_shifts) - try std.ArrayList(sourcemap.SourceMapShifts).initCapacity(bun.default_allocator, pieces.len + 1); + try std.ArrayList(SourceMap.SourceMapShifts).initCapacity(bun.default_allocator, pieces.len + 1); if (enable_source_map_shifts) shifts.appendAssumeCapacity(shift); @@ -245,7 +245,7 @@ pub const Chunk = struct { } const debug_id_len = if (enable_source_map_shifts and FeatureFlags.source_map_debug_id) - std.fmt.count("\n//# debugId={}\n", .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}) + std.fmt.count("\n//# debugId={}\n", .{bun.SourceMap.DebugIDFormatter{ .id = chunk.isolated_hash }}) else 0; @@ -256,7 +256,7 @@ pub const Chunk = struct { const data = piece.data(); if (enable_source_map_shifts) { - var data_offset = sourcemap.LineColumnOffset{}; + var data_offset = SourceMap.LineColumnOffset{}; data_offset.advance(data); shift.before.add(data_offset); shift.after.add(data_offset); @@ -353,7 +353,7 @@ pub const Chunk = struct { remain = remain[(std.fmt.bufPrint( remain, "\n//# debugId={}\n", - .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}, + .{bun.SourceMap.DebugIDFormatter{ .id = chunk.isolated_hash }}, ) catch |err| switch (err) { error.NoSpaceLeft => std.debug.panic( "unexpected NoSpaceLeft error from bufPrint", @@ -370,7 +370,7 @@ pub const Chunk = struct { .shifts = if (enable_source_map_shifts) shifts.items else - &[_]sourcemap.SourceMapShifts{}, + &[_]SourceMap.SourceMapShifts{}, }; }, .joiner => |*joiner| { @@ -386,7 +386,7 @@ pub const Chunk = struct { const debug_id_fmt = std.fmt.allocPrint( graph.heap.allocator(), "\n//# debugId={}\n", - .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}, + .{bun.SourceMap.DebugIDFormatter{ .id = chunk.isolated_hash }}, ) catch |err| bun.handleOom(err); break :brk try joiner.doneWithEnd(allocator, debug_id_fmt); @@ -397,12 +397,12 @@ pub const Chunk = struct { return .{ .buffer = buffer, - .shifts = &[_]sourcemap.SourceMapShifts{}, + .shifts = &[_]SourceMap.SourceMapShifts{}, }; }, .empty => return .{ .buffer = "", - .shifts = &[_]sourcemap.SourceMapShifts{}, + .shifts = &[_]SourceMap.SourceMapShifts{}, }, } } @@ -651,10 +651,10 @@ const FeatureFlags = bun.FeatureFlags; const ImportKind = bun.ImportKind; const ImportRecord = bun.ImportRecord; const Output = bun.Output; +const SourceMap = bun.SourceMap; const StringJoiner = bun.StringJoiner; const default_allocator = bun.default_allocator; const renamer = bun.renamer; -const sourcemap = bun.sourcemap; const strings = bun.strings; const AutoBitSet = bun.bit_set.AutoBitSet; const BabyList = bun.collections.BabyList; diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 8b5e8aba8f..200d3c3d40 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -129,7 +129,7 @@ pub const LinkerContext = struct { pub fn computeLineOffsets(this: *LinkerContext, alloc: std.mem.Allocator, source_index: Index.Int) void { debug("Computing LineOffsetTable: {d}", .{source_index}); - const line_offset_table: *bun.sourcemap.LineOffsetTable.List = &this.graph.files.items(.line_offset_table)[source_index]; + const line_offset_table: *bun.SourceMap.LineOffsetTable.List = &this.graph.files.items(.line_offset_table)[source_index]; const source: *const Logger.Source = &this.parse_graph.input_files.items(.source)[source_index]; const loader: options.Loader = this.parse_graph.input_files.items(.loader)[source_index]; @@ -142,7 +142,7 @@ pub const LinkerContext = struct { const approximate_line_count = this.graph.ast.items(.approximate_newline_count)[source_index]; - line_offset_table.* = bun.sourcemap.LineOffsetTable.generate( + line_offset_table.* = bun.SourceMap.LineOffsetTable.generate( alloc, source.contents, @@ -686,7 +686,7 @@ pub const LinkerContext = struct { results: std.MultiArrayList(CompileResultForSourceMap), chunk_abs_dir: string, can_have_shifts: bool, - ) !sourcemap.SourceMapPieces { + ) !SourceMap.SourceMapPieces { const trace = bun.perf.trace("Bundler.generateSourceMapForChunk"); defer trace.end(); @@ -776,7 +776,7 @@ pub const LinkerContext = struct { ); const mapping_start = j.len; - var prev_end_state = sourcemap.SourceMapState{}; + var prev_end_state = SourceMap.SourceMapState{}; var prev_column_offset: i32 = 0; const source_map_chunks = results.items(.source_map_chunk); const offsets = results.items(.generated_offset); @@ -784,7 +784,7 @@ pub const LinkerContext = struct { const mapping_source_index = source_id_map.get(current_source_index) orelse unreachable; // the pass above during printing of "sources" must add the index - var start_state = sourcemap.SourceMapState{ + var start_state = SourceMap.SourceMapState{ .source_index = mapping_source_index, .generated_line = offset.lines.zeroBased(), .generated_column = offset.columns.zeroBased(), @@ -794,7 +794,7 @@ pub const LinkerContext = struct { start_state.generated_column += prev_column_offset; } - try sourcemap.appendSourceMapChunk(&j, worker.allocator, prev_end_state, start_state, chunk.buffer.list.items); + try SourceMap.appendSourceMapChunk(&j, worker.allocator, prev_end_state, start_state, chunk.buffer.list.items); prev_end_state = chunk.end_state; prev_end_state.source_index = mapping_source_index; @@ -810,7 +810,7 @@ pub const LinkerContext = struct { if (comptime FeatureFlags.source_map_debug_id) { j.pushStatic("\",\n \"debugId\": \""); j.push( - try std.fmt.allocPrint(worker.allocator, "{}", .{bun.sourcemap.DebugIDFormatter{ .id = isolated_hash }}), + try std.fmt.allocPrint(worker.allocator, "{}", .{bun.SourceMap.DebugIDFormatter{ .id = isolated_hash }}), worker.allocator, ); j.pushStatic("\",\n \"names\": []\n}"); @@ -821,7 +821,7 @@ pub const LinkerContext = struct { const done = try j.done(worker.allocator); bun.assert(done[0] == '{'); - var pieces = sourcemap.SourceMapPieces.init(worker.allocator); + var pieces = SourceMap.SourceMapPieces.init(worker.allocator); if (can_have_shifts) { try pieces.prefix.appendSlice(done[0..mapping_start]); try pieces.mappings.appendSlice(done[mapping_start..mapping_end]); @@ -1411,7 +1411,7 @@ pub const LinkerContext = struct { const SubstituteChunkFinalPathResult = struct { j: StringJoiner, - shifts: []sourcemap.SourceMapShifts, + shifts: []SourceMap.SourceMapShifts, }; pub fn mangleLocalCss(c: *LinkerContext) void { @@ -2684,11 +2684,11 @@ const MultiArrayList = bun.MultiArrayList; const MutableString = bun.MutableString; const OOM = bun.OOM; const Output = bun.Output; +const SourceMap = bun.SourceMap; const StringJoiner = bun.StringJoiner; const bake = bun.bake; const base64 = bun.base64; const renamer = bun.renamer; -const sourcemap = bun.sourcemap; const strings = bun.strings; const sync = bun.threading; const AutoBitSet = bun.bit_set.AutoBitSet; diff --git a/src/bundler/LinkerGraph.zig b/src/bundler/LinkerGraph.zig index c160e96c28..8fd01500ae 100644 --- a/src/bundler/LinkerGraph.zig +++ b/src/bundler/LinkerGraph.zig @@ -458,7 +458,7 @@ pub const File = struct { /// a Source.Index to its output path inb reakOutputIntoPieces entry_point_chunk_index: u32 = std.math.maxInt(u32), - line_offset_table: bun.sourcemap.LineOffsetTable.List = .empty, + line_offset_table: bun.SourceMap.LineOffsetTable.List = .empty, quoted_source_contents: Owned(?[]u8) = .initNull(), pub fn isEntryPoint(this: *const File) bool { diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 9b5d912dd5..712a8a99b5 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2868,7 +2868,7 @@ pub const BundleV2 = struct { .parts_in_chunk_in_order = js_part_ranges, }, }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), }; // Then all the distinct CSS bundles (these are JS->CSS, not CSS->CSS) @@ -2886,7 +2886,7 @@ pub const BundleV2 = struct { .asts = try this.allocator().alloc(bun.css.BundlerStyleSheet, order.len), }, }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), }; } @@ -2899,7 +2899,7 @@ pub const BundleV2 = struct { .is_entry_point = false, }, .content = .html, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), }; } @@ -4264,7 +4264,7 @@ pub const CompileResult = union(enum) { css: struct { result: bun.Maybe([]const u8, anyerror), source_index: Index.Int, - source_map: ?bun.sourcemap.Chunk = null, + source_map: ?bun.SourceMap.Chunk = null, }, html: struct { source_index: Index.Int, @@ -4295,7 +4295,7 @@ pub const CompileResult = union(enum) { }; } - pub fn sourceMapChunk(this: *const CompileResult) ?sourcemap.Chunk { + pub fn sourceMapChunk(this: *const CompileResult) ?SourceMap.Chunk { return switch (this.*) { .javascript => |r| switch (r.result) { .result => |r2| r2.source_map, @@ -4314,8 +4314,8 @@ pub const CompileResult = union(enum) { }; pub const CompileResultForSourceMap = struct { - source_map_chunk: sourcemap.Chunk, - generated_offset: sourcemap.LineColumnOffset, + source_map_chunk: SourceMap.Chunk, + generated_offset: SourceMap.LineColumnOffset, source_index: u32, }; @@ -4503,7 +4503,7 @@ pub const Part = js_ast.Part; pub const js_printer = @import("../js_printer.zig"); pub const js_ast = bun.ast; pub const linker = @import("../linker.zig"); -pub const sourcemap = bun.sourcemap; +pub const SourceMap = bun.SourceMap; pub const StringJoiner = bun.StringJoiner; pub const base64 = bun.base64; pub const Ref = bun.ast.Ref; diff --git a/src/bundler/linker_context/computeChunks.zig b/src/bundler/linker_context/computeChunks.zig index fd98457e4c..18e8910a6b 100644 --- a/src/bundler/linker_context/computeChunks.zig +++ b/src/bundler/linker_context/computeChunks.zig @@ -63,7 +63,7 @@ pub noinline fn computeChunks( }, .entry_bits = entry_bits.*, .content = .html, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; } @@ -97,7 +97,7 @@ pub noinline fn computeChunks( .asts = bun.handleOom(this.allocator().alloc(bun.css.BundlerStyleSheet, order.len)), }, }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), .has_html_chunk = has_html_chunk, .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; @@ -120,7 +120,7 @@ pub noinline fn computeChunks( .javascript = .{}, }, .has_html_chunk = has_html_chunk, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; @@ -173,7 +173,7 @@ pub noinline fn computeChunks( }, }, .files_with_parts_in_chunk = css_files_with_parts_in_chunk, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), .has_html_chunk = has_html_chunk, .is_browser_chunk_from_server_build = could_be_browser_target_from_server_build and ast_targets[source_index] == .browser, }; @@ -217,7 +217,7 @@ pub noinline fn computeChunks( .content = .{ .javascript = .{}, }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator()), + .output_source_map = SourceMap.SourceMapPieces.init(this.allocator()), .is_browser_chunk_from_server_build = is_browser_chunk_from_server_build, }; } @@ -422,8 +422,8 @@ const std = @import("std"); const bun = @import("bun"); const BabyList = bun.BabyList; +const SourceMap = bun.SourceMap; const options = bun.options; -const sourcemap = bun.sourcemap; const AutoBitSet = bun.bit_set.AutoBitSet; const bundler = bun.bundle_v2; diff --git a/src/bundler/linker_context/postProcessCSSChunk.zig b/src/bundler/linker_context/postProcessCSSChunk.zig index c969b53c0e..1f2b6dc5b4 100644 --- a/src/bundler/linker_context/postProcessCSSChunk.zig +++ b/src/bundler/linker_context/postProcessCSSChunk.zig @@ -8,7 +8,7 @@ pub fn postProcessCSSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, ch }, }; - var line_offset: bun.sourcemap.LineColumnOffset.Optional = if (c.options.source_maps != .none) .{ .value = .{} } else .{ .null = {} }; + var line_offset: bun.SourceMap.LineColumnOffset.Optional = if (c.options.source_maps != .none) .{ .value = .{} } else .{ .null = {} }; var newline_before_comment = false; diff --git a/src/bundler/linker_context/postProcessJSChunk.zig b/src/bundler/linker_context/postProcessJSChunk.zig index 110b3870cd..d174286d16 100644 --- a/src/bundler/linker_context/postProcessJSChunk.zig +++ b/src/bundler/linker_context/postProcessJSChunk.zig @@ -110,7 +110,7 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu errdefer j.deinit(); const output_format = c.options.output_format; - var line_offset: bun.sourcemap.LineColumnOffset.Optional = if (c.options.source_maps != .none) .{ .value = .{} } else .{ .null = {} }; + var line_offset: bun.SourceMap.LineColumnOffset.Optional = if (c.options.source_maps != .none) .{ .value = .{} } else .{ .null = {} }; // Concatenate the generated JavaScript chunks together diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index e49fd8c7e1..5d4081a7e0 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -426,7 +426,6 @@ const base64 = bun.base64; const default_allocator = bun.default_allocator; const jsc = bun.jsc; const options = bun.options; -const sourcemap = bun.sourcemap; const strings = bun.strings; const bundler = bun.bundle_v2; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 8436e1394d..6466f14b88 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -960,7 +960,7 @@ pub const CommandLineReporter = struct { var map = coverage.ByteRangeMapping.map orelse return; var iter = map.valueIterator(); - var byte_ranges = try std.ArrayList(bun.sourcemap.coverage.ByteRangeMapping).initCapacity(bun.default_allocator, map.count()); + var byte_ranges = try std.ArrayList(bun.SourceMap.coverage.ByteRangeMapping).initCapacity(bun.default_allocator, map.count()); while (iter.next()) |entry| { byte_ranges.appendAssumeCapacity(entry.*); @@ -971,10 +971,10 @@ pub const CommandLineReporter = struct { } std.sort.pdq( - bun.sourcemap.coverage.ByteRangeMapping, + bun.SourceMap.coverage.ByteRangeMapping, byte_ranges.items, {}, - bun.sourcemap.coverage.ByteRangeMapping.isLessThan, + bun.SourceMap.coverage.ByteRangeMapping.isLessThan, ); try this.printCodeCoverage(vm, opts, byte_ranges.items, reporters, enable_ansi_colors); @@ -984,7 +984,7 @@ pub const CommandLineReporter = struct { _: *CommandLineReporter, vm: *jsc.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, - byte_ranges: []bun.sourcemap.coverage.ByteRangeMapping, + byte_ranges: []bun.SourceMap.coverage.ByteRangeMapping, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool, ) !void { @@ -1054,7 +1054,7 @@ pub const CommandLineReporter = struct { var console_buffer_buffer = console_buffer.bufferedWriter(); var console_writer = console_buffer_buffer.writer(); - var avg = bun.sourcemap.coverage.Fraction{ + var avg = bun.SourceMap.coverage.Fraction{ .functions = 0.0, .lines = 0.0, .stmts = 0.0, @@ -1185,7 +1185,7 @@ pub const CommandLineReporter = struct { avg.stmts /= avg_count; } - const failed = if (avg_count > 0) base_fraction else bun.sourcemap.coverage.Fraction{ + const failed = if (avg_count > 0) base_fraction else bun.SourceMap.coverage.Fraction{ .functions = 0, .lines = 0, .stmts = 0, @@ -1280,7 +1280,7 @@ pub const TestCommand = struct { skip_test_files: bool = !Environment.allow_assert, reporters: Reporters = .{ .text = true, .lcov = false }, reports_directory: string = "coverage", - fractions: bun.sourcemap.coverage.Fraction = .{}, + fractions: bun.SourceMap.coverage.Fraction = .{}, ignore_sourcemap: bool = false, enabled: bool = false, fail_on_low_coverage: bool = false, @@ -2010,12 +2010,12 @@ const strings = bun.strings; const uws = bun.uws; const HTTPThread = bun.http.HTTPThread; +const coverage = bun.SourceMap.coverage; +const CodeCoverageReport = coverage.Report; + const jsc = bun.jsc; const jest = jsc.Jest; const Snapshots = jsc.Snapshot.Snapshots; const TestRunner = jsc.Jest.TestRunner; const Test = TestRunner.Test; - -const coverage = bun.sourcemap.coverage; -const CodeCoverageReport = coverage.Report; diff --git a/src/js_printer.zig b/src/js_printer.zig index c510555bba..15fee9f6eb 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -390,7 +390,7 @@ pub const Options = struct { allocator: std.mem.Allocator = default_allocator, source_map_allocator: ?std.mem.Allocator = null, source_map_handler: ?SourceMapHandler = null, - source_map_builder: ?*bun.sourcemap.Chunk.Builder = null, + source_map_builder: ?*bun.SourceMap.Chunk.Builder = null, css_import_behavior: api.CssInJsBehavior = api.CssInJsBehavior.facade, target: options.Target = .browser, diff --git a/src/sourcemap/CodeCoverage.zig b/src/sourcemap/CodeCoverage.zig index bc4a8ee4be..b6d6899ce4 100644 --- a/src/sourcemap/CodeCoverage.zig +++ b/src/sourcemap/CodeCoverage.zig @@ -726,7 +726,7 @@ const std = @import("std"); const bun = @import("bun"); const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const LineOffsetTable = bun.sourcemap.LineOffsetTable; +const LineOffsetTable = bun.SourceMap.LineOffsetTable; const Output = bun.Output; const prettyFmt = Output.prettyFmt; diff --git a/src/sourcemap/JSSourceMap.zig b/src/sourcemap/JSSourceMap.zig index 0414359019..6162eda416 100644 --- a/src/sourcemap/JSSourceMap.zig +++ b/src/sourcemap/JSSourceMap.zig @@ -2,7 +2,7 @@ /// const JSSourceMap = @This(); -sourcemap: *bun.sourcemap.ParsedSourceMap, +sourcemap: *bun.SourceMap.ParsedSourceMap, sources: []bun.String = &.{}, names: []bun.String = &.{}, @@ -136,7 +136,7 @@ pub fn constructor( } // Parse the VLQ mappings - const parse_result = bun.sourcemap.Mapping.parse( + const parse_result = bun.SourceMap.Mapping.parse( bun.default_allocator, mappings_str.slice(), null, // estimated_mapping_count @@ -156,7 +156,7 @@ pub fn constructor( }; const source_map = bun.new(JSSourceMap, .{ - .sourcemap = bun.new(bun.sourcemap.ParsedSourceMap, mapping_list), + .sourcemap = bun.new(bun.SourceMap.ParsedSourceMap, mapping_list), .sources = sources.items, .names = names.items, }); @@ -200,7 +200,7 @@ fn getLineColumn(globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSErr }; } -fn mappingNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue { +fn mappingNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.SourceMap.Mapping) bun.JSError!JSValue { const name_index = mapping.nameIndex(); if (name_index >= 0) { if (this.sourcemap.mappings.getName(name_index)) |name| { @@ -215,7 +215,7 @@ fn mappingNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapp return .js_undefined; } -fn sourceNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue { +fn sourceNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.SourceMap.Mapping) bun.JSError!JSValue { const source_index = mapping.sourceIndex(); if (source_index >= 0 and source_index < @as(i32, @intCast(this.sources.len))) { return this.sources[@intCast(source_index)].toJS(globalObject); From 2afafbfa23b42f7eb1877cbeae012cd0855674ed Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 27 Oct 2025 11:26:21 -0800 Subject: [PATCH 254/391] zig: remove Location.suggestion (#23478) --- src/api/schema.zig | 5 ----- src/bundler/ParseTask.zig | 1 - src/logger.zig | 10 +--------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/api/schema.zig b/src/api/schema.zig index 8e28eb94fd..cbb4274055 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2335,9 +2335,6 @@ pub const api = struct { /// line_text line_text: []const u8, - /// suggestion - suggestion: []const u8, - /// offset offset: u32 = 0, @@ -2349,7 +2346,6 @@ pub const api = struct { this.line = try reader.readValue(i32); this.column = try reader.readValue(i32); this.line_text = try reader.readValue([]const u8); - this.suggestion = try reader.readValue([]const u8); this.offset = try reader.readValue(u32); return this; } @@ -2360,7 +2356,6 @@ pub const api = struct { try writer.writeInt(this.line); try writer.writeInt(this.column); try writer.writeValue(@TypeOf(this.line_text), this.line_text); - try writer.writeValue(@TypeOf(this.suggestion), this.suggestion); try writer.writeInt(this.offset); } }; diff --git a/src/bundler/ParseTask.zig b/src/bundler/ParseTask.zig index 60241db5dd..a20b43cb53 100644 --- a/src/bundler/ParseTask.zig +++ b/src/bundler/ParseTask.zig @@ -833,7 +833,6 @@ const OnBeforeParsePlugin = struct { @max(this.column, -1), @max(this.column_end - this.column, 0), if (source_line_text.len > 0) bun.handleOom(allocator.dupe(u8, source_line_text)) else null, - null, ); var msg = Logger.Msg{ .data = .{ .location = location, .text = bun.handleOom(allocator.dupe(u8, this.message())) } }; switch (this.level) { diff --git a/src/logger.zig b/src/logger.zig index c778cc9cad..ab70c6b1c5 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -86,8 +86,6 @@ pub const Location = struct { length: usize = 0, /// Text on the line, avoiding the need to refetch the source code line_text: ?string = null, - // TODO: remove this unused field - suggestion: ?string = null, // TODO: document or remove offset: usize = 0, @@ -96,7 +94,6 @@ pub const Location = struct { cost += this.file.len; cost += this.namespace.len; if (this.line_text) |text| cost += text.len; - if (this.suggestion) |text| cost += text.len; return cost; } @@ -104,7 +101,6 @@ pub const Location = struct { builder.count(this.file); builder.count(this.namespace); if (this.line_text) |text| builder.count(text); - if (this.suggestion) |text| builder.count(text); } pub fn clone(this: Location, allocator: std.mem.Allocator) !Location { @@ -115,7 +111,6 @@ pub const Location = struct { .column = this.column, .length = this.length, .line_text = if (this.line_text != null) try allocator.dupe(u8, this.line_text.?) else null, - .suggestion = if (this.suggestion != null) try allocator.dupe(u8, this.suggestion.?) else null, .offset = this.offset, }; } @@ -128,7 +123,6 @@ pub const Location = struct { .column = this.column, .length = this.length, .line_text = if (this.line_text != null) string_builder.append(this.line_text.?) else null, - .suggestion = if (this.suggestion != null) string_builder.append(this.suggestion.?) else null, .offset = this.offset, }; } @@ -140,7 +134,6 @@ pub const Location = struct { .line = this.line, .column = this.column, .line_text = this.line_text orelse "", - .suggestion = this.suggestion orelse "", .offset = @as(u32, @truncate(this.offset)), }; } @@ -148,7 +141,7 @@ pub const Location = struct { // don't really know what's safe to deinit here! pub fn deinit(_: *Location, _: std.mem.Allocator) void {} - pub fn init(file: string, namespace: string, line: i32, column: i32, length: u32, line_text: ?string, suggestion: ?string) Location { + pub fn init(file: string, namespace: string, line: i32, column: i32, length: u32, line_text: ?string) Location { return Location{ .file = file, .namespace = namespace, @@ -156,7 +149,6 @@ pub const Location = struct { .column = column, .length = length, .line_text = line_text, - .suggestion = suggestion, .offset = length, }; } From 64bfd8b938fc92922481cc8e4c40e0c66c54c9cc Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 27 Oct 2025 11:49:41 -0800 Subject: [PATCH 255/391] Revert "deps: update elysia to 1.4.13" (#24133) --- test/vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/vendor.json b/test/vendor.json index 06a7d08a12..05ca430f3a 100644 --- a/test/vendor.json +++ b/test/vendor.json @@ -2,6 +2,6 @@ { "package": "elysia", "repository": "https://github.com/elysiajs/elysia", - "tag": "1.4.13" + "tag": "1.4.12" } ] From f3ed784a6b356b6552964f042ad532f81e95feda Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 27 Oct 2025 12:11:00 -0800 Subject: [PATCH 256/391] scripts: teach machine.mjs how to spawn a freebsd image on aws (#24109) exploratory look into https://github.com/oven-sh/bun/issues/1524 this still leaves that far off from being closed but an important first step this is important because this script is used to spawn our base images for CI and will provide boxes for local testing not sure how far i'll get but a rough "road to freebsd" map for anyone reading: - [x] this - [ ] ensure `bootstrap.sh` can run successfully - [ ] ensure WebKit can build from source - [ ] ensure other dependencies can build from source - [ ] add freebsd to our WebKit fork releases - [ ] add freebsd to our Zig fork releases - [ ] ensure bun can build from source - [ ] run `[build images]` and add freebsd to CI - [ ] fix runtime test failures image --- package.json | 1 + scripts/machine.mjs | 25 +++++++++++++++++++++++-- scripts/utils.mjs | 12 +++++++----- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c0fcee4b5f..b5d18cf725 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "machine:linux:alpine": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=alpine --release=3.22", "machine:linux:amazonlinux": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=amazonlinux --release=2023", "machine:windows:2019": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=windows --release=2019", + "machine:freebsd": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.large --os=freebsd --release=14.3", "sync-webkit-source": "bun ./scripts/sync-webkit-source.ts" } } diff --git a/scripts/machine.mjs b/scripts/machine.mjs index 5ea5cdae66..5c7998aa80 100755 --- a/scripts/machine.mjs +++ b/scripts/machine.mjs @@ -389,6 +389,9 @@ const aws = { owner = "amazon"; name = `Windows_Server-${release || "*"}-English-Full-Base-*`; } + } else if (os === "freebsd") { + owner = "782442783595"; // upstream member of FreeBSD team, likely Colin Percival + name = `FreeBSD ${release}-STABLE-${{ "aarch64": "arm64", "x64": "amd64" }[arch] ?? "amd64"}-* UEFI-PREFERRED cloud-init UFS`; } if (!name) { @@ -400,6 +403,7 @@ const aws = { "owner-alias": owner, "name": name, }); + // console.table(baseImages.map(v => v.Name)); if (!baseImages.length) { throw new Error(`No base image found: ${inspect(options)}`); @@ -425,6 +429,8 @@ const aws = { } const { ImageId, Name, RootDeviceName, BlockDeviceMappings } = image; + // console.table({ os, arch, instanceType, Name, ImageId }); + const blockDeviceMappings = BlockDeviceMappings.map(device => { const { DeviceName } = device; if (DeviceName === RootDeviceName) { @@ -620,6 +626,7 @@ const aws = { * @property {SshKey[]} [sshKeys] * @property {string} [username] * @property {string} [password] + * @property {Os} [os] */ /** @@ -648,6 +655,7 @@ function getCloudInit(cloudInit) { const authorizedKeys = cloudInit["sshKeys"]?.map(({ publicKey }) => publicKey) || []; let sftpPath = "/usr/lib/openssh/sftp-server"; + let shell = "/bin/bash"; switch (cloudInit["distro"]) { case "alpine": sftpPath = "/usr/lib/ssh/sftp-server"; @@ -658,6 +666,18 @@ function getCloudInit(cloudInit) { sftpPath = "/usr/libexec/openssh/sftp-server"; break; } + switch (cloudInit["os"]) { + case "linux": + case "windows": + // handled above + break; + case "freebsd": + sftpPath = "/usr/libexec/openssh/sftp-server"; + shell = "/bin/csh"; + break; + default: + throw new Error(`Unsupported os: ${cloudInit["os"]}`); + } let users; if (username === "root") { @@ -671,7 +691,7 @@ function getCloudInit(cloudInit) { users: - name: ${username} sudo: ALL=(ALL) NOPASSWD:ALL - shell: /bin/bash + shell: ${shell} ssh_authorized_keys: ${authorizedKeys.map(key => ` - ${key}`).join("\n")} @@ -1050,7 +1070,7 @@ function getCloud(name) { } /** - * @typedef {"linux" | "darwin" | "windows"} Os + * @typedef {"linux" | "darwin" | "windows" | "freebsd"} Os * @typedef {"aarch64" | "x64"} Arch * @typedef {"macos" | "windowsserver" | "debian" | "ubuntu" | "alpine" | "amazonlinux"} Distro */ @@ -1204,6 +1224,7 @@ async function main() { }; let { detached, bootstrap, ci, os, arch, distro, release, features } = options; + if (os === "freebsd") bootstrap = false; let name = `${os}-${arch}-${(release || "").replace(/\./g, "")}`; diff --git a/scripts/utils.mjs b/scripts/utils.mjs index 604227f9cd..c9ad28be53 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -1538,7 +1538,7 @@ export function parseNumber(value) { /** * @param {string} string - * @returns {"darwin" | "linux" | "windows"} + * @returns {"darwin" | "linux" | "windows" | "freebsd"} */ export function parseOs(string) { if (/darwin|apple|mac/i.test(string)) { @@ -1550,6 +1550,9 @@ export function parseOs(string) { if (/win/i.test(string)) { return "windows"; } + if (/freebsd/i.test(string)) { + return "freebsd"; + } throw new Error(`Unsupported operating system: ${string}`); } @@ -1900,22 +1903,21 @@ export function getUsernameForDistro(distro) { if (/windows/i.test(distro)) { return "administrator"; } - if (/alpine|centos/i.test(distro)) { return "root"; } - if (/debian/i.test(distro)) { return "admin"; } - if (/ubuntu/i.test(distro)) { return "ubuntu"; } - if (/amazon|amzn|al\d+|rhel/i.test(distro)) { return "ec2-user"; } + if (/freebsd/i.test(distro)) { + return "root"; + } throw new Error(`Unsupported distro: ${distro}`); } From 6580b563b00c55270a68f42374afe5c2ff36d14d Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 27 Oct 2025 14:19:38 -0700 Subject: [PATCH 257/391] Refactor Subprocess to use JSRef instead of hasPendingActivity (#24090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Refactors `Subprocess` to use explicit strong/weak reference management via `JSRef` instead of the `hasPendingActivity` mechanism that relies on JSC's internal `WeakHandleOwner`. ## Changes ### Core Refactoring - **JSRef.zig**: Added `update()` method to update references in-place - **subprocess.zig**: Changed `this_jsvalue: JSValue` to `this_value: JSRef` - **subprocess.zig**: Renamed `hasPendingActivityNonThreadsafe()` to `computeHasPendingActivity()` - **subprocess.zig**: Updated `updateHasPendingActivity()` to upgrade/downgrade `JSRef` based on pending activity - **subprocess.zig**: Removed `hasPendingActivity()` C callback function - **subprocess.zig**: Updated `finalize()` to call `this_value.finalize()` - **BunObject.classes.ts**: Set `hasPendingActivity: false` for Subprocess - **Writable.zig**: Updated references from `this_jsvalue` to `this_value.tryGet()` - **ipc.zig**: Updated references from `this_jsvalue` to `this_value.tryGet()` ## How It Works **Before**: Used `hasPendingActivity: true` which created a `JSC::Weak` reference with a `JSC::WeakHandleOwner` that kept the object alive as long as the C callback returned true. **After**: Uses `JSRef` with explicit lifecycle management: 1. Starts with a **weak** reference when subprocess is created 2. Immediately calls `updateHasPendingActivity()` after creation 3. **Upgrades to strong** reference when `computeHasPendingActivity()` returns true: - Subprocess hasn't exited - Has active stdio streams - Has active IPC connection 4. **Downgrades to weak** reference when all activity completes 5. GC can collect the subprocess once it's weak and no other references exist ## Benefits - Explicit control over subprocess lifecycle instead of relying on JSC's internal mechanisms - Clearer semantics: strong reference = "keep alive", weak reference = "can be GC'd" - Removes dependency on `WeakHandleOwner` callback overhead ## Testing - ✅ `test/js/bun/spawn/spawn.ipc.test.ts` - All 4 tests pass - ✅ `test/js/bun/spawn/spawn-stress.test.ts` - All tests pass (100 iterations) - ⚠️ `test/js/bun/spawn/spawnSync.test.ts` - 3/6 pass (3 pre-existing timing-based failures unrelated to this change) Manual testing confirms: - Subprocess is kept alive without user reference while running - Subprocess can be GC'd after completion - IPC keeps subprocess alive correctly - No crashes or memory leaks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/bun.js/api/BunObject.classes.ts | 1 - src/bun.js/api/bun/subprocess.zig | 50 ++++++++++++---------- src/bun.js/api/bun/subprocess/Writable.zig | 8 ++-- src/bun.js/bindings/JSRef.zig | 17 ++++++++ src/bun.js/ipc.zig | 2 +- 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/bun.js/api/BunObject.classes.ts b/src/bun.js/api/BunObject.classes.ts index 455c750f5a..b55a31d629 100644 --- a/src/bun.js/api/BunObject.classes.ts +++ b/src/bun.js/api/BunObject.classes.ts @@ -46,7 +46,6 @@ export default [ construct: true, noConstructor: true, finalize: true, - hasPendingActivity: true, configurable: false, memoryCost: true, klass: {}, diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index c0f0024e06..3ec2bd7cfb 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -28,8 +28,7 @@ observable_getters: std.enums.EnumSet(enum { stdio, }) = .{}, closed: std.enums.EnumSet(StdioKind) = .{}, -has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), -this_jsvalue: jsc.JSValue = .zero, +this_value: jsc.JSRef = jsc.JSRef.empty(), /// `null` indicates all of the IPC data is uninitialized. ipc_data: ?IPC.SendQueue, @@ -169,7 +168,7 @@ pub fn hasExited(this: *const Subprocess) bool { return this.process.hasExited(); } -pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { +pub fn computeHasPendingActivity(this: *const Subprocess) bool { if (this.ipc_data != null) { return true; } @@ -186,16 +185,19 @@ pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { } pub fn updateHasPendingActivity(this: *Subprocess) void { + if (this.flags.is_sync) return; + + const has_pending = this.computeHasPendingActivity(); if (comptime Environment.isDebug) { - log("updateHasPendingActivity() {any} -> {any}", .{ - this.has_pending_activity.raw, - this.hasPendingActivityNonThreadsafe(), - }); + log("updateHasPendingActivity() -> {any}", .{has_pending}); + } + + // Upgrade or downgrade the reference based on pending activity + if (has_pending) { + this.this_value.upgrade(this.globalThis); + } else { + this.this_value.downgrade(); } - this.has_pending_activity.store( - this.hasPendingActivityNonThreadsafe(), - .monotonic, - ); } pub fn hasPendingActivityStdio(this: *const Subprocess) bool { @@ -247,10 +249,6 @@ pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void { } } -pub fn hasPendingActivity(this: *Subprocess) callconv(.C) bool { - return this.has_pending_activity.load(.acquire); -} - pub fn jsRef(this: *Subprocess) void { this.process.enableKeepingEventLoopAlive(); @@ -406,7 +404,9 @@ pub fn kill( globalThis: *JSGlobalObject, callframe: *jsc.CallFrame, ) bun.JSError!JSValue { - this.this_jsvalue = callframe.this(); + // Safe: this method can only be called while the object is alive (reachable from JS) + // The finalizer only runs when the object becomes unreachable + this.this_value.update(globalThis, callframe.this()); const arguments = callframe.arguments_old(1); // If signal is 0, then no actual signal is sent, but error checking @@ -606,7 +606,7 @@ fn consumeOnDisconnectCallback(this_jsvalue: JSValue, globalThis: *jsc.JSGlobalO pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Status, rusage: *const Rusage) void { log("onProcessExit()", .{}); - const this_jsvalue = this.this_jsvalue; + const this_jsvalue = this.this_value.tryGet() orelse .zero; const globalThis = this.globalThis; const jsc_vm = globalThis.bunVM(); this_jsvalue.ensureStillAlive(); @@ -809,11 +809,11 @@ pub fn finalize(this: *Subprocess) callconv(.C) void { // Ensure any code which references the "this" value doesn't attempt to // access it after it's been freed We cannot call any methods which // access GC'd values during the finalizer - this.this_jsvalue = .zero; + this.this_value.finalize(); this.clearAbortSignal(); - bun.assert(!this.hasPendingActivity() or jsc.VirtualMachine.get().isShuttingDown()); + bun.assert(!this.computeHasPendingActivity() or jsc.VirtualMachine.get().isShuttingDown()); this.finalizeStreams(); this.process.detach(); @@ -1567,7 +1567,11 @@ pub fn spawnMaybeSync( subprocess.toJS(globalThis) else JSValue.zero; - subprocess.this_jsvalue = out; + if (out != .zero) { + subprocess.this_value.setWeak(out); + // Immediately upgrade to strong if there's pending activity to prevent premature GC + subprocess.updateHasPendingActivity(); + } var send_exit_notification = false; @@ -1703,7 +1707,7 @@ pub fn spawnMaybeSync( defer { jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; } - while (subprocess.hasPendingActivityNonThreadsafe()) { + while (subprocess.computeHasPendingActivity()) { if (subprocess.stdin == .buffer) { subprocess.stdin.buffer.watch(); } @@ -1778,7 +1782,7 @@ pub fn handleIPCMessage( }, .data => |data| { IPC.log("Received IPC message from child", .{}); - const this_jsvalue = this.this_jsvalue; + const this_jsvalue = this.this_value.tryGet() orelse .zero; defer this_jsvalue.ensureStillAlive(); if (this_jsvalue != .zero) { if (jsc.Codegen.JSSubprocess.ipcCallbackGetCached(this_jsvalue)) |cb| { @@ -1801,7 +1805,7 @@ pub fn handleIPCMessage( pub fn handleIPCClose(this: *Subprocess) void { IPClog("Subprocess#handleIPCClose", .{}); - const this_jsvalue = this.this_jsvalue; + const this_jsvalue = this.this_value.tryGet() orelse .zero; defer this_jsvalue.ensureStillAlive(); const globalThis = this.globalThis; this.updateHasPendingActivity(); diff --git a/src/bun.js/api/bun/subprocess/Writable.zig b/src/bun.js/api/bun/subprocess/Writable.zig index 47e61ec1b4..dde982beef 100644 --- a/src/bun.js/api/bun/subprocess/Writable.zig +++ b/src/bun.js/api/bun/subprocess/Writable.zig @@ -54,8 +54,8 @@ pub const Writable = union(enum) { pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { const process: *Subprocess = @fieldParentPtr("stdin", this); - if (process.this_jsvalue != .zero) { - if (js.stdinGetCached(process.this_jsvalue)) |existing_value| { + if (process.this_value.tryGet()) |this_jsvalue| { + if (js.stdinGetCached(this_jsvalue)) |existing_value| { jsc.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); } } @@ -270,8 +270,8 @@ pub const Writable = union(enum) { pub fn finalize(this: *Writable) void { const subprocess: *Subprocess = @fieldParentPtr("stdin", this); - if (subprocess.this_jsvalue != .zero) { - if (jsc.Codegen.JSSubprocess.stdinGetCached(subprocess.this_jsvalue)) |existing_value| { + if (subprocess.this_value.tryGet()) |this_jsvalue| { + if (jsc.Codegen.JSSubprocess.stdinGetCached(this_jsvalue)) |existing_value| { jsc.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); } } diff --git a/src/bun.js/bindings/JSRef.zig b/src/bun.js/bindings/JSRef.zig index a90e0087a7..a8e8516570 100644 --- a/src/bun.js/bindings/JSRef.zig +++ b/src/bun.js/bindings/JSRef.zig @@ -201,6 +201,23 @@ pub const JSRef = union(enum) { this.deinit(); this.* = .{ .finalized = {} }; } + + pub fn update(this: *@This(), globalThis: *jsc.JSGlobalObject, value: JSValue) void { + switch (this.*) { + .weak => { + bun.debugAssert(!value.isEmptyOrUndefinedOrNull()); + this.weak = value; + }, + .strong => { + if (this.strong.get() != value) { + this.strong.set(globalThis, value); + } + }, + .finalized => { + bun.debugAssert(false); + }, + } + } }; const bun = @import("bun"); diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 3d961afa7a..d6ef0f2158 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -1087,7 +1087,7 @@ fn handleIPCMessage(send_queue: *SendQueue, message: DecodedIPCMessage, globalTh const fd: bun.FD = bun.take(&send_queue.incoming_fd).?; const target: bun.jsc.JSValue = switch (send_queue.owner) { - .subprocess => |subprocess| subprocess.this_jsvalue, + .subprocess => |subprocess| subprocess.this_value.tryGet() orelse .zero, .virtual_machine => bun.jsc.JSValue.null, }; From 668eba0eb855fbcbdc9200360ad35d0c9d62884c Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 27 Oct 2025 15:24:38 -0700 Subject: [PATCH 258/391] fix(node:http): Fix ServerResponse.writableNeedDrain causing stream pause (#24137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #19111 This PR fixes a bug where `fs.createReadStream().pipe(ServerResponse)` would fail to transfer data when ServerResponse had no handle (standalone usage). This affected Vite's static file serving and other middleware adapters using the connect-to-web pattern. ## Root Cause The bug was in the `ServerResponse.writableNeedDrain` getter at line 1529 of `_http_server.ts`: ```typescript return !this.destroyed && !this.finished && (this[kHandle]?.bufferedAmount ?? 1) !== 0; ``` When `ServerResponse` had no handle (which is common in middleware scenarios), the nullish coalescing operator defaulted `bufferedAmount` to **1** instead of **0**. This caused `writableNeedDrain` to always return `true`. ## Impact When `pipe()` checks `dest.writableNeedDrain === true`, it immediately pauses the source stream to handle backpressure. With the bug, standalone ServerResponse instances always appeared to need draining, causing piped streams to pause and never resume. ## Fix Changed the default value from `1` to `0`: ```typescript return !this.destroyed && !this.finished && (this[kHandle]?.bufferedAmount ?? 0) !== 0; ``` ## Test Plan - ✅ Added regression test in `test/regression/issue/19111.test.ts` - ✅ Verified fix with actual Vite middleware reproduction - ✅ Confirmed behavior matches Node.js Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/js/node/_http_server.ts | 2 +- test/regression/issue/19111.test.ts | 99 +++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/19111.test.ts diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index 81d627e5f5..6ffcaaf6ad 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -1526,7 +1526,7 @@ ServerResponse.prototype._implicitHeader = function () { Object.defineProperty(ServerResponse.prototype, "writableNeedDrain", { get() { - return !this.destroyed && !this.finished && (this[kHandle]?.bufferedAmount ?? 1) !== 0; + return !this.destroyed && !this.finished && (this[kHandle]?.bufferedAmount ?? 0) !== 0; }, }); diff --git a/test/regression/issue/19111.test.ts b/test/regression/issue/19111.test.ts new file mode 100644 index 0000000000..43e9446904 --- /dev/null +++ b/test/regression/issue/19111.test.ts @@ -0,0 +1,99 @@ +// https://github.com/oven-sh/bun/issues/19111 +// stream.Readable's `readable` event not firing in Bun 1.2.6+ +import assert from "node:assert"; +import { IncomingMessage, ServerResponse } from "node:http"; +import { PassThrough, Readable } from "node:stream"; +import { test } from "node:test"; + +// Helper to create mock IncomingMessage +function createMockIncomingMessage(url: string): IncomingMessage { + return Object.assign(Readable.from([]), { + url, + method: "GET", + headers: {}, + }) as IncomingMessage; +} + +// Focused regression test: Standalone ServerResponse.writableNeedDrain should be false +test("Standalone ServerResponse.writableNeedDrain is false", () => { + const mockReq = createMockIncomingMessage("/need-drain"); + const res = new ServerResponse(mockReq); + + // Regression for #19111: previously true due to defaulting bufferedAmount to 1 + assert.strictEqual(res.writableNeedDrain, false); +}); + +// Helper function for connect-to-web pattern +function createServerResponse(incomingMessage: IncomingMessage) { + const res = new ServerResponse(incomingMessage); + const passThrough = new PassThrough(); + let resolved = false; + + const onReadable = new Promise<{ + readable: Readable; + headers: Record; + statusCode: number; + }>((resolve, reject) => { + const handleReadable = () => { + if (resolved) return; + resolved = true; + resolve({ + readable: passThrough, + headers: res.getHeaders(), + statusCode: res.statusCode, + }); + }; + + const handleError = (err: Error) => { + reject(err); + }; + + passThrough.once("readable", handleReadable); + passThrough.once("end", handleReadable); + passThrough.once("error", handleError); + res.once("error", handleError); + }); + + res.once("finish", () => { + passThrough.end(); + }); + + passThrough.on("drain", () => { + res.emit("drain"); + }); + + res.write = passThrough.write.bind(passThrough); + res.end = (passThrough as any).end.bind(passThrough); + + res.writeHead = function writeHead(statusCode: number, statusMessage?: string | any, headers?: any): ServerResponse { + res.statusCode = statusCode; + if (typeof statusMessage === "object") { + headers = statusMessage; + statusMessage = undefined; + } + if (headers) { + Object.entries(headers).forEach(([key, value]) => { + if (value !== undefined) { + res.setHeader(key, value); + } + }); + } + return res; + }; + + return { res, onReadable }; +} + +test("Readable.pipe(ServerResponse) flows without stalling (regression for #19111)", async () => { + const mockReq = createMockIncomingMessage("/pipe"); + const { res, onReadable } = createServerResponse(mockReq); + + // Pipe a readable source into ServerResponse; should not stall + const src = Readable.from(["Hello, ", "world!"]); + res.writeHead(200, { "Content-Type": "text/plain" }); + src.pipe(res); + + const out = await onReadable; + assert.strictEqual(out.statusCode, 200); + assert.strictEqual(out.headers["content-type"], "text/plain"); +}); From a0a69ee146b39b9ee80dffaaa9ad79cf87f7dff8 Mon Sep 17 00:00:00 2001 From: Felipe Cardozo Date: Mon, 27 Oct 2025 22:31:33 -0300 Subject: [PATCH 259/391] fix: body already used error to throw TypeError (#24114) Should fix https://github.com/oven-sh/bun/issues/24104 ### What does this PR do? This PR is changing `ERR_BODY_ALREADY_USED` to be TypeError instead of Error. ### How did you verify your code works? A test case added to verify that request call correctly throws a TypeError after another request call on the same Request, confirming the fix addresses the issue. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/bindings/ErrorCode.ts | 2 +- test/js/web/fetch/body-mixin-errors.test.ts | 25 +++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 31a3c28bac..e87e171e6e 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -20,7 +20,7 @@ const errors: ErrorCodeMapping = [ ["ERR_ASSERTION", Error], ["ERR_ASYNC_CALLBACK", TypeError], ["ERR_ASYNC_TYPE", TypeError], - ["ERR_BODY_ALREADY_USED", Error], + ["ERR_BODY_ALREADY_USED", TypeError], ["ERR_BORINGSSL", Error], ["ERR_ZSTD", Error], ["ERR_BROTLI_INVALID_PARAM", RangeError], diff --git a/test/js/web/fetch/body-mixin-errors.test.ts b/test/js/web/fetch/body-mixin-errors.test.ts index b7568e4dc4..5fce3d4a6c 100644 --- a/test/js/web/fetch/body-mixin-errors.test.ts +++ b/test/js/web/fetch/body-mixin-errors.test.ts @@ -1,17 +1,24 @@ import { describe, expect, it } from "bun:test"; describe("body-mixin-errors", () => { - it("should fail when bodyUsed", async () => { - var res = new Response("a"); - expect(res.bodyUsed).toBe(false); - await res.text(); - expect(res.bodyUsed).toBe(true); + it.concurrent.each([ + ["Response", () => new Response("a"), (b: Response | Request) => b.text()], + [ + "Request", + () => new Request("https://example.com", { body: "{}", method: "POST" }), + (b: Response | Request) => b.json(), + ], + ])("should throw TypeError when body already used on %s", async (type, createBody, secondCall) => { + const body = createBody(); + await body.text(); try { - await res.text(); - throw new Error("should not get here"); - } catch (e: any) { - expect(e.message).toBe("Body already used"); + await secondCall(body); + expect.unreachable("body is already used"); + } catch (err: any) { + expect(err.name).toBe("TypeError"); + expect(err.message).toBe("Body already used"); + expect(err instanceof TypeError).toBe(true); } }); }); From 523fc14d76454b8569542e54aa8c9793e22536e1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 27 Oct 2025 18:58:02 -0700 Subject: [PATCH 260/391] Deflake websocket test --- test/js/web/websocket/websocket.test.js | 118 ++++++++++++------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index e40a2d17ac..7caee1a279 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -512,65 +512,6 @@ describe.concurrent("WebSocket", () => { await Promise.all([promise, promise2]); }); - it("instances should be finalized when GC'd", async () => { - let current_websocket_count = 0; - let initial_websocket_count = 0; - function getWebSocketCount() { - Bun.gc(true); - const objectTypeCounts = require("bun:jsc").heapStats().objectTypeCounts || { - WebSocket: 0, - }; - return objectTypeCounts.WebSocket || 0; - } - - async function run() { - using server = Bun.serve({ - port: 0, - fetch(req, server) { - return server.upgrade(req); - }, - websocket: { - open() {}, - data() {}, - message() {}, - drain() {}, - }, - }); - - function onOpen(sock, resolve) { - sock.addEventListener("close", resolve, { once: true }); - sock.close(); - } - - function openAndCloseWS() { - const { promise, resolve } = Promise.withResolvers(); - const sock = new WebSocket(server.url.href.replace("http", "ws")); - sock.addEventListener("open", onOpen.bind(undefined, sock, resolve), { - once: true, - }); - - return promise; - } - - for (let i = 0; i < 1000; i++) { - await openAndCloseWS(); - if (i % 100 === 0) { - if (initial_websocket_count === 0) { - initial_websocket_count = getWebSocketCount(); - } - } - } - } - await run(); - - // wait next tick to run the last time - await Bun.sleep(100); - current_websocket_count = getWebSocketCount(); - console.log({ current_websocket_count, initial_websocket_count }); - // expect that current and initial websocket be close to the same (normaly 1 or 2 difference) - expect(Math.abs(current_websocket_count - initial_websocket_count)).toBeLessThanOrEqual(50); - }); - it("should be able to send big messages", async () => { using serve = Bun.serve({ port: 0, @@ -865,3 +806,62 @@ it.concurrent("#16995", async () => { socket.close(); } }); + +it.serial("instances should be finalized when GC'd", async () => { + let current_websocket_count = 0; + let initial_websocket_count = 0; + function getWebSocketCount() { + Bun.gc(true); + const objectTypeCounts = require("bun:jsc").heapStats().objectTypeCounts || { + WebSocket: 0, + }; + return objectTypeCounts.WebSocket || 0; + } + + async function run() { + using server = Bun.serve({ + port: 0, + fetch(req, server) { + return server.upgrade(req); + }, + websocket: { + open() {}, + data() {}, + message() {}, + drain() {}, + }, + }); + + function onOpen(sock, resolve) { + sock.addEventListener("close", resolve, { once: true }); + sock.close(); + } + + function openAndCloseWS() { + const { promise, resolve } = Promise.withResolvers(); + const sock = new WebSocket(server.url.href.replace("http", "ws")); + sock.addEventListener("open", onOpen.bind(undefined, sock, resolve), { + once: true, + }); + + return promise; + } + + for (let i = 0; i < 1000; i++) { + await openAndCloseWS(); + if (i % 100 === 0) { + if (initial_websocket_count === 0) { + initial_websocket_count = getWebSocketCount(); + } + } + } + } + await run(); + + // wait next tick to run the last time + await Bun.sleep(100); + current_websocket_count = getWebSocketCount(); + console.log({ current_websocket_count, initial_websocket_count }); + // expect that current and initial websocket be close to the same (normaly 1 or 2 difference) + expect(Math.abs(current_websocket_count - initial_websocket_count)).toBeLessThanOrEqual(50); +}); From eb77bdd28662fce35a40aa2b5f50589aa4d070a4 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 28 Oct 2025 00:05:16 -0700 Subject: [PATCH 261/391] Refactor: Split sourcemap.zig into separate struct files (#24141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR refactors the sourcemap module by extracting large structs from `src/sourcemap/sourcemap.zig` into their own dedicated files, improving code organization and maintainability. ## Changes - **Extracted `ParsedSourceMap` struct** to `src/sourcemap/ParsedSourceMap.zig` - Made `SourceContentPtr` and related methods public - Made `standaloneModuleGraphData` public for external access - **Extracted `Chunk` struct** to `src/sourcemap/Chunk.zig` - Added import for `appendMappingToBuffer` from parent module - Includes all nested types: `VLQSourceMap`, `NewBuilder`, `Builder` - **Extracted `Mapping` struct** to `src/sourcemap/Mapping.zig` - Added necessary imports: `assert`, `ParseResult`, `debug` - Includes nested types: `MappingWithoutName`, `List`, `Lookup` - **Updated `src/sourcemap/sourcemap.zig`** - Replaced struct definitions with imports: `@import("./StructName.zig")` - Maintained all public APIs All structs now follow the `const StructName = @This()` pattern for top-level declarations. ## Testing - ✅ Compiled successfully with `bun bd` - ✅ All existing functionality preserved - ✅ No API changes - fully backwards compatible ## Before - Single 2000+ line file with multiple large structs - Difficult to navigate and maintain ## After - Modular structure with separate files for each major struct - Easier to find and modify specific functionality - Better code organization 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/sourcemap/Chunk.zig | 373 ++++++++++ src/sourcemap/Mapping.zig | 599 ++++++++++++++++ src/sourcemap/ParsedSourceMap.zig | 166 +++++ src/sourcemap/sourcemap.zig | 1101 +---------------------------- 4 files changed, 1141 insertions(+), 1098 deletions(-) create mode 100644 src/sourcemap/Chunk.zig create mode 100644 src/sourcemap/Mapping.zig create mode 100644 src/sourcemap/ParsedSourceMap.zig diff --git a/src/sourcemap/Chunk.zig b/src/sourcemap/Chunk.zig new file mode 100644 index 0000000000..59f236c5b5 --- /dev/null +++ b/src/sourcemap/Chunk.zig @@ -0,0 +1,373 @@ +const Chunk = @This(); + +buffer: MutableString, + +mappings_count: usize = 0, + +/// This end state will be used to rewrite the start of the following source +/// map chunk so that the delta-encoded VLQ numbers are preserved. +end_state: SourceMapState = .{}, + +/// There probably isn't a source mapping at the end of the file (nor should +/// there be) but if we're appending another source map chunk after this one, +/// we'll need to know how many characters were in the last line we generated. +final_generated_column: i32 = 0, + +/// ignore empty chunks +should_ignore: bool = true, + +pub fn initEmpty() Chunk { + return .{ + .buffer = MutableString.initEmpty(bun.default_allocator), + .mappings_count = 0, + .end_state = .{}, + .final_generated_column = 0, + .should_ignore = true, + }; +} + +pub fn deinit(this: *Chunk) void { + this.buffer.deinit(); +} + +pub fn printSourceMapContents( + chunk: Chunk, + source: *const Logger.Source, + mutable: *MutableString, + include_sources_contents: bool, + comptime ascii_only: bool, +) !void { + try printSourceMapContentsAtOffset( + chunk, + source, + mutable, + include_sources_contents, + 0, + ascii_only, + ); +} + +pub fn printSourceMapContentsAtOffset( + chunk: Chunk, + source: *const Logger.Source, + mutable: *MutableString, + include_sources_contents: bool, + offset: usize, + comptime ascii_only: bool, +) !void { + // attempt to pre-allocate + + var filename_buf: bun.PathBuffer = undefined; + var filename = source.path.text; + if (strings.hasPrefix(source.path.text, FileSystem.instance.top_level_dir)) { + filename = filename[FileSystem.instance.top_level_dir.len - 1 ..]; + } else if (filename.len > 0 and filename[0] != '/') { + filename_buf[0] = '/'; + @memcpy(filename_buf[1..][0..filename.len], filename); + filename = filename_buf[0 .. filename.len + 1]; + } + + mutable.growIfNeeded( + filename.len + 2 + (source.contents.len * @as(usize, @intFromBool(include_sources_contents))) + (chunk.buffer.list.items.len - offset) + 32 + 39 + 29 + 22 + 20, + ) catch unreachable; + try mutable.append("{\n \"version\":3,\n \"sources\": ["); + + try JSPrinter.quoteForJSON(filename, mutable, ascii_only); + + if (include_sources_contents) { + try mutable.append("],\n \"sourcesContent\": ["); + try JSPrinter.quoteForJSON(source.contents, mutable, ascii_only); + } + + try mutable.append("],\n \"mappings\": "); + try JSPrinter.quoteForJSON(chunk.buffer.list.items[offset..], mutable, ascii_only); + try mutable.append(", \"names\": []\n}"); +} + +// TODO: remove the indirection by having generic functions for SourceMapFormat and NewBuilder. Source maps are always VLQ +pub fn SourceMapFormat(comptime Type: type) type { + return struct { + ctx: Type, + const Format = @This(); + + pub fn init(allocator: std.mem.Allocator, prepend_count: bool) Format { + return .{ .ctx = Type.init(allocator, prepend_count) }; + } + + pub inline fn appendLineSeparator(this: *Format) anyerror!void { + try this.ctx.appendLineSeparator(); + } + + pub inline fn append(this: *Format, current_state: SourceMapState, prev_state: SourceMapState) anyerror!void { + try this.ctx.append(current_state, prev_state); + } + + pub inline fn shouldIgnore(this: Format) bool { + return this.ctx.shouldIgnore(); + } + + pub inline fn getBuffer(this: Format) MutableString { + return this.ctx.getBuffer(); + } + + pub inline fn takeBuffer(this: *Format) MutableString { + return this.ctx.takeBuffer(); + } + + pub inline fn getCount(this: Format) usize { + return this.ctx.getCount(); + } + }; +} + +pub const VLQSourceMap = struct { + data: MutableString, + count: usize = 0, + offset: usize = 0, + approximate_input_line_count: usize = 0, + + pub fn init(allocator: std.mem.Allocator, prepend_count: bool) VLQSourceMap { + var map = VLQSourceMap{ + .data = MutableString.initEmpty(allocator), + }; + + // For bun.js, we store the number of mappings and how many bytes the final list is at the beginning of the array + if (prepend_count) { + map.offset = 24; + map.data.append(&([_]u8{0} ** 24)) catch unreachable; + } + + return map; + } + + pub fn appendLineSeparator(this: *VLQSourceMap) anyerror!void { + try this.data.appendChar(';'); + } + + pub fn append(this: *VLQSourceMap, current_state: SourceMapState, prev_state: SourceMapState) anyerror!void { + const last_byte: u8 = if (this.data.list.items.len > this.offset) + this.data.list.items[this.data.list.items.len - 1] + else + 0; + + appendMappingToBuffer(&this.data, last_byte, prev_state, current_state); + this.count += 1; + } + + pub fn shouldIgnore(this: VLQSourceMap) bool { + return this.count == 0; + } + + pub fn getBuffer(this: VLQSourceMap) MutableString { + return this.data; + } + + pub fn takeBuffer(this: *VLQSourceMap) MutableString { + defer this.data = .initEmpty(this.data.allocator); + return this.data; + } + + pub fn getCount(this: VLQSourceMap) usize { + return this.count; + } +}; + +pub fn NewBuilder(comptime SourceMapFormatType: type) type { + return struct { + const ThisBuilder = @This(); + source_map: SourceMapper, + line_offset_tables: LineOffsetTable.List = .{}, + prev_state: SourceMapState = SourceMapState{}, + last_generated_update: u32 = 0, + generated_column: i32 = 0, + prev_loc: Logger.Loc = Logger.Loc.Empty, + has_prev_state: bool = false, + + line_offset_table_byte_offset_list: []const u32 = &.{}, + + // This is a workaround for a bug in the popular "source-map" library: + // https://github.com/mozilla/source-map/issues/261. The library will + // sometimes return null when querying a source map unless every line + // starts with a mapping at column zero. + // + // The workaround is to replicate the previous mapping if a line ends + // up not starting with a mapping. This is done lazily because we want + // to avoid replicating the previous mapping if we don't need to. + line_starts_with_mapping: bool = false, + cover_lines_without_mappings: bool = false, + + approximate_input_line_count: usize = 0, + + /// When generating sourcemappings for bun, we store a count of how many mappings there were + prepend_count: bool = false, + + pub const SourceMapper = SourceMapFormat(SourceMapFormatType); + + pub noinline fn generateChunk(b: *ThisBuilder, output: []const u8) Chunk { + b.updateGeneratedLineAndColumn(output); + var buffer = b.source_map.getBuffer(); + if (b.prepend_count) { + buffer.list.items[0..8].* = @as([8]u8, @bitCast(buffer.list.items.len)); + buffer.list.items[8..16].* = @as([8]u8, @bitCast(b.source_map.getCount())); + buffer.list.items[16..24].* = @as([8]u8, @bitCast(b.approximate_input_line_count)); + } + return Chunk{ + .buffer = b.source_map.takeBuffer(), + .mappings_count = b.source_map.getCount(), + .end_state = b.prev_state, + .final_generated_column = b.generated_column, + .should_ignore = b.source_map.shouldIgnore(), + }; + } + + // Scan over the printed text since the last source mapping and update the + // generated line and column numbers + pub fn updateGeneratedLineAndColumn(b: *ThisBuilder, output: []const u8) void { + const slice = output[b.last_generated_update..]; + var needs_mapping = b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.has_prev_state; + + var i: usize = 0; + const n = @as(usize, @intCast(slice.len)); + var c: i32 = 0; + while (i < n) { + const len = strings.wtf8ByteSequenceLengthWithInvalid(slice[i]); + c = strings.decodeWTF8RuneT(slice[i..].ptr[0..4], len, i32, strings.unicode_replacement); + i += @as(usize, len); + + switch (c) { + 14...127 => { + if (strings.indexOfNewlineOrNonASCII(slice, @as(u32, @intCast(i)))) |j| { + b.generated_column += @as(i32, @intCast((@as(usize, j) - i) + 1)); + i = j; + continue; + } else { + b.generated_column += @as(i32, @intCast(slice[i..].len)) + 1; + i = n; + break; + } + }, + '\r', '\n', 0x2028, 0x2029 => { + // windows newline + if (c == '\r') { + const newline_check = b.last_generated_update + i + 1; + if (newline_check < output.len and output[newline_check] == '\n') { + continue; + } + } + + // If we're about to move to the next line and the previous line didn't have + // any mappings, add a mapping at the start of the previous line. + if (needs_mapping) { + b.appendMappingWithoutRemapping(.{ + .generated_line = b.prev_state.generated_line, + .generated_column = 0, + .source_index = b.prev_state.source_index, + .original_line = b.prev_state.original_line, + .original_column = b.prev_state.original_column, + }); + } + + b.prev_state.generated_line += 1; + b.prev_state.generated_column = 0; + b.generated_column = 0; + b.source_map.appendLineSeparator() catch unreachable; + + // This new line doesn't have a mapping yet + b.line_starts_with_mapping = false; + + needs_mapping = b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.has_prev_state; + }, + + else => { + // Mozilla's "source-map" library counts columns using UTF-16 code units + b.generated_column += @as(i32, @intFromBool(c > 0xFFFF)) + 1; + }, + } + } + + b.last_generated_update = @as(u32, @truncate(output.len)); + } + + pub fn appendMapping(b: *ThisBuilder, current_state: SourceMapState) void { + b.appendMappingWithoutRemapping(current_state); + } + + pub fn appendMappingWithoutRemapping(b: *ThisBuilder, current_state: SourceMapState) void { + b.source_map.append(current_state, b.prev_state) catch unreachable; + b.prev_state = current_state; + b.has_prev_state = true; + } + + pub fn addSourceMapping(b: *ThisBuilder, loc: Logger.Loc, output: []const u8) void { + if ( + // don't insert mappings for same location twice + b.prev_loc.eql(loc) or + // exclude generated code from source + loc.start == Logger.Loc.Empty.start) + return; + + b.prev_loc = loc; + const list = b.line_offset_tables; + + // We have no sourcemappings. + // This happens for example when importing an asset which does not support sourcemaps + // like a png or a jpg + // + // import foo from "./foo.png"; + // + if (list.len == 0) { + return; + } + + const original_line = LineOffsetTable.findLine(b.line_offset_table_byte_offset_list, loc); + const line = list.get(@as(usize, @intCast(@max(original_line, 0)))); + + // Use the line to compute the column + var original_column = loc.start - @as(i32, @intCast(line.byte_offset_to_start_of_line)); + if (line.columns_for_non_ascii.len > 0 and original_column >= @as(i32, @intCast(line.byte_offset_to_first_non_ascii))) { + original_column = line.columns_for_non_ascii.slice()[@as(u32, @intCast(original_column)) - line.byte_offset_to_first_non_ascii]; + } + + b.updateGeneratedLineAndColumn(output); + + // If this line doesn't start with a mapping and we're about to add a mapping + // that's not at the start, insert a mapping first so the line starts with one. + if (b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.generated_column > 0 and b.has_prev_state) { + b.appendMappingWithoutRemapping(.{ + .generated_line = b.prev_state.generated_line, + .generated_column = 0, + .source_index = b.prev_state.source_index, + .original_line = b.prev_state.original_line, + .original_column = b.prev_state.original_column, + }); + } + + b.appendMapping(.{ + .generated_line = b.prev_state.generated_line, + .generated_column = @max(b.generated_column, 0), + .source_index = b.prev_state.source_index, + .original_line = @max(original_line, 0), + .original_column = @max(original_column, 0), + }); + + // This line now has a mapping on it, so don't insert another one + b.line_starts_with_mapping = true; + } + }; +} + +pub const Builder = NewBuilder(VLQSourceMap); + +const std = @import("std"); + +const SourceMap = @import("./sourcemap.zig"); +const LineOffsetTable = SourceMap.LineOffsetTable; +const SourceMapState = SourceMap.SourceMapState; +const appendMappingToBuffer = SourceMap.appendMappingToBuffer; + +const bun = @import("bun"); +const JSPrinter = bun.js_printer; +const Logger = bun.logger; +const MutableString = bun.MutableString; +const strings = bun.strings; +const FileSystem = bun.fs.FileSystem; diff --git a/src/sourcemap/Mapping.zig b/src/sourcemap/Mapping.zig new file mode 100644 index 0000000000..bbd8f0ede6 --- /dev/null +++ b/src/sourcemap/Mapping.zig @@ -0,0 +1,599 @@ +const Mapping = @This(); + +const debug = bun.Output.scoped(.SourceMap, .visible); + +generated: LineColumnOffset, +original: LineColumnOffset, +source_index: i32, +name_index: i32 = -1, + +/// Optimization: if we don't care about the "names" column, then don't store the names. +pub const MappingWithoutName = struct { + generated: LineColumnOffset, + original: LineColumnOffset, + source_index: i32, + + pub fn toNamed(this: *const MappingWithoutName) Mapping { + return .{ + .generated = this.generated, + .original = this.original, + .source_index = this.source_index, + .name_index = -1, + }; + } +}; + +pub const List = struct { + impl: Value = .{ .without_names = .{} }, + names: []const bun.Semver.String = &[_]bun.Semver.String{}, + names_buffer: bun.ByteList = .{}, + + pub const Value = union(enum) { + without_names: bun.MultiArrayList(MappingWithoutName), + with_names: bun.MultiArrayList(Mapping), + + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .without_names => |*list| list.memoryCost(), + .with_names => |*list| list.memoryCost(), + }; + } + + pub fn ensureTotalCapacity(this: *Value, allocator: std.mem.Allocator, count: usize) !void { + switch (this.*) { + inline else => |*list| try list.ensureTotalCapacity(allocator, count), + } + } + }; + + fn ensureWithNames(this: *List, allocator: std.mem.Allocator) !void { + if (this.impl == .with_names) return; + + var without_names = this.impl.without_names; + var with_names = bun.MultiArrayList(Mapping){}; + try with_names.ensureTotalCapacity(allocator, without_names.len); + defer without_names.deinit(allocator); + + with_names.len = without_names.len; + var old_slices = without_names.slice(); + var new_slices = with_names.slice(); + + @memcpy(new_slices.items(.generated), old_slices.items(.generated)); + @memcpy(new_slices.items(.original), old_slices.items(.original)); + @memcpy(new_slices.items(.source_index), old_slices.items(.source_index)); + @memset(new_slices.items(.name_index), -1); + + this.impl = .{ .with_names = with_names }; + } + + fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: bun.Ordinal, column: bun.Ordinal) ?usize { + var count = line_column_offsets.len; + var index: usize = 0; + while (count > 0) { + const step = count / 2; + const i: usize = index + step; + const mapping = line_column_offsets[i]; + if (mapping.lines.zeroBased() < line.zeroBased() or (mapping.lines.zeroBased() == line.zeroBased() and mapping.columns.zeroBased() <= column.zeroBased())) { + index = i + 1; + count -|= step + 1; + } else { + count = step; + } + } + + if (index > 0) { + if (line_column_offsets[index - 1].lines.zeroBased() == line.zeroBased()) { + return index - 1; + } + } + + return null; + } + + pub fn findIndex(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?usize { + switch (this.impl) { + inline else => |*list| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + return i; + } + }, + } + + return null; + } + + const SortContext = struct { + generated: []const LineColumnOffset, + pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool { + const a = ctx.generated[a_index]; + const b = ctx.generated[b_index]; + + return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased()); + } + }; + + pub fn sort(this: *List) void { + switch (this.impl) { + .without_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + .with_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + } + } + + pub fn append(this: *List, allocator: std.mem.Allocator, mapping: *const Mapping) !void { + switch (this.impl) { + .without_names => |*list| { + try list.append(allocator, .{ + .generated = mapping.generated, + .original = mapping.original, + .source_index = mapping.source_index, + }); + }, + .with_names => |*list| { + try list.append(allocator, mapping.*); + }, + } + } + + pub fn find(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?Mapping { + switch (this.impl) { + inline else => |*list, tag| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + if (tag == .without_names) { + return list.get(i).toNamed(); + } else { + return list.get(i); + } + } + }, + } + + return null; + } + pub fn generated(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.generated), + }; + } + + pub fn original(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.original), + }; + } + + pub fn sourceIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.source_index), + }; + } + + pub fn nameIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.name_index), + }; + } + + pub fn deinit(self: *List, allocator: std.mem.Allocator) void { + switch (self.impl) { + inline else => |*list| list.deinit(allocator), + } + + self.names_buffer.deinit(allocator); + allocator.free(self.names); + } + + pub fn getName(this: *List, index: i32) ?[]const u8 { + if (index < 0) return null; + const i: usize = @intCast(index); + + if (i >= this.names.len) return null; + + if (this.impl == .with_names) { + const str: *const bun.Semver.String = &this.names[i]; + return str.slice(this.names_buffer.slice()); + } + + return null; + } + + pub fn memoryCost(this: *const List) usize { + return this.impl.memoryCost() + this.names_buffer.memoryCost() + + (this.names.len * @sizeOf(bun.Semver.String)); + } + + pub fn ensureTotalCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void { + try this.impl.ensureTotalCapacity(allocator, count); + } +}; + +pub const Lookup = struct { + mapping: Mapping, + source_map: ?*ParsedSourceMap = null, + /// Owned by default_allocator always + /// use `getSourceCode` to access this as a Slice + prefetched_source_code: ?[]const u8, + + name: ?[]const u8 = null, + + /// This creates a bun.String if the source remap *changes* the source url, + /// which is only possible if the executed file differs from the source file: + /// + /// - `bun build --sourcemap`, it is another file on disk + /// - `bun build --compile --sourcemap`, it is an embedded file. + pub fn displaySourceURLIfNeeded(lookup: Lookup, base_filename: []const u8) ?bun.String { + const source_map = lookup.source_map orelse return null; + // See doc comment on `external_source_names` + if (source_map.external_source_names.len == 0) + return null; + if (lookup.mapping.source_index >= source_map.external_source_names.len) + return null; + + const name = source_map.external_source_names[@intCast(lookup.mapping.source_index)]; + + if (source_map.is_standalone_module_graph) { + return bun.String.cloneUTF8(name); + } + + if (std.fs.path.isAbsolute(base_filename)) { + const dir = bun.path.dirname(base_filename, .auto); + return bun.String.cloneUTF8(bun.path.joinAbs(dir, .auto, name)); + } + + return bun.String.init(name); + } + + /// Only valid if `lookup.source_map.isExternal()` + /// This has the possibility of invoking a call to the filesystem. + /// + /// This data is freed after printed on the assumption that printing + /// errors to the console are rare (this isnt used for error.stack) + pub fn getSourceCode(lookup: Lookup, base_filename: []const u8) ?bun.jsc.ZigString.Slice { + const bytes = bytes: { + if (lookup.prefetched_source_code) |code| { + break :bytes code; + } + + const source_map = lookup.source_map orelse return null; + assert(source_map.isExternal()); + + const provider = source_map.underlying_provider.provider() orelse + return null; + + const index = lookup.mapping.source_index; + + // Standalone module graph source maps are stored (in memory) compressed. + // They are decompressed on demand. + if (source_map.is_standalone_module_graph) { + const serialized = source_map.standaloneModuleGraphData(); + if (index >= source_map.external_source_names.len) + return null; + + const code = serialized.sourceFileContents(@intCast(index)); + + return bun.jsc.ZigString.Slice.fromUTF8NeverFree(code orelse return null); + } + + if (provider.getSourceMap( + base_filename, + source_map.underlying_provider.load_hint, + .{ .source_only = @intCast(index) }, + )) |parsed| + if (parsed.source_contents) |contents| + break :bytes contents; + + if (index >= source_map.external_source_names.len) + return null; + + const name = source_map.external_source_names[@intCast(index)]; + + var buf: bun.PathBuffer = undefined; + const normalized = bun.path.joinAbsStringBufZ( + bun.path.dirname(base_filename, .auto), + &buf, + &.{name}, + .loose, + ); + switch (bun.sys.File.readFrom( + std.fs.cwd(), + normalized, + bun.default_allocator, + )) { + .result => |r| break :bytes r, + .err => return null, + } + }; + + return bun.jsc.ZigString.Slice.init(bun.default_allocator, bytes); + } +}; + +pub inline fn generatedLine(mapping: *const Mapping) i32 { + return mapping.generated.lines.zeroBased(); +} + +pub inline fn generatedColumn(mapping: *const Mapping) i32 { + return mapping.generated.columns.zeroBased(); +} + +pub inline fn sourceIndex(mapping: *const Mapping) i32 { + return mapping.source_index; +} + +pub inline fn originalLine(mapping: *const Mapping) i32 { + return mapping.original.lines.zeroBased(); +} + +pub inline fn originalColumn(mapping: *const Mapping) i32 { + return mapping.original.columns.zeroBased(); +} + +pub inline fn nameIndex(mapping: *const Mapping) i32 { + return mapping.name_index; +} + +pub fn parse( + allocator: std.mem.Allocator, + bytes: []const u8, + estimated_mapping_count: ?usize, + sources_count: i32, + input_line_count: usize, + options: struct { + allow_names: bool = false, + sort: bool = false, + }, +) ParseResult { + debug("parse mappings ({d} bytes)", .{bytes.len}); + + var mapping = Mapping.List{}; + errdefer mapping.deinit(allocator); + + if (estimated_mapping_count) |count| { + mapping.ensureTotalCapacity(allocator, count) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{}, + }, + }; + }; + } + + var generated = LineColumnOffset{ .lines = bun.Ordinal.start, .columns = bun.Ordinal.start }; + var original = LineColumnOffset{ .lines = bun.Ordinal.start, .columns = bun.Ordinal.start }; + var name_index: i32 = 0; + var source_index: i32 = 0; + var needs_sort = false; + var remain = bytes; + var has_names = false; + while (remain.len > 0) { + if (remain[0] == ';') { + generated.columns = bun.Ordinal.start; + + while (strings.hasPrefixComptime( + remain, + comptime [_]u8{';'} ** (@sizeOf(usize) / 2), + )) { + generated.lines = generated.lines.addScalar(@sizeOf(usize) / 2); + remain = remain[@sizeOf(usize) / 2 ..]; + } + + while (remain.len > 0 and remain[0] == ';') { + generated.lines = generated.lines.addScalar(1); + remain = remain[1..]; + } + + if (remain.len == 0) { + break; + } + } + + // Read the generated column + const generated_column_delta = decodeVLQ(remain, 0); + + if (generated_column_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Missing generated column value", + .err = error.MissingGeneratedColumnValue, + .value = generated.columns.zeroBased(), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + + needs_sort = needs_sort or generated_column_delta.value < 0; + + generated.columns = generated.columns.addScalar(generated_column_delta.value); + if (generated.columns.zeroBased() < 0) { + return .{ + .fail = .{ + .msg = "Invalid generated column value", + .err = error.InvalidGeneratedColumnValue, + .value = generated.columns.zeroBased(), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + + remain = remain[generated_column_delta.start..]; + + // According to the specification, it's valid for a mapping to have 1, + // 4, or 5 variable-length fields. Having one field means there's no + // original location information, which is pretty useless. Just ignore + // those entries. + if (remain.len == 0) + break; + + switch (remain[0]) { + ',' => { + remain = remain[1..]; + continue; + }, + ';' => { + continue; + }, + else => {}, + } + + // Read the original source + const source_index_delta = decodeVLQ(remain, 0); + if (source_index_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Invalid source index delta", + .err = error.InvalidSourceIndexDelta, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + source_index += source_index_delta.value; + + if (source_index < 0 or source_index > sources_count) { + return .{ + .fail = .{ + .msg = "Invalid source index value", + .err = error.InvalidSourceIndexValue, + .value = source_index, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[source_index_delta.start..]; + + // Read the original line + const original_line_delta = decodeVLQ(remain, 0); + if (original_line_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Missing original line", + .err = error.MissingOriginalLine, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + + original.lines = original.lines.addScalar(original_line_delta.value); + if (original.lines.zeroBased() < 0) { + return .{ + .fail = .{ + .msg = "Invalid original line value", + .err = error.InvalidOriginalLineValue, + .value = original.lines.zeroBased(), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[original_line_delta.start..]; + + // Read the original column + const original_column_delta = decodeVLQ(remain, 0); + if (original_column_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Missing original column value", + .err = error.MissingOriginalColumnValue, + .value = original.columns.zeroBased(), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + + original.columns = original.columns.addScalar(original_column_delta.value); + if (original.columns.zeroBased() < 0) { + return .{ + .fail = .{ + .msg = "Invalid original column value", + .err = error.InvalidOriginalColumnValue, + .value = original.columns.zeroBased(), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[original_column_delta.start..]; + + if (remain.len > 0) { + switch (remain[0]) { + ',' => { + // 4 column, but there's more on this line. + remain = remain[1..]; + }, + // 4 column, and there's no more on this line. + ';' => {}, + + // 5th column: the name + else => |c| { + // Read the name index + const name_index_delta = decodeVLQ(remain, 0); + if (name_index_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Invalid name index delta", + .err = error.InvalidNameIndexDelta, + .value = @intCast(c), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[name_index_delta.start..]; + + if (options.allow_names) { + name_index += name_index_delta.value; + if (!has_names) { + mapping.ensureWithNames(allocator) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + }; + } + has_names = true; + } + + if (remain.len > 0) { + switch (remain[0]) { + // There's more on this line. + ',' => { + remain = remain[1..]; + }, + // That's the end of the line. + ';' => {}, + else => {}, + } + } + }, + } + } + mapping.append(allocator, &.{ + .generated = generated, + .original = original, + .source_index = source_index, + .name_index = name_index, + }) catch |err| bun.handleOom(err); + } + + if (needs_sort and options.sort) { + mapping.sort(); + } + + return .{ .success = .{ + .ref_count = .init(), + .mappings = mapping, + .input_line_count = input_line_count, + } }; +} + +const std = @import("std"); + +const SourceMap = @import("./sourcemap.zig"); +const LineColumnOffset = SourceMap.LineColumnOffset; +const ParseResult = SourceMap.ParseResult; +const ParsedSourceMap = SourceMap.ParsedSourceMap; +const decodeVLQ = SourceMap.VLQ.decode; + +const bun = @import("bun"); +const assert = bun.assert; +const strings = bun.strings; diff --git a/src/sourcemap/ParsedSourceMap.zig b/src/sourcemap/ParsedSourceMap.zig new file mode 100644 index 0000000000..b774d00f03 --- /dev/null +++ b/src/sourcemap/ParsedSourceMap.zig @@ -0,0 +1,166 @@ +const ParsedSourceMap = @This(); + +const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{}); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +/// ParsedSourceMap can be acquired by different threads via the thread-safe +/// source map store (SavedSourceMap), so the reference count must be thread-safe. +ref_count: RefCount, + +input_line_count: usize = 0, +mappings: Mapping.List = .{}, + +/// If this is empty, this implies that the source code is a single file +/// transpiled on-demand. If there are items, then it means this is a file +/// loaded without transpilation but with external sources. This array +/// maps `source_index` to the correct filename. +external_source_names: []const []const u8 = &.{}, +/// In order to load source contents from a source-map after the fact, +/// a handle to the underlying source provider is stored. Within this pointer, +/// a flag is stored if it is known to be an inline or external source map. +/// +/// Source contents are large, we don't preserve them in memory. This has +/// the downside of repeatedly re-decoding sourcemaps if multiple errors +/// are emitted (specifically with Bun.inspect / unhandled; the ones that +/// rely on source contents) +underlying_provider: SourceContentPtr = .none, + +is_standalone_module_graph: bool = false, + +const SourceProviderKind = enum(u2) { zig, bake, dev_server }; +const AnySourceProvider = union(enum) { + zig: *SourceProviderMap, + bake: *BakeSourceProvider, + dev_server: *DevServerSourceProvider, + + pub fn ptr(this: AnySourceProvider) *anyopaque { + return switch (this) { + .zig => @ptrCast(this.zig), + .bake => @ptrCast(this.bake), + .dev_server => @ptrCast(this.dev_server), + }; + } + + pub fn getSourceMap( + this: AnySourceProvider, + source_filename: []const u8, + load_hint: SourceMapLoadHint, + result: ParseUrlResultHint, + ) ?SourceMap.ParseUrl { + return switch (this) { + .zig => this.zig.getSourceMap(source_filename, load_hint, result), + .bake => this.bake.getSourceMap(source_filename, load_hint, result), + .dev_server => this.dev_server.getSourceMap(source_filename, load_hint, result), + }; + } +}; + +pub const SourceContentPtr = packed struct(u64) { + load_hint: SourceMapLoadHint, + kind: SourceProviderKind, + data: u60, + + pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 }; + + pub fn fromProvider(p: *SourceProviderMap) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .zig }; + } + + pub fn fromBakeProvider(p: *BakeSourceProvider) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake }; + } + + pub fn fromDevServerProvider(p: *DevServerSourceProvider) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .dev_server }; + } + + pub fn provider(sc: SourceContentPtr) ?AnySourceProvider { + switch (sc.kind) { + .zig => return .{ .zig = @ptrFromInt(sc.data) }, + .bake => return .{ .bake = @ptrFromInt(sc.data) }, + .dev_server => return .{ .dev_server = @ptrFromInt(sc.data) }, + } + } +}; + +pub fn isExternal(psm: *ParsedSourceMap) bool { + return psm.external_source_names.len != 0; +} + +fn deinit(this: *ParsedSourceMap) void { + const allocator = bun.default_allocator; + + this.mappings.deinit(allocator); + + if (this.external_source_names.len > 0) { + for (this.external_source_names) |name| + allocator.free(name); + allocator.free(this.external_source_names); + } + + bun.destroy(this); +} + +pub fn standaloneModuleGraphData(this: *ParsedSourceMap) *bun.StandaloneModuleGraph.SerializedSourceMap.Loaded { + bun.assert(this.is_standalone_module_graph); + return @ptrFromInt(this.underlying_provider.data); +} + +pub fn memoryCost(this: *const ParsedSourceMap) usize { + return @sizeOf(ParsedSourceMap) + this.mappings.memoryCost() + this.external_source_names.len * @sizeOf([]const u8); +} + +pub fn writeVLQs(map: *const ParsedSourceMap, writer: anytype) !void { + var last_col: i32 = 0; + var last_src: i32 = 0; + var last_ol: i32 = 0; + var last_oc: i32 = 0; + var current_line: i32 = 0; + for ( + map.mappings.generated(), + map.mappings.original(), + map.mappings.sourceIndex(), + 0.., + ) |gen, orig, source_index, i| { + if (current_line != gen.lines.zeroBased()) { + assert(gen.lines.zeroBased() > current_line); + const inc = gen.lines.zeroBased() - current_line; + try writer.writeByteNTimes(';', @intCast(inc)); + current_line = gen.lines.zeroBased(); + last_col = 0; + } else if (i != 0) { + try writer.writeByte(','); + } + try VLQ.encode(gen.columns.zeroBased() - last_col).writeTo(writer); + last_col = gen.columns.zeroBased(); + try VLQ.encode(source_index - last_src).writeTo(writer); + last_src = source_index; + try VLQ.encode(orig.lines.zeroBased() - last_ol).writeTo(writer); + last_ol = orig.lines.zeroBased(); + try VLQ.encode(orig.columns.zeroBased() - last_oc).writeTo(writer); + last_oc = orig.columns.zeroBased(); + } +} + +pub fn formatVLQs(map: *const ParsedSourceMap) std.fmt.Formatter(formatVLQsImpl) { + return .{ .data = map }; +} + +fn formatVLQsImpl(map: *const ParsedSourceMap, comptime _: []const u8, _: std.fmt.FormatOptions, w: anytype) !void { + try map.writeVLQs(w); +} + +const std = @import("std"); + +const SourceMap = @import("./sourcemap.zig"); +const BakeSourceProvider = SourceMap.BakeSourceProvider; +const DevServerSourceProvider = SourceMap.DevServerSourceProvider; +const Mapping = SourceMap.Mapping; +const ParseUrlResultHint = SourceMap.ParseUrlResultHint; +const SourceMapLoadHint = SourceMap.SourceMapLoadHint; +const SourceProviderMap = SourceMap.SourceProviderMap; +const VLQ = SourceMap.VLQ; + +const bun = @import("bun"); +const assert = bun.assert; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 5e9f6ff1f2..f452d59d07 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -250,591 +250,7 @@ pub fn parseJSON( } /// Corresponds to a segment in the "mappings" field of a sourcemap -pub const Mapping = struct { - generated: LineColumnOffset, - original: LineColumnOffset, - source_index: i32, - name_index: i32 = -1, - - /// Optimization: if we don't care about the "names" column, then don't store the names. - pub const MappingWithoutName = struct { - generated: LineColumnOffset, - original: LineColumnOffset, - source_index: i32, - - pub fn toNamed(this: *const MappingWithoutName) Mapping { - return .{ - .generated = this.generated, - .original = this.original, - .source_index = this.source_index, - .name_index = -1, - }; - } - }; - - pub const List = struct { - impl: Value = .{ .without_names = .{} }, - names: []const bun.Semver.String = &[_]bun.Semver.String{}, - names_buffer: bun.ByteList = .{}, - - pub const Value = union(enum) { - without_names: bun.MultiArrayList(MappingWithoutName), - with_names: bun.MultiArrayList(Mapping), - - pub fn memoryCost(this: *const Value) usize { - return switch (this.*) { - .without_names => |*list| list.memoryCost(), - .with_names => |*list| list.memoryCost(), - }; - } - - pub fn ensureTotalCapacity(this: *Value, allocator: std.mem.Allocator, count: usize) !void { - switch (this.*) { - inline else => |*list| try list.ensureTotalCapacity(allocator, count), - } - } - }; - - fn ensureWithNames(this: *List, allocator: std.mem.Allocator) !void { - if (this.impl == .with_names) return; - - var without_names = this.impl.without_names; - var with_names = bun.MultiArrayList(Mapping){}; - try with_names.ensureTotalCapacity(allocator, without_names.len); - defer without_names.deinit(allocator); - - with_names.len = without_names.len; - var old_slices = without_names.slice(); - var new_slices = with_names.slice(); - - @memcpy(new_slices.items(.generated), old_slices.items(.generated)); - @memcpy(new_slices.items(.original), old_slices.items(.original)); - @memcpy(new_slices.items(.source_index), old_slices.items(.source_index)); - @memset(new_slices.items(.name_index), -1); - - this.impl = .{ .with_names = with_names }; - } - - fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: bun.Ordinal, column: bun.Ordinal) ?usize { - var count = line_column_offsets.len; - var index: usize = 0; - while (count > 0) { - const step = count / 2; - const i: usize = index + step; - const mapping = line_column_offsets[i]; - if (mapping.lines.zeroBased() < line.zeroBased() or (mapping.lines.zeroBased() == line.zeroBased() and mapping.columns.zeroBased() <= column.zeroBased())) { - index = i + 1; - count -|= step + 1; - } else { - count = step; - } - } - - if (index > 0) { - if (line_column_offsets[index - 1].lines.zeroBased() == line.zeroBased()) { - return index - 1; - } - } - - return null; - } - - pub fn findIndex(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?usize { - switch (this.impl) { - inline else => |*list| { - if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { - return i; - } - }, - } - - return null; - } - - const SortContext = struct { - generated: []const LineColumnOffset, - pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool { - const a = ctx.generated[a_index]; - const b = ctx.generated[b_index]; - - return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased()); - } - }; - - pub fn sort(this: *List) void { - switch (this.impl) { - .without_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), - .with_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), - } - } - - pub fn append(this: *List, allocator: std.mem.Allocator, mapping: *const Mapping) !void { - switch (this.impl) { - .without_names => |*list| { - try list.append(allocator, .{ - .generated = mapping.generated, - .original = mapping.original, - .source_index = mapping.source_index, - }); - }, - .with_names => |*list| { - try list.append(allocator, mapping.*); - }, - } - } - - pub fn find(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?Mapping { - switch (this.impl) { - inline else => |*list, tag| { - if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { - if (tag == .without_names) { - return list.get(i).toNamed(); - } else { - return list.get(i); - } - } - }, - } - - return null; - } - pub fn generated(self: *const List) []const LineColumnOffset { - return switch (self.impl) { - inline else => |*list| list.items(.generated), - }; - } - - pub fn original(self: *const List) []const LineColumnOffset { - return switch (self.impl) { - inline else => |*list| list.items(.original), - }; - } - - pub fn sourceIndex(self: *const List) []const i32 { - return switch (self.impl) { - inline else => |*list| list.items(.source_index), - }; - } - - pub fn nameIndex(self: *const List) []const i32 { - return switch (self.impl) { - inline else => |*list| list.items(.name_index), - }; - } - - pub fn deinit(self: *List, allocator: std.mem.Allocator) void { - switch (self.impl) { - inline else => |*list| list.deinit(allocator), - } - - self.names_buffer.deinit(allocator); - allocator.free(self.names); - } - - pub fn getName(this: *List, index: i32) ?[]const u8 { - if (index < 0) return null; - const i: usize = @intCast(index); - - if (i >= this.names.len) return null; - - if (this.impl == .with_names) { - const str: *const bun.Semver.String = &this.names[i]; - return str.slice(this.names_buffer.slice()); - } - - return null; - } - - pub fn memoryCost(this: *const List) usize { - return this.impl.memoryCost() + this.names_buffer.memoryCost() + - (this.names.len * @sizeOf(bun.Semver.String)); - } - - pub fn ensureTotalCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void { - try this.impl.ensureTotalCapacity(allocator, count); - } - }; - - pub const Lookup = struct { - mapping: Mapping, - source_map: ?*ParsedSourceMap = null, - /// Owned by default_allocator always - /// use `getSourceCode` to access this as a Slice - prefetched_source_code: ?[]const u8, - - name: ?[]const u8 = null, - - /// This creates a bun.String if the source remap *changes* the source url, - /// which is only possible if the executed file differs from the source file: - /// - /// - `bun build --sourcemap`, it is another file on disk - /// - `bun build --compile --sourcemap`, it is an embedded file. - pub fn displaySourceURLIfNeeded(lookup: Lookup, base_filename: []const u8) ?bun.String { - const source_map = lookup.source_map orelse return null; - // See doc comment on `external_source_names` - if (source_map.external_source_names.len == 0) - return null; - if (lookup.mapping.source_index >= source_map.external_source_names.len) - return null; - - const name = source_map.external_source_names[@intCast(lookup.mapping.source_index)]; - - if (source_map.is_standalone_module_graph) { - return bun.String.cloneUTF8(name); - } - - if (std.fs.path.isAbsolute(base_filename)) { - const dir = bun.path.dirname(base_filename, .auto); - return bun.String.cloneUTF8(bun.path.joinAbs(dir, .auto, name)); - } - - return bun.String.init(name); - } - - /// Only valid if `lookup.source_map.isExternal()` - /// This has the possibility of invoking a call to the filesystem. - /// - /// This data is freed after printed on the assumption that printing - /// errors to the console are rare (this isnt used for error.stack) - pub fn getSourceCode(lookup: Lookup, base_filename: []const u8) ?bun.jsc.ZigString.Slice { - const bytes = bytes: { - if (lookup.prefetched_source_code) |code| { - break :bytes code; - } - - const source_map = lookup.source_map orelse return null; - assert(source_map.isExternal()); - - const provider = source_map.underlying_provider.provider() orelse - return null; - - const index = lookup.mapping.source_index; - - // Standalone module graph source maps are stored (in memory) compressed. - // They are decompressed on demand. - if (source_map.is_standalone_module_graph) { - const serialized = source_map.standaloneModuleGraphData(); - if (index >= source_map.external_source_names.len) - return null; - - const code = serialized.sourceFileContents(@intCast(index)); - - return bun.jsc.ZigString.Slice.fromUTF8NeverFree(code orelse return null); - } - - if (provider.getSourceMap( - base_filename, - source_map.underlying_provider.load_hint, - .{ .source_only = @intCast(index) }, - )) |parsed| - if (parsed.source_contents) |contents| - break :bytes contents; - - if (index >= source_map.external_source_names.len) - return null; - - const name = source_map.external_source_names[@intCast(index)]; - - var buf: bun.PathBuffer = undefined; - const normalized = bun.path.joinAbsStringBufZ( - bun.path.dirname(base_filename, .auto), - &buf, - &.{name}, - .loose, - ); - switch (bun.sys.File.readFrom( - std.fs.cwd(), - normalized, - bun.default_allocator, - )) { - .result => |r| break :bytes r, - .err => return null, - } - }; - - return bun.jsc.ZigString.Slice.init(bun.default_allocator, bytes); - } - }; - - pub inline fn generatedLine(mapping: *const Mapping) i32 { - return mapping.generated.lines.zeroBased(); - } - - pub inline fn generatedColumn(mapping: *const Mapping) i32 { - return mapping.generated.columns.zeroBased(); - } - - pub inline fn sourceIndex(mapping: *const Mapping) i32 { - return mapping.source_index; - } - - pub inline fn originalLine(mapping: *const Mapping) i32 { - return mapping.original.lines.zeroBased(); - } - - pub inline fn originalColumn(mapping: *const Mapping) i32 { - return mapping.original.columns.zeroBased(); - } - - pub inline fn nameIndex(mapping: *const Mapping) i32 { - return mapping.name_index; - } - - pub fn parse( - allocator: std.mem.Allocator, - bytes: []const u8, - estimated_mapping_count: ?usize, - sources_count: i32, - input_line_count: usize, - options: struct { - allow_names: bool = false, - sort: bool = false, - }, - ) ParseResult { - debug("parse mappings ({d} bytes)", .{bytes.len}); - - var mapping = Mapping.List{}; - errdefer mapping.deinit(allocator); - - if (estimated_mapping_count) |count| { - mapping.ensureTotalCapacity(allocator, count) catch { - return .{ - .fail = .{ - .msg = "Out of memory", - .err = error.OutOfMemory, - .loc = .{}, - }, - }; - }; - } - - var generated = LineColumnOffset{ .lines = bun.Ordinal.start, .columns = bun.Ordinal.start }; - var original = LineColumnOffset{ .lines = bun.Ordinal.start, .columns = bun.Ordinal.start }; - var name_index: i32 = 0; - var source_index: i32 = 0; - var needs_sort = false; - var remain = bytes; - var has_names = false; - while (remain.len > 0) { - if (remain[0] == ';') { - generated.columns = bun.Ordinal.start; - - while (strings.hasPrefixComptime( - remain, - comptime [_]u8{';'} ** (@sizeOf(usize) / 2), - )) { - generated.lines = generated.lines.addScalar(@sizeOf(usize) / 2); - remain = remain[@sizeOf(usize) / 2 ..]; - } - - while (remain.len > 0 and remain[0] == ';') { - generated.lines = generated.lines.addScalar(1); - remain = remain[1..]; - } - - if (remain.len == 0) { - break; - } - } - - // Read the generated column - const generated_column_delta = decodeVLQ(remain, 0); - - if (generated_column_delta.start == 0) { - return .{ - .fail = .{ - .msg = "Missing generated column value", - .err = error.MissingGeneratedColumnValue, - .value = generated.columns.zeroBased(), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - - needs_sort = needs_sort or generated_column_delta.value < 0; - - generated.columns = generated.columns.addScalar(generated_column_delta.value); - if (generated.columns.zeroBased() < 0) { - return .{ - .fail = .{ - .msg = "Invalid generated column value", - .err = error.InvalidGeneratedColumnValue, - .value = generated.columns.zeroBased(), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - - remain = remain[generated_column_delta.start..]; - - // According to the specification, it's valid for a mapping to have 1, - // 4, or 5 variable-length fields. Having one field means there's no - // original location information, which is pretty useless. Just ignore - // those entries. - if (remain.len == 0) - break; - - switch (remain[0]) { - ',' => { - remain = remain[1..]; - continue; - }, - ';' => { - continue; - }, - else => {}, - } - - // Read the original source - const source_index_delta = decodeVLQ(remain, 0); - if (source_index_delta.start == 0) { - return .{ - .fail = .{ - .msg = "Invalid source index delta", - .err = error.InvalidSourceIndexDelta, - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - source_index += source_index_delta.value; - - if (source_index < 0 or source_index > sources_count) { - return .{ - .fail = .{ - .msg = "Invalid source index value", - .err = error.InvalidSourceIndexValue, - .value = source_index, - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - remain = remain[source_index_delta.start..]; - - // Read the original line - const original_line_delta = decodeVLQ(remain, 0); - if (original_line_delta.start == 0) { - return .{ - .fail = .{ - .msg = "Missing original line", - .err = error.MissingOriginalLine, - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - - original.lines = original.lines.addScalar(original_line_delta.value); - if (original.lines.zeroBased() < 0) { - return .{ - .fail = .{ - .msg = "Invalid original line value", - .err = error.InvalidOriginalLineValue, - .value = original.lines.zeroBased(), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - remain = remain[original_line_delta.start..]; - - // Read the original column - const original_column_delta = decodeVLQ(remain, 0); - if (original_column_delta.start == 0) { - return .{ - .fail = .{ - .msg = "Missing original column value", - .err = error.MissingOriginalColumnValue, - .value = original.columns.zeroBased(), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - - original.columns = original.columns.addScalar(original_column_delta.value); - if (original.columns.zeroBased() < 0) { - return .{ - .fail = .{ - .msg = "Invalid original column value", - .err = error.InvalidOriginalColumnValue, - .value = original.columns.zeroBased(), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - remain = remain[original_column_delta.start..]; - - if (remain.len > 0) { - switch (remain[0]) { - ',' => { - // 4 column, but there's more on this line. - remain = remain[1..]; - }, - // 4 column, and there's no more on this line. - ';' => {}, - - // 5th column: the name - else => |c| { - // Read the name index - const name_index_delta = decodeVLQ(remain, 0); - if (name_index_delta.start == 0) { - return .{ - .fail = .{ - .msg = "Invalid name index delta", - .err = error.InvalidNameIndexDelta, - .value = @intCast(c), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - } - remain = remain[name_index_delta.start..]; - - if (options.allow_names) { - name_index += name_index_delta.value; - if (!has_names) { - mapping.ensureWithNames(allocator) catch { - return .{ - .fail = .{ - .msg = "Out of memory", - .err = error.OutOfMemory, - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; - }; - } - has_names = true; - } - - if (remain.len > 0) { - switch (remain[0]) { - // There's more on this line. - ',' => { - remain = remain[1..]; - }, - // That's the end of the line. - ';' => {}, - else => {}, - } - } - }, - } - } - mapping.append(allocator, &.{ - .generated = generated, - .original = original, - .source_index = source_index, - .name_index = name_index, - }) catch |err| bun.handleOom(err); - } - - if (needs_sort and options.sort) { - mapping.sort(); - } - - return .{ .success = .{ - .ref_count = .init(), - .mappings = mapping, - .input_line_count = input_line_count, - } }; - } -}; +pub const Mapping = @import("./Mapping.zig"); pub const ParseResult = union(enum) { fail: struct { @@ -859,158 +275,7 @@ pub const ParseResult = union(enum) { success: ParsedSourceMap, }; -pub const ParsedSourceMap = struct { - const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{}); - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; - - /// ParsedSourceMap can be acquired by different threads via the thread-safe - /// source map store (SavedSourceMap), so the reference count must be thread-safe. - ref_count: RefCount, - - input_line_count: usize = 0, - mappings: Mapping.List = .{}, - - /// If this is empty, this implies that the source code is a single file - /// transpiled on-demand. If there are items, then it means this is a file - /// loaded without transpilation but with external sources. This array - /// maps `source_index` to the correct filename. - external_source_names: []const []const u8 = &.{}, - /// In order to load source contents from a source-map after the fact, - /// a handle to the underlying source provider is stored. Within this pointer, - /// a flag is stored if it is known to be an inline or external source map. - /// - /// Source contents are large, we don't preserve them in memory. This has - /// the downside of repeatedly re-decoding sourcemaps if multiple errors - /// are emitted (specifically with Bun.inspect / unhandled; the ones that - /// rely on source contents) - underlying_provider: SourceContentPtr = .none, - - is_standalone_module_graph: bool = false, - - const SourceProviderKind = enum(u2) { zig, bake, dev_server }; - const AnySourceProvider = union(enum) { - zig: *SourceProviderMap, - bake: *BakeSourceProvider, - dev_server: *DevServerSourceProvider, - - pub fn ptr(this: AnySourceProvider) *anyopaque { - return switch (this) { - .zig => @ptrCast(this.zig), - .bake => @ptrCast(this.bake), - .dev_server => @ptrCast(this.dev_server), - }; - } - - pub fn getSourceMap( - this: AnySourceProvider, - source_filename: []const u8, - load_hint: SourceMapLoadHint, - result: ParseUrlResultHint, - ) ?SourceMap.ParseUrl { - return switch (this) { - .zig => this.zig.getSourceMap(source_filename, load_hint, result), - .bake => this.bake.getSourceMap(source_filename, load_hint, result), - .dev_server => this.dev_server.getSourceMap(source_filename, load_hint, result), - }; - } - }; - - const SourceContentPtr = packed struct(u64) { - load_hint: SourceMapLoadHint, - kind: SourceProviderKind, - data: u60, - - pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 }; - - fn fromProvider(p: *SourceProviderMap) SourceContentPtr { - return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .zig }; - } - - fn fromBakeProvider(p: *BakeSourceProvider) SourceContentPtr { - return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake }; - } - - fn fromDevServerProvider(p: *DevServerSourceProvider) SourceContentPtr { - return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .dev_server }; - } - - pub fn provider(sc: SourceContentPtr) ?AnySourceProvider { - switch (sc.kind) { - .zig => return .{ .zig = @ptrFromInt(sc.data) }, - .bake => return .{ .bake = @ptrFromInt(sc.data) }, - .dev_server => return .{ .dev_server = @ptrFromInt(sc.data) }, - } - } - }; - - pub fn isExternal(psm: *ParsedSourceMap) bool { - return psm.external_source_names.len != 0; - } - - fn deinit(this: *ParsedSourceMap) void { - const allocator = bun.default_allocator; - - this.mappings.deinit(allocator); - - if (this.external_source_names.len > 0) { - for (this.external_source_names) |name| - allocator.free(name); - allocator.free(this.external_source_names); - } - - bun.destroy(this); - } - - fn standaloneModuleGraphData(this: *ParsedSourceMap) *bun.StandaloneModuleGraph.SerializedSourceMap.Loaded { - bun.assert(this.is_standalone_module_graph); - return @ptrFromInt(this.underlying_provider.data); - } - - pub fn memoryCost(this: *const ParsedSourceMap) usize { - return @sizeOf(ParsedSourceMap) + this.mappings.memoryCost() + this.external_source_names.len * @sizeOf([]const u8); - } - - pub fn writeVLQs(map: *const ParsedSourceMap, writer: anytype) !void { - var last_col: i32 = 0; - var last_src: i32 = 0; - var last_ol: i32 = 0; - var last_oc: i32 = 0; - var current_line: i32 = 0; - for ( - map.mappings.generated(), - map.mappings.original(), - map.mappings.sourceIndex(), - 0.., - ) |gen, orig, source_index, i| { - if (current_line != gen.lines.zeroBased()) { - assert(gen.lines.zeroBased() > current_line); - const inc = gen.lines.zeroBased() - current_line; - try writer.writeByteNTimes(';', @intCast(inc)); - current_line = gen.lines.zeroBased(); - last_col = 0; - } else if (i != 0) { - try writer.writeByte(','); - } - try VLQ.encode(gen.columns.zeroBased() - last_col).writeTo(writer); - last_col = gen.columns.zeroBased(); - try VLQ.encode(source_index - last_src).writeTo(writer); - last_src = source_index; - try VLQ.encode(orig.lines.zeroBased() - last_ol).writeTo(writer); - last_ol = orig.lines.zeroBased(); - try VLQ.encode(orig.columns.zeroBased() - last_oc).writeTo(writer); - last_oc = orig.columns.zeroBased(); - } - } - - pub fn formatVLQs(map: *const ParsedSourceMap) std.fmt.Formatter(formatVLQsImpl) { - return .{ .data = map }; - } - - fn formatVLQsImpl(map: *const ParsedSourceMap, comptime _: []const u8, _: std.fmt.FormatOptions, w: anytype) !void { - try map.writeVLQs(w); - } -}; +pub const ParsedSourceMap = @import("./ParsedSourceMap.zig"); /// For some sourcemap loading code, this enum is used as a hint if it should /// bother loading source code into memory. Most uses of source maps only care @@ -1668,365 +933,7 @@ pub fn appendMappingToBuffer(buffer: *MutableString, last_byte: u8, prev_state: } } -pub const Chunk = struct { - buffer: MutableString, - - mappings_count: usize = 0, - - /// This end state will be used to rewrite the start of the following source - /// map chunk so that the delta-encoded VLQ numbers are preserved. - end_state: SourceMapState = .{}, - - /// There probably isn't a source mapping at the end of the file (nor should - /// there be) but if we're appending another source map chunk after this one, - /// we'll need to know how many characters were in the last line we generated. - final_generated_column: i32 = 0, - - /// ignore empty chunks - should_ignore: bool = true, - - pub fn initEmpty() Chunk { - return .{ - .buffer = MutableString.initEmpty(bun.default_allocator), - .mappings_count = 0, - .end_state = .{}, - .final_generated_column = 0, - .should_ignore = true, - }; - } - - pub fn deinit(this: *Chunk) void { - this.buffer.deinit(); - } - - pub fn printSourceMapContents( - chunk: Chunk, - source: *const Logger.Source, - mutable: *MutableString, - include_sources_contents: bool, - comptime ascii_only: bool, - ) !void { - try printSourceMapContentsAtOffset( - chunk, - source, - mutable, - include_sources_contents, - 0, - ascii_only, - ); - } - - pub fn printSourceMapContentsAtOffset( - chunk: Chunk, - source: *const Logger.Source, - mutable: *MutableString, - include_sources_contents: bool, - offset: usize, - comptime ascii_only: bool, - ) !void { - // attempt to pre-allocate - - var filename_buf: bun.PathBuffer = undefined; - var filename = source.path.text; - if (strings.hasPrefix(source.path.text, FileSystem.instance.top_level_dir)) { - filename = filename[FileSystem.instance.top_level_dir.len - 1 ..]; - } else if (filename.len > 0 and filename[0] != '/') { - filename_buf[0] = '/'; - @memcpy(filename_buf[1..][0..filename.len], filename); - filename = filename_buf[0 .. filename.len + 1]; - } - - mutable.growIfNeeded( - filename.len + 2 + (source.contents.len * @as(usize, @intFromBool(include_sources_contents))) + (chunk.buffer.list.items.len - offset) + 32 + 39 + 29 + 22 + 20, - ) catch unreachable; - try mutable.append("{\n \"version\":3,\n \"sources\": ["); - - try JSPrinter.quoteForJSON(filename, mutable, ascii_only); - - if (include_sources_contents) { - try mutable.append("],\n \"sourcesContent\": ["); - try JSPrinter.quoteForJSON(source.contents, mutable, ascii_only); - } - - try mutable.append("],\n \"mappings\": "); - try JSPrinter.quoteForJSON(chunk.buffer.list.items[offset..], mutable, ascii_only); - try mutable.append(", \"names\": []\n}"); - } - - // TODO: remove the indirection by having generic functions for SourceMapFormat and NewBuilder. Source maps are always VLQ - pub fn SourceMapFormat(comptime Type: type) type { - return struct { - ctx: Type, - const Format = @This(); - - pub fn init(allocator: std.mem.Allocator, prepend_count: bool) Format { - return .{ .ctx = Type.init(allocator, prepend_count) }; - } - - pub inline fn appendLineSeparator(this: *Format) anyerror!void { - try this.ctx.appendLineSeparator(); - } - - pub inline fn append(this: *Format, current_state: SourceMapState, prev_state: SourceMapState) anyerror!void { - try this.ctx.append(current_state, prev_state); - } - - pub inline fn shouldIgnore(this: Format) bool { - return this.ctx.shouldIgnore(); - } - - pub inline fn getBuffer(this: Format) MutableString { - return this.ctx.getBuffer(); - } - - pub inline fn takeBuffer(this: *Format) MutableString { - return this.ctx.takeBuffer(); - } - - pub inline fn getCount(this: Format) usize { - return this.ctx.getCount(); - } - }; - } - - pub const VLQSourceMap = struct { - data: MutableString, - count: usize = 0, - offset: usize = 0, - approximate_input_line_count: usize = 0, - - pub fn init(allocator: std.mem.Allocator, prepend_count: bool) VLQSourceMap { - var map = VLQSourceMap{ - .data = MutableString.initEmpty(allocator), - }; - - // For bun.js, we store the number of mappings and how many bytes the final list is at the beginning of the array - if (prepend_count) { - map.offset = 24; - map.data.append(&([_]u8{0} ** 24)) catch unreachable; - } - - return map; - } - - pub fn appendLineSeparator(this: *VLQSourceMap) anyerror!void { - try this.data.appendChar(';'); - } - - pub fn append(this: *VLQSourceMap, current_state: SourceMapState, prev_state: SourceMapState) anyerror!void { - const last_byte: u8 = if (this.data.list.items.len > this.offset) - this.data.list.items[this.data.list.items.len - 1] - else - 0; - - appendMappingToBuffer(&this.data, last_byte, prev_state, current_state); - this.count += 1; - } - - pub fn shouldIgnore(this: VLQSourceMap) bool { - return this.count == 0; - } - - pub fn getBuffer(this: VLQSourceMap) MutableString { - return this.data; - } - - pub fn takeBuffer(this: *VLQSourceMap) MutableString { - defer this.data = .initEmpty(this.data.allocator); - return this.data; - } - - pub fn getCount(this: VLQSourceMap) usize { - return this.count; - } - }; - - pub fn NewBuilder(comptime SourceMapFormatType: type) type { - return struct { - const ThisBuilder = @This(); - source_map: SourceMapper, - line_offset_tables: LineOffsetTable.List = .{}, - prev_state: SourceMapState = SourceMapState{}, - last_generated_update: u32 = 0, - generated_column: i32 = 0, - prev_loc: Logger.Loc = Logger.Loc.Empty, - has_prev_state: bool = false, - - line_offset_table_byte_offset_list: []const u32 = &.{}, - - // This is a workaround for a bug in the popular "source-map" library: - // https://github.com/mozilla/source-map/issues/261. The library will - // sometimes return null when querying a source map unless every line - // starts with a mapping at column zero. - // - // The workaround is to replicate the previous mapping if a line ends - // up not starting with a mapping. This is done lazily because we want - // to avoid replicating the previous mapping if we don't need to. - line_starts_with_mapping: bool = false, - cover_lines_without_mappings: bool = false, - - approximate_input_line_count: usize = 0, - - /// When generating sourcemappings for bun, we store a count of how many mappings there were - prepend_count: bool = false, - - pub const SourceMapper = SourceMapFormat(SourceMapFormatType); - - pub noinline fn generateChunk(b: *ThisBuilder, output: []const u8) Chunk { - b.updateGeneratedLineAndColumn(output); - var buffer = b.source_map.getBuffer(); - if (b.prepend_count) { - buffer.list.items[0..8].* = @as([8]u8, @bitCast(buffer.list.items.len)); - buffer.list.items[8..16].* = @as([8]u8, @bitCast(b.source_map.getCount())); - buffer.list.items[16..24].* = @as([8]u8, @bitCast(b.approximate_input_line_count)); - } - return Chunk{ - .buffer = b.source_map.takeBuffer(), - .mappings_count = b.source_map.getCount(), - .end_state = b.prev_state, - .final_generated_column = b.generated_column, - .should_ignore = b.source_map.shouldIgnore(), - }; - } - - // Scan over the printed text since the last source mapping and update the - // generated line and column numbers - pub fn updateGeneratedLineAndColumn(b: *ThisBuilder, output: []const u8) void { - const slice = output[b.last_generated_update..]; - var needs_mapping = b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.has_prev_state; - - var i: usize = 0; - const n = @as(usize, @intCast(slice.len)); - var c: i32 = 0; - while (i < n) { - const len = strings.wtf8ByteSequenceLengthWithInvalid(slice[i]); - c = strings.decodeWTF8RuneT(slice[i..].ptr[0..4], len, i32, strings.unicode_replacement); - i += @as(usize, len); - - switch (c) { - 14...127 => { - if (strings.indexOfNewlineOrNonASCII(slice, @as(u32, @intCast(i)))) |j| { - b.generated_column += @as(i32, @intCast((@as(usize, j) - i) + 1)); - i = j; - continue; - } else { - b.generated_column += @as(i32, @intCast(slice[i..].len)) + 1; - i = n; - break; - } - }, - '\r', '\n', 0x2028, 0x2029 => { - // windows newline - if (c == '\r') { - const newline_check = b.last_generated_update + i + 1; - if (newline_check < output.len and output[newline_check] == '\n') { - continue; - } - } - - // If we're about to move to the next line and the previous line didn't have - // any mappings, add a mapping at the start of the previous line. - if (needs_mapping) { - b.appendMappingWithoutRemapping(.{ - .generated_line = b.prev_state.generated_line, - .generated_column = 0, - .source_index = b.prev_state.source_index, - .original_line = b.prev_state.original_line, - .original_column = b.prev_state.original_column, - }); - } - - b.prev_state.generated_line += 1; - b.prev_state.generated_column = 0; - b.generated_column = 0; - b.source_map.appendLineSeparator() catch unreachable; - - // This new line doesn't have a mapping yet - b.line_starts_with_mapping = false; - - needs_mapping = b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.has_prev_state; - }, - - else => { - // Mozilla's "source-map" library counts columns using UTF-16 code units - b.generated_column += @as(i32, @intFromBool(c > 0xFFFF)) + 1; - }, - } - } - - b.last_generated_update = @as(u32, @truncate(output.len)); - } - - pub fn appendMapping(b: *ThisBuilder, current_state: SourceMapState) void { - b.appendMappingWithoutRemapping(current_state); - } - - pub fn appendMappingWithoutRemapping(b: *ThisBuilder, current_state: SourceMapState) void { - b.source_map.append(current_state, b.prev_state) catch unreachable; - b.prev_state = current_state; - b.has_prev_state = true; - } - - pub fn addSourceMapping(b: *ThisBuilder, loc: Logger.Loc, output: []const u8) void { - if ( - // don't insert mappings for same location twice - b.prev_loc.eql(loc) or - // exclude generated code from source - loc.start == Logger.Loc.Empty.start) - return; - - b.prev_loc = loc; - const list = b.line_offset_tables; - - // We have no sourcemappings. - // This happens for example when importing an asset which does not support sourcemaps - // like a png or a jpg - // - // import foo from "./foo.png"; - // - if (list.len == 0) { - return; - } - - const original_line = LineOffsetTable.findLine(b.line_offset_table_byte_offset_list, loc); - const line = list.get(@as(usize, @intCast(@max(original_line, 0)))); - - // Use the line to compute the column - var original_column = loc.start - @as(i32, @intCast(line.byte_offset_to_start_of_line)); - if (line.columns_for_non_ascii.len > 0 and original_column >= @as(i32, @intCast(line.byte_offset_to_first_non_ascii))) { - original_column = line.columns_for_non_ascii.slice()[@as(u32, @intCast(original_column)) - line.byte_offset_to_first_non_ascii]; - } - - b.updateGeneratedLineAndColumn(output); - - // If this line doesn't start with a mapping and we're about to add a mapping - // that's not at the start, insert a mapping first so the line starts with one. - if (b.cover_lines_without_mappings and !b.line_starts_with_mapping and b.generated_column > 0 and b.has_prev_state) { - b.appendMappingWithoutRemapping(.{ - .generated_line = b.prev_state.generated_line, - .generated_column = 0, - .source_index = b.prev_state.source_index, - .original_line = b.prev_state.original_line, - .original_column = b.prev_state.original_column, - }); - } - - b.appendMapping(.{ - .generated_line = b.prev_state.generated_line, - .generated_column = @max(b.generated_column, 0), - .source_index = b.prev_state.source_index, - .original_line = @max(original_line, 0), - .original_column = @max(original_column, 0), - }); - - // This line now has a mapping on it, so don't insert another one - b.line_starts_with_mapping = true; - } - }; - } - - pub const Builder = NewBuilder(VLQSourceMap); -}; +pub const Chunk = @import("./Chunk.zig"); /// https://sentry.engineering/blog/the-case-for-debug-ids /// https://github.com/mitsuhiko/source-map-rfc/blob/proposals/debug-id/proposals/debug-id.md @@ -2058,11 +965,9 @@ const string = []const u8; const std = @import("std"); const bun = @import("bun"); -const JSPrinter = bun.js_printer; const Logger = bun.logger; const MutableString = bun.MutableString; const StringJoiner = bun.StringJoiner; const URL = bun.URL; const assert = bun.assert; const strings = bun.strings; -const FileSystem = bun.fs.FileSystem; From 51431b6e653534345fdec39de05e8d101872d3fd Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 28 Oct 2025 12:31:42 -0700 Subject: [PATCH 262/391] Fix sourcemap comparator to use strict weak ordering (#24146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the comparator function in `src/sourcemap/Mapping.zig` to use strict weak ordering as required by sort algorithms. ## Changes - Changed `<=` to `<` in the column comparison to ensure strict ordering - Refactored the comparator to use clearer if-statement structure - Added index comparison as a tiebreaker for stable sorting when both line and column positions are equal ## Problem The original comparator used `<=` which would return true for equal elements, violating the strict weak ordering requirement. This could lead to undefined behavior in sorting. **Before:** ```zig return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased()); ``` **After:** ```zig if (a.lines.zeroBased() != b.lines.zeroBased()) { return a.lines.zeroBased() < b.lines.zeroBased(); } if (a.columns.zeroBased() != b.columns.zeroBased()) { return a.columns.zeroBased() < b.columns.zeroBased(); } return a_index < b_index; ``` ## Test plan - [x] Verified compilation with `bun bd` - The sort now properly follows strict weak ordering semantics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/sourcemap/Mapping.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sourcemap/Mapping.zig b/src/sourcemap/Mapping.zig index bbd8f0ede6..971a5d45c7 100644 --- a/src/sourcemap/Mapping.zig +++ b/src/sourcemap/Mapping.zig @@ -108,7 +108,13 @@ pub const List = struct { const a = ctx.generated[a_index]; const b = ctx.generated[b_index]; - return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased()); + if (a.lines.zeroBased() != b.lines.zeroBased()) { + return a.lines.zeroBased() < b.lines.zeroBased(); + } + if (a.columns.zeroBased() != b.columns.zeroBased()) { + return a.columns.zeroBased() < b.columns.zeroBased(); + } + return a_index < b_index; } }; From 4f1b90ad1d0da07ae990cc6885a54cc8a306d1b2 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 28 Oct 2025 12:32:15 -0700 Subject: [PATCH 263/391] Fix EventEmitter crash in removeAllListeners with removeListener meta-listener (#24148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #24147 - Fixed EventEmitter crash when `removeAllListeners()` is called from within an event handler while a `removeListener` meta-listener is registered - Added undefined check before iterating over listeners array to match Node.js behavior - Added comprehensive regression tests ## Bug Description When `removeAllListeners(type)` was called: 1. From within an event handler 2. While a `removeListener` meta-listener was registered 3. For an event type with no listeners It would crash with: `TypeError: undefined is not an object (evaluating 'this._events')` ## Root Cause The `removeAllListeners` function tried to access `listeners.length` without checking if `listeners` was defined first. When called with an event type that had no listeners, `events[type]` returned `undefined`, causing the crash. ## Fix Added a check `if (listeners !== undefined)` before iterating, matching the behavior in Node.js core: https://github.com/nodejs/node/blob/main/lib/events.js#L768 ## Test plan - ✅ Created regression test in `test/regression/issue/24147.test.ts` - ✅ Verified test fails with `USE_SYSTEM_BUN=1 bun test` (reproduces bug) - ✅ Verified test passes with `bun bd test` (confirms fix) - ✅ Test covers the exact reproduction case from the issue - ✅ Additional tests for edge cases (actual listeners, nested calls) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/js/node/events.ts | 4 +- test/regression/issue/24147.test.ts | 81 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/24147.test.ts diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 069ee1c2e2..7ba8f6cd9d 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -392,7 +392,9 @@ EventEmitterPrototype.removeAllListeners = function removeAllListeners(type) { // emit in LIFO order const listeners = events[type]; - for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]); + if (listeners !== undefined) { + for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]); + } return this; }; diff --git a/test/regression/issue/24147.test.ts b/test/regression/issue/24147.test.ts new file mode 100644 index 0000000000..4fb7052185 --- /dev/null +++ b/test/regression/issue/24147.test.ts @@ -0,0 +1,81 @@ +// https://github.com/oven-sh/bun/issues/24147 +// EventEmitter: this._events becomes undefined when removeAllListeners() +// called from event handler with removeListener meta-listener + +import { EventEmitter } from "events"; +import assert from "node:assert"; +import { test } from "node:test"; + +test("removeAllListeners() from event handler with removeListener meta-listener", () => { + const emitter = new EventEmitter(); + + emitter.on("test", () => { + // This should not crash even though there are no 'foo' listeners + emitter.removeAllListeners("foo"); + }); + + // Register a removeListener meta-listener to trigger the bug + emitter.on("removeListener", () => {}); + + // This should not throw + assert.doesNotThrow(() => emitter.emit("test")); +}); + +test("removeAllListeners() with actual listeners to remove", () => { + const emitter = new EventEmitter(); + let fooCallCount = 0; + let removeListenerCallCount = 0; + + emitter.on("foo", () => fooCallCount++); + emitter.on("foo", () => fooCallCount++); + + emitter.on("test", () => { + // Remove all 'foo' listeners while inside an event handler + emitter.removeAllListeners("foo"); + }); + + // Track removeListener calls + emitter.on("removeListener", () => { + removeListenerCallCount++; + }); + + // Emit test event which triggers removeAllListeners + emitter.emit("test"); + + // Verify listeners were removed + assert.strictEqual(emitter.listenerCount("foo"), 0); + + // Verify removeListener was called twice (once for each foo listener) + assert.strictEqual(removeListenerCallCount, 2); + + // Verify foo listeners were never called + assert.strictEqual(fooCallCount, 0); +}); + +test("nested removeAllListeners() calls", () => { + const emitter = new EventEmitter(); + const events: string[] = []; + + emitter.on("outer", () => { + events.push("outer-start"); + emitter.removeAllListeners("inner"); + events.push("outer-end"); + }); + + emitter.on("inner", () => { + events.push("inner"); + }); + + emitter.on("removeListener", type => { + events.push(`removeListener:${String(type)}`); + }); + + // This should not crash + assert.doesNotThrow(() => emitter.emit("outer")); + + // Verify correct execution order + assert.deepStrictEqual(events, ["outer-start", "removeListener:inner", "outer-end"]); + + // Verify inner listeners were removed + assert.strictEqual(emitter.listenerCount("inner"), 0); +}); From 98c04e37ec7b95dd1453f4b9e804ee89877e291a Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 28 Oct 2025 12:32:53 -0700 Subject: [PATCH 264/391] Fix source index bounds check in sourcemap decoder (#24145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix the source index bounds check in `src/sourcemap/Mapping.zig` to correctly validate indices against the range `[0, sources_count)`. ## Changes - Changed the bounds check condition from `source_index > sources_count` to `source_index >= sources_count` on line 452 - This prevents accepting `source_index == sources_count`, which would be out of bounds when indexing into the sources array ## Test plan - [x] Built successfully with `bun bd` - The existing test suite should continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/sourcemap/Mapping.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sourcemap/Mapping.zig b/src/sourcemap/Mapping.zig index 971a5d45c7..96b3791dd6 100644 --- a/src/sourcemap/Mapping.zig +++ b/src/sourcemap/Mapping.zig @@ -455,7 +455,7 @@ pub fn parse( } source_index += source_index_delta.value; - if (source_index < 0 or source_index > sources_count) { + if (source_index < 0 or source_index >= sources_count) { return .{ .fail = .{ .msg = "Invalid source index value", From fe1bc5663704586a46f062d36005d7c3023555d3 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 29 Oct 2025 07:16:32 +0100 Subject: [PATCH 265/391] Add workerd benchmark --- bench/react-hello-world/bun.lock | 14 ++--- bench/react-hello-world/package.json | 7 +-- .../react-hello-world.workerd.config.capnp | 23 ++++++++ .../react-hello-world.workerd.js | 53 +++++++++++++++++++ .../react-hello-world.workerd.jsx | 24 +++++++++ 5 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 bench/react-hello-world/react-hello-world.workerd.config.capnp create mode 100644 bench/react-hello-world/react-hello-world.workerd.js create mode 100644 bench/react-hello-world/react-hello-world.workerd.jsx diff --git a/bench/react-hello-world/bun.lock b/bench/react-hello-world/bun.lock index 56594f42eb..218c02e565 100644 --- a/bench/react-hello-world/bun.lock +++ b/bench/react-hello-world/bun.lock @@ -4,20 +4,16 @@ "": { "name": "react-hello-world", "dependencies": { - "react": "next", - "react-dom": "next", + "react": "^19.2.0", + "react-dom": "^19.2.0", }, }, }, "packages": { - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], - "react": ["react@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-l6RbwXa9Peerh9pQEq62DDypxSQfavbybY0wV1vwZ63X0P5VaaEesZAz1KPpnVvXjTtQaOMQsIPvnQwmaVqzTQ=="], - - "react-dom": ["react-dom@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "0.24.0-next-b72ed698f-20230303" }, "peerDependencies": { "react": "18.3.0-next-b72ed698f-20230303" } }, "sha512-0Gh/gmTT6H8KxswIQB/8shdTTfs6QIu86nNqZf3Y0RBqIwgTVxRaQVz14/Fw4/Nt81nK/Jt6KT4bx3yvOxZDGQ=="], - - "scheduler": ["scheduler@0.24.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ct4DMMFbc2kFxCdvbG+i/Jn1S1oqrIFSn2VX/mam+Ya0iuNy+lb8rgT7A+YBUqrQNDaNEqABYI2sOQgqoRxp7w=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], } } diff --git a/bench/react-hello-world/package.json b/bench/react-hello-world/package.json index b114852054..ca4b400596 100644 --- a/bench/react-hello-world/package.json +++ b/bench/react-hello-world/package.json @@ -4,13 +4,14 @@ "description": "", "main": "react-hello-world.node.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "build:workerd": "bun build react-hello-world.workerd.jsx --outfile=react-hello-world.workerd.js --format=esm --production" }, "keywords": [], "author": "Colin McDonnell", "license": "ISC", "dependencies": { - "react": "next", - "react-dom": "next" + "react": "^19.2.0", + "react-dom": "^19.2.0" } } diff --git a/bench/react-hello-world/react-hello-world.workerd.config.capnp b/bench/react-hello-world/react-hello-world.workerd.config.capnp new file mode 100644 index 0000000000..e624b143be --- /dev/null +++ b/bench/react-hello-world/react-hello-world.workerd.config.capnp @@ -0,0 +1,23 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ + (name = "main", worker = .mainWorker), + ], + + sockets = [ + ( name = "http", + address = "*:3001", + http = (), + service = "main" + ), + ] +); + +const mainWorker :Workerd.Worker = ( + modules = [ + (name = "worker", esModule = embed "react-hello-world.workerd.js"), + ], + compatibilityDate = "2025-01-01", + compatibilityFlags = ["nodejs_compat"], +); diff --git a/bench/react-hello-world/react-hello-world.workerd.js b/bench/react-hello-world/react-hello-world.workerd.js new file mode 100644 index 0000000000..ae8c4334ed --- /dev/null +++ b/bench/react-hello-world/react-hello-world.workerd.js @@ -0,0 +1,53 @@ +var VC=Object.create;var{getPrototypeOf:SC,defineProperty:XE,getOwnPropertyNames:FC}=Object;var hC=Object.prototype.hasOwnProperty;var Dc=(f,u,c)=>{c=f!=null?VC(SC(f)):{};let y=u||!f||!f.__esModule?XE(c,"default",{value:f,enumerable:!0}):c;for(let _ of FC(f))if(!hC.call(y,_))XE(y,_,{get:()=>f[_],enumerable:!0});return y};var mx=(f,u)=>()=>(u||f((u={exports:{}}).exports,u),u.exports);var BE=(f,u)=>{for(var c in u)XE(f,c,{get:u[c],enumerable:!0,configurable:!0,set:(y)=>u[c]=()=>y})};var iC=(f,u)=>()=>(f&&(u=f(f=0)),u);var Dy=mx((_g)=>{var PE=Symbol.for("react.transitional.element"),tC=Symbol.for("react.portal"),KC=Symbol.for("react.fragment"),kC=Symbol.for("react.strict_mode"),dC=Symbol.for("react.profiler"),bC=Symbol.for("react.consumer"),lC=Symbol.for("react.context"),pC=Symbol.for("react.forward_ref"),qC=Symbol.for("react.suspense"),oC=Symbol.for("react.memo"),Yx=Symbol.for("react.lazy"),eC=Symbol.for("react.activity"),Hx=Symbol.iterator;function aC(f){if(f===null||typeof f!=="object")return null;return f=Hx&&f[Hx]||f["@@iterator"],typeof f==="function"?f:null}var Mx={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},nx=Object.assign,Nx={};function zc(f,u,c){this.props=f,this.context=u,this.refs=Nx,this.updater=c||Mx}zc.prototype.isReactComponent={};zc.prototype.setState=function(f,u){if(typeof f!=="object"&&typeof f!=="function"&&f!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,f,u,"setState")};zc.prototype.forceUpdate=function(f){this.updater.enqueueForceUpdate(this,f,"forceUpdate")};function rx(){}rx.prototype=zc.prototype;function JE(f,u,c){this.props=f,this.context=u,this.refs=Nx,this.updater=c||Mx}var VE=JE.prototype=new rx;VE.constructor=JE;nx(VE,zc.prototype);VE.isPureReactComponent=!0;var Ix=Array.isArray;function ZE(){}var K={H:null,A:null,T:null,S:null},Dx=Object.prototype.hasOwnProperty;function SE(f,u,c){var y=c.ref;return{$$typeof:PE,type:f,key:u,ref:y!==void 0?y:null,props:c}}function sC(f,u){return SE(f.type,u,f.props)}function FE(f){return typeof f==="object"&&f!==null&&f.$$typeof===PE}function fg(f){var u={"=":"=0",":":"=2"};return"$"+f.replace(/[=:]/g,function(c){return u[c]})}var Ux=/\/+/g;function QE(f,u){return typeof f==="object"&&f!==null&&f.key!=null?fg(""+f.key):u.toString(36)}function ug(f){switch(f.status){case"fulfilled":return f.value;case"rejected":throw f.reason;default:switch(typeof f.status==="string"?f.then(ZE,ZE):(f.status="pending",f.then(function(u){f.status==="pending"&&(f.status="fulfilled",f.value=u)},function(u){f.status==="pending"&&(f.status="rejected",f.reason=u)})),f.status){case"fulfilled":return f.value;case"rejected":throw f.reason}}throw f}function $c(f,u,c,y,_){var E=typeof f;if(E==="undefined"||E==="boolean")f=null;var v=!1;if(f===null)v=!0;else switch(E){case"bigint":case"string":case"number":v=!0;break;case"object":switch(f.$$typeof){case PE:case tC:v=!0;break;case Yx:return v=f._init,$c(v(f._payload),u,c,y,_)}}if(v)return _=_(f),v=y===""?"."+QE(f,0):y,Ix(_)?(c="",v!=null&&(c=v.replace(Ux,"$&/")+"/"),$c(_,u,c,"",function(R){return R})):_!=null&&(FE(_)&&(_=sC(_,c+(_.key==null||f&&f.key===_.key?"":(""+_.key).replace(Ux,"$&/")+"/")+v)),u.push(_)),1;v=0;var T=y===""?".":y+":";if(Ix(f))for(var x=0;xix,useFormStatus:()=>hx,useFormState:()=>Fx,unstable_batchedUpdates:()=>Sx,requestFormReset:()=>Vx,preloadModule:()=>Jx,preload:()=>Px,preinitModule:()=>Zx,preinit:()=>Qx,prefetchDNS:()=>Bx,preconnect:()=>Xx,flushSync:()=>Gx,createPortal:()=>jx,__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE:()=>Wx});function zx(f){var u="https://react.dev/errors/"+f;if(1{$x=Dc(Dy(),1);zf={d:{f:Ku,r:function(){throw Error(zx(522))},D:Ku,C:Ku,L:Ku,m:Ku,X:Ku,S:Ku,M:Ku},p:0,findDOMNode:null},lg=Symbol.for("react.portal");$y=$x.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;Wx=zf});var iE=mx((Zw,kx)=>{tx();function Kx(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=="function")return;try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Kx)}catch(f){console.error(f)}}Kx(),kx.exports=hE});var bc=Dc(Dy(),1);var Mv={};BE(Mv,{version:()=>cR,renderToString:()=>uR,renderToStaticMarkup:()=>fR});var Q_=Dc(Dy(),1),YT=Dc(iE(),1);function n(f){var u="https://react.dev/errors/"+f;if(1>>16)&65535)<<16)&4294967295,E=E<<15|E>>>17,E=461845907*(E&65535)+((461845907*(E>>>16)&65535)<<16)&4294967295,_^=E,_=_<<13|_>>>19,_=5*(_&65535)+((5*(_>>>16)&65535)<<16)&4294967295,_=(_&65535)+27492+(((_>>>16)+58964&65535)<<16)}switch(E=0,c){case 3:E^=(f.charCodeAt(u+2)&255)<<16;case 2:E^=(f.charCodeAt(u+1)&255)<<8;case 1:E^=f.charCodeAt(u)&255,E=3432918353*(E&65535)+((3432918353*(E>>>16)&65535)<<16)&4294967295,E=E<<15|E>>>17,_^=461845907*(E&65535)+((461845907*(E>>>16)&65535)<<16)&4294967295}return _^=f.length,_^=_>>>16,_=2246822507*(_&65535)+((2246822507*(_>>>16)&65535)<<16)&4294967295,_^=_>>>13,_=3266489909*(_&65535)+((3266489909*(_>>>16)&65535)<<16)&4294967295,(_^_>>>16)>>>0}var Zf=Object.assign,k=Object.prototype.hasOwnProperty,sg=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$"),lx={},px={};function Rv(f){if(k.call(px,f))return!0;if(k.call(lx,f))return!1;if(sg.test(f))return px[f]=!0;return lx[f]=!0,!1}var fO=new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split(" ")),uO=new Map([["acceptCharset","accept-charset"],["htmlFor","for"],["httpEquiv","http-equiv"],["crossOrigin","crossorigin"],["accentHeight","accent-height"],["alignmentBaseline","alignment-baseline"],["arabicForm","arabic-form"],["baselineShift","baseline-shift"],["capHeight","cap-height"],["clipPath","clip-path"],["clipRule","clip-rule"],["colorInterpolation","color-interpolation"],["colorInterpolationFilters","color-interpolation-filters"],["colorProfile","color-profile"],["colorRendering","color-rendering"],["dominantBaseline","dominant-baseline"],["enableBackground","enable-background"],["fillOpacity","fill-opacity"],["fillRule","fill-rule"],["floodColor","flood-color"],["floodOpacity","flood-opacity"],["fontFamily","font-family"],["fontSize","font-size"],["fontSizeAdjust","font-size-adjust"],["fontStretch","font-stretch"],["fontStyle","font-style"],["fontVariant","font-variant"],["fontWeight","font-weight"],["glyphName","glyph-name"],["glyphOrientationHorizontal","glyph-orientation-horizontal"],["glyphOrientationVertical","glyph-orientation-vertical"],["horizAdvX","horiz-adv-x"],["horizOriginX","horiz-origin-x"],["imageRendering","image-rendering"],["letterSpacing","letter-spacing"],["lightingColor","lighting-color"],["markerEnd","marker-end"],["markerMid","marker-mid"],["markerStart","marker-start"],["overlinePosition","overline-position"],["overlineThickness","overline-thickness"],["paintOrder","paint-order"],["panose-1","panose-1"],["pointerEvents","pointer-events"],["renderingIntent","rendering-intent"],["shapeRendering","shape-rendering"],["stopColor","stop-color"],["stopOpacity","stop-opacity"],["strikethroughPosition","strikethrough-position"],["strikethroughThickness","strikethrough-thickness"],["strokeDasharray","stroke-dasharray"],["strokeDashoffset","stroke-dashoffset"],["strokeLinecap","stroke-linecap"],["strokeLinejoin","stroke-linejoin"],["strokeMiterlimit","stroke-miterlimit"],["strokeOpacity","stroke-opacity"],["strokeWidth","stroke-width"],["textAnchor","text-anchor"],["textDecoration","text-decoration"],["textRendering","text-rendering"],["transformOrigin","transform-origin"],["underlinePosition","underline-position"],["underlineThickness","underline-thickness"],["unicodeBidi","unicode-bidi"],["unicodeRange","unicode-range"],["unitsPerEm","units-per-em"],["vAlphabetic","v-alphabetic"],["vHanging","v-hanging"],["vIdeographic","v-ideographic"],["vMathematical","v-mathematical"],["vectorEffect","vector-effect"],["vertAdvY","vert-adv-y"],["vertOriginX","vert-origin-x"],["vertOriginY","vert-origin-y"],["wordSpacing","word-spacing"],["writingMode","writing-mode"],["xmlnsXlink","xmlns:xlink"],["xHeight","x-height"]]),cO=/["'&<>]/;function X(f){if(typeof f==="boolean"||typeof f==="number"||typeof f==="bigint")return""+f;f=""+f;var u=cO.exec(f);if(u){var c="",y,_=0;for(y=u.index;yf.insertionMode)return Rf(3,null,y,null);break;case"html":if(f.insertionMode===0)return Rf(1,null,y,null)}return 6<=f.insertionMode||2>f.insertionMode?Rf(2,null,y,null):f.tagScope!==y?Rf(f.insertionMode,f.selectedValue,y,null):f}function BT(f){return f===null?null:{update:f.update,enter:"none",exit:"none",share:f.update,name:f.autoName,autoName:f.autoName,nameIdx:0}}function pE(f,u){return u.tagScope&32&&(f.instructions|=128),Rf(u.insertionMode,u.selectedValue,u.tagScope|12,BT(u.viewTransition))}function w_(f,u){f=BT(u.viewTransition);var c=u.tagScope|16;return f!==null&&f.share!=="none"&&(c|=64),Rf(u.insertionMode,u.selectedValue,c,f)}var ox=new Map;function QT(f,u){if(typeof u!=="object")throw Error(n(62));var c=!0,y;for(y in u)if(k.call(u,y)){var _=u[y];if(_!=null&&typeof _!=="boolean"&&_!==""){if(y.indexOf("--")===0){var E=X(y);_=X((""+_).trim())}else E=ox.get(y),E===void 0&&(E=X(y.replace(yO,"-$1").toLowerCase().replace(_O,"-ms-")),ox.set(y,E)),_=typeof _==="number"?_===0||fO.has(y)?""+_:_+"px":X((""+_).trim());c?(c=!1,f.push(' style="',E,":",_)):f.push(";",E,":",_)}}c||f.push('"')}function qE(f,u,c){c&&typeof c!=="function"&&typeof c!=="symbol"&&f.push(" ",u,'=""')}function Af(f,u,c){typeof c!=="function"&&typeof c!=="symbol"&&typeof c!=="boolean"&&f.push(" ",u,'="',X(c),'"')}var ZT=X("javascript:throw new Error('React form unexpectedly submitted.')");function tE(f,u){this.push('")}function PT(f){if(typeof f!=="string")throw Error(n(480))}function JT(f,u){if(typeof u.$$FORM_ACTION==="function"){var c=f.nextFormID++;f=f.idPrefix+c;try{var y=u.$$FORM_ACTION(f);if(y){var _=y.data;_!=null&&_.forEach(PT)}return y}catch(E){if(typeof E==="object"&&E!==null&&typeof E.then==="function")throw E}}return null}function ex(f,u,c,y,_,E,v,T){var x=null;if(typeof y==="function"){var R=JT(u,y);R!==null?(T=R.name,y=R.action||"",_=R.encType,E=R.method,v=R.target,x=R.data):(f.push(" ","formAction",'="',ZT,'"'),v=E=_=y=T=null,VT(u,c))}return T!=null&&V(f,"name",T),y!=null&&V(f,"formAction",y),_!=null&&V(f,"formEncType",_),E!=null&&V(f,"formMethod",E),v!=null&&V(f,"formTarget",v),x}function V(f,u,c){switch(u){case"className":Af(f,"class",c);break;case"tabIndex":Af(f,"tabindex",c);break;case"dir":case"role":case"viewBox":case"width":case"height":Af(f,u,c);break;case"style":QT(f,c);break;case"src":case"href":if(c==="")break;case"action":case"formAction":if(c==null||typeof c==="function"||typeof c==="symbol"||typeof c==="boolean")break;c=Xy(""+c),f.push(" ",u,'="',X(c),'"');break;case"defaultValue":case"defaultChecked":case"innerHTML":case"suppressContentEditableWarning":case"suppressHydrationWarning":case"ref":break;case"autoFocus":case"multiple":case"muted":qE(f,u.toLowerCase(),c);break;case"xlinkHref":if(typeof c==="function"||typeof c==="symbol"||typeof c==="boolean")break;c=Xy(""+c),f.push(" ","xlink:href",'="',X(c),'"');break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":typeof c!=="function"&&typeof c!=="symbol"&&f.push(" ",u,'="',X(c),'"');break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":c&&typeof c!=="function"&&typeof c!=="symbol"&&f.push(" ",u,'=""');break;case"capture":case"download":c===!0?f.push(" ",u,'=""'):c!==!1&&typeof c!=="function"&&typeof c!=="symbol"&&f.push(" ",u,'="',X(c),'"');break;case"cols":case"rows":case"size":case"span":typeof c!=="function"&&typeof c!=="symbol"&&!isNaN(c)&&1<=c&&f.push(" ",u,'="',X(c),'"');break;case"rowSpan":case"start":typeof c==="function"||typeof c==="symbol"||isNaN(c)||f.push(" ",u,'="',X(c),'"');break;case"xlinkActuate":Af(f,"xlink:actuate",c);break;case"xlinkArcrole":Af(f,"xlink:arcrole",c);break;case"xlinkRole":Af(f,"xlink:role",c);break;case"xlinkShow":Af(f,"xlink:show",c);break;case"xlinkTitle":Af(f,"xlink:title",c);break;case"xlinkType":Af(f,"xlink:type",c);break;case"xmlBase":Af(f,"xml:base",c);break;case"xmlLang":Af(f,"xml:lang",c);break;case"xmlSpace":Af(f,"xml:space",c);break;default:if(!(2",`addEventListener("submit",function(a){if(!a.defaultPrevented){var c=a.target,d=a.submitter,e=c.action,b=d;if(d){var f=d.getAttribute("formAction");null!=f&&(e=f,b=null)}"javascript:throw new Error('React form unexpectedly submitted.')"===e&&(a.preventDefault(),b?(a=document.createElement("input"),a.name=b.name,a.value=b.value,b.parentNode.insertBefore(a,b),b=new FormData(c),a.parentNode.removeChild(a)):b=new FormData(c),a=c.ownerDocument||c,(a.$$reactFormReplay=a.$$reactFormReplay||[]).push(c,d,b))}});`,"")):y.unshift(u.startInlineScript,">",`addEventListener("submit",function(a){if(!a.defaultPrevented){var c=a.target,d=a.submitter,e=c.action,b=d;if(d){var f=d.getAttribute("formAction");null!=f&&(e=f,b=null)}"javascript:throw new Error('React form unexpectedly submitted.')"===e&&(a.preventDefault(),b?(a=document.createElement("input"),a.name=b.name,a.value=b.value,b.parentNode.insertBefore(a,b),b=new FormData(c),a.parentNode.removeChild(a)):b=new FormData(c),a=c.ownerDocument||c,(a.$$reactFormReplay=a.$$reactFormReplay||[]).push(c,d,b))}});`,"")}}function wf(f,u){f.push(cf("link"));for(var c in u)if(k.call(u,c)){var y=u[c];if(y!=null)switch(c){case"children":case"dangerouslySetInnerHTML":throw Error(n(399,"link"));default:V(f,c,y)}}return f.push("/>"),null}var ax=/(<\/|<)(s)(tyle)/gi;function sx(f,u,c,y){return""+u+(c==="s"?"\\73 ":"\\53 ")+y}function jc(f,u,c){f.push(cf(c));for(var y in u)if(k.call(u,y)){var _=u[y];if(_!=null)switch(y){case"children":case"dangerouslySetInnerHTML":throw Error(n(399,c));default:V(f,y,_)}}return f.push("/>"),null}function fT(f,u){f.push(cf("title"));var c=null,y=null,_;for(_ in u)if(k.call(u,_)){var E=u[_];if(E!=null)switch(_){case"children":c=E;break;case"dangerouslySetInnerHTML":y=E;break;default:V(f,_,E)}}return f.push(">"),u=Array.isArray(c)?2>c.length?c[0]:null:c,typeof u!=="function"&&typeof u!=="symbol"&&u!==null&&u!==void 0&&f.push(X(""+u)),kf(f,y,c),f.push(Ic("title")),null}function n_(f,u){f.push(cf("script"));var c=null,y=null,_;for(_ in u)if(k.call(u,_)){var E=u[_];if(E!=null)switch(_){case"children":c=E;break;case"dangerouslySetInnerHTML":y=E;break;default:V(f,_,E)}}return f.push(">"),kf(f,y,c),typeof c==="string"&&f.push((""+c).replace(GT,XT)),f.push(Ic("script")),null}function KE(f,u,c){f.push(cf(c));var y=c=null,_;for(_ in u)if(k.call(u,_)){var E=u[_];if(E!=null)switch(_){case"children":c=E;break;case"dangerouslySetInnerHTML":y=E;break;default:V(f,_,E)}}return f.push(">"),kf(f,y,c),c}function g_(f,u,c){f.push(cf(c));var y=c=null,_;for(_ in u)if(k.call(u,_)){var E=u[_];if(E!=null)switch(_){case"children":c=E;break;case"dangerouslySetInnerHTML":y=E;break;default:V(f,_,E)}}return f.push(">"),kf(f,y,c),typeof c==="string"?(f.push(X(c)),null):c}var RO=/^[a-zA-Z][a-zA-Z:_\.\-\d]*$/,uT=new Map;function cf(f){var u=uT.get(f);if(u===void 0){if(!RO.test(f))throw Error(n(65,f));u="<"+f,uT.set(f,u)}return u}function CO(f,u,c,y,_,E,v,T,x){switch(u){case"div":case"span":case"svg":case"path":break;case"a":f.push(cf("a"));var R=null,C=null,g;for(g in c)if(k.call(c,g)){var O=c[g];if(O!=null)switch(g){case"children":R=O;break;case"dangerouslySetInnerHTML":C=O;break;case"href":O===""?Af(f,"href",""):V(f,g,O);break;default:V(f,g,O)}}if(f.push(">"),kf(f,C,R),typeof R==="string"){f.push(X(R));var m=null}else m=R;return m;case"g":case"p":case"li":break;case"select":f.push(cf("select"));var M=null,U=null,I;for(I in c)if(k.call(c,I)){var Y=c[I];if(Y!=null)switch(I){case"children":M=Y;break;case"dangerouslySetInnerHTML":U=Y;break;case"defaultValue":case"value":break;default:V(f,I,Y)}}return f.push(">"),kf(f,U,M),M;case"option":var r=T.selectedValue;f.push(cf("option"));var G=null,B=null,z=null,L=null,e;for(e in c)if(k.call(c,e)){var b=c[e];if(b!=null)switch(e){case"children":G=b;break;case"selected":z=b;break;case"dangerouslySetInnerHTML":L=b;break;case"value":B=b;default:V(f,e,b)}}if(r!=null){var $=B!==null?""+B:TO(G);if(M_(r)){for(var a=0;a"),kf(f,L,G),G;case"textarea":f.push(cf("textarea"));var D=null,F=null,J=null,j;for(j in c)if(k.call(c,j)){var l=c[j];if(l!=null)switch(j){case"children":J=l;break;case"value":D=l;break;case"defaultValue":F=l;break;case"dangerouslySetInnerHTML":throw Error(n(91));default:V(f,j,l)}}if(D===null&&F!==null&&(D=F),f.push(">"),J!=null){if(D!=null)throw Error(n(92));if(M_(J)){if(1"),Du!=null&&Du.forEach(tE,f),null;case"button":f.push(cf("button"));var Eu=null,lc=null,pc=null,qc=null,oc=null,ec=null,ac=null,vu;for(vu in c)if(k.call(c,vu)){var uf=c[vu];if(uf!=null)switch(vu){case"children":Eu=uf;break;case"dangerouslySetInnerHTML":lc=uf;break;case"name":pc=uf;break;case"formAction":qc=uf;break;case"formEncType":oc=uf;break;case"formMethod":ec=uf;break;case"formTarget":ac=uf;break;default:V(f,vu,uf)}}var sc=ex(f,y,_,qc,oc,ec,ac,pc);if(f.push(">"),sc!=null&&sc.forEach(tE,f),kf(f,lc,Eu),typeof Eu==="string"){f.push(X(Eu));var fy=null}else fy=Eu;return fy;case"form":f.push(cf("form"));var xu=null,uy=null,Mf=null,Tu=null,Ru=null,Cu=null,gu;for(gu in c)if(k.call(c,gu)){var xf=c[gu];if(xf!=null)switch(gu){case"children":xu=xf;break;case"dangerouslySetInnerHTML":uy=xf;break;case"action":Mf=xf;break;case"encType":Tu=xf;break;case"method":Ru=xf;break;case"target":Cu=xf;break;default:V(f,gu,xf)}}var uc=null,cc=null;if(typeof Mf==="function"){var nf=JT(y,Mf);nf!==null?(Mf=nf.action||"",Tu=nf.encType,Ru=nf.method,Cu=nf.target,uc=nf.data,cc=nf.name):(f.push(" ","action",'="',ZT,'"'),Cu=Ru=Tu=Mf=null,VT(y,_))}if(Mf!=null&&V(f,"action",Mf),Tu!=null&&V(f,"encType",Tu),Ru!=null&&V(f,"method",Ru),Cu!=null&&V(f,"target",Cu),f.push(">"),cc!==null&&(f.push('"),uc!=null&&uc.forEach(tE,f)),kf(f,uy,xu),typeof xu==="string"){f.push(X(xu));var cy=null}else cy=xu;return cy;case"menuitem":f.push(cf("menuitem"));for(var $u in c)if(k.call(c,$u)){var yy=c[$u];if(yy!=null)switch($u){case"children":case"dangerouslySetInnerHTML":throw Error(n(400));default:V(f,$u,yy)}}return f.push(">"),null;case"object":f.push(cf("object"));var Ou=null,_y=null,Au;for(Au in c)if(k.call(c,Au)){var wu=c[Au];if(wu!=null)switch(Au){case"children":Ou=wu;break;case"dangerouslySetInnerHTML":_y=wu;break;case"data":var Ey=Xy(""+wu);if(Ey==="")break;f.push(" ","data",'="',X(Ey),'"');break;default:V(f,Au,wu)}}if(f.push(">"),kf(f,_y,Ou),typeof Ou==="string"){f.push(X(Ou));var vy=null}else vy=Ou;return vy;case"title":var YE=T.tagScope&1,ME=T.tagScope&4;if(T.insertionMode===4||YE||c.itemProp!=null)var yc=fT(f,c);else ME?yc=null:(fT(_.hoistableChunks,c),yc=void 0);return yc;case"link":var nE=T.tagScope&1,NE=T.tagScope&4,rE=c.rel,Tf=c.href,zu=c.precedence;if(T.insertionMode===4||nE||c.itemProp!=null||typeof rE!=="string"||typeof Tf!=="string"||Tf===""){wf(f,c);var mu=null}else if(c.rel==="stylesheet")if(typeof zu!=="string"||c.disabled!=null||c.onLoad||c.onError)mu=wf(f,c);else{var Vf=_.styles.get(zu),Wu=y.styleResources.hasOwnProperty(Tf)?y.styleResources[Tf]:void 0;if(Wu!==null){y.styleResources[Tf]=null,Vf||(Vf={precedence:X(zu),rules:[],hrefs:[],sheets:new Map},_.styles.set(zu,Vf));var ju={state:0,props:Zf({},c,{"data-precedence":c.precedence,precedence:null})};if(Wu){Wu.length===2&&By(ju.props,Wu);var _c=_.preloads.stylesheets.get(Tf);_c&&0<_c.length?_c.length=0:ju.state=1}Vf.sheets.set(Tf,ju),v&&v.stylesheets.add(ju)}else if(Vf){var xy=Vf.sheets.get(Tf);xy&&v&&v.stylesheets.add(xy)}x&&f.push(""),mu=null}else c.onLoad||c.onError?mu=wf(f,c):(x&&f.push(""),mu=NE?null:wf(_.hoistableChunks,c));return mu;case"script":var DE=T.tagScope&1,Ec=c.async;if(typeof c.src!=="string"||!c.src||!Ec||typeof Ec==="function"||typeof Ec==="symbol"||c.onLoad||c.onError||T.insertionMode===4||DE||c.itemProp!=null)var Ty=n_(f,c);else{var Gu=c.src;if(c.type==="module")var Xu=y.moduleScriptResources,Ry=_.preloads.moduleScripts;else Xu=y.scriptResources,Ry=_.preloads.scripts;var Bu=Xu.hasOwnProperty(Gu)?Xu[Gu]:void 0;if(Bu!==null){Xu[Gu]=null;var vc=c;if(Bu){Bu.length===2&&(vc=Zf({},c),By(vc,Bu));var Cy=Ry.get(Gu);Cy&&(Cy.length=0)}var gy=[];_.scripts.add(gy),n_(gy,vc)}x&&f.push(""),Ty=null}return Ty;case"style":var $E=T.tagScope&1,Qu=c.precedence,Sf=c.href,zE=c.nonce;if(T.insertionMode===4||$E||c.itemProp!=null||typeof Qu!=="string"||typeof Sf!=="string"||Sf===""){f.push(cf("style"));var Ff=null,Oy=null,Hu;for(Hu in c)if(k.call(c,Hu)){var Zu=c[Hu];if(Zu!=null)switch(Hu){case"children":Ff=Zu;break;case"dangerouslySetInnerHTML":Oy=Zu;break;default:V(f,Hu,Zu)}}f.push(">");var Iu=Array.isArray(Ff)?2>Ff.length?Ff[0]:null:Ff;typeof Iu!=="function"&&typeof Iu!=="symbol"&&Iu!==null&&Iu!==void 0&&f.push((""+Iu).replace(ax,sx)),kf(f,Oy,Ff),f.push(Ic("style"));var Ay=null}else{var Nf=_.styles.get(Qu);if((y.styleResources.hasOwnProperty(Sf)?y.styleResources[Sf]:void 0)!==null){y.styleResources[Sf]=null,Nf||(Nf={precedence:X(Qu),rules:[],hrefs:[],sheets:new Map},_.styles.set(Qu,Nf));var wy=_.nonce.style;if(!wy||wy===zE){Nf.hrefs.push(X(Sf));var my=Nf.rules,hf=null,Hy=null,Pu;for(Pu in c)if(k.call(c,Pu)){var xc=c[Pu];if(xc!=null)switch(Pu){case"children":hf=xc;break;case"dangerouslySetInnerHTML":Hy=xc}}var Uu=Array.isArray(hf)?2>hf.length?hf[0]:null:hf;typeof Uu!=="function"&&typeof Uu!=="symbol"&&Uu!==null&&Uu!==void 0&&my.push((""+Uu).replace(ax,sx)),kf(my,Hy,hf)}}Nf&&v&&v.styles.add(Nf),x&&f.push(""),Ay=void 0}return Ay;case"meta":var WE=T.tagScope&1,jE=T.tagScope&4;if(T.insertionMode===4||WE||c.itemProp!=null)var Iy=jc(f,c,"meta");else x&&f.push(""),Iy=jE?null:typeof c.charSet==="string"?jc(_.charsetChunks,c,"meta"):c.name==="viewport"?jc(_.viewportChunks,c,"meta"):jc(_.hoistableChunks,c,"meta");return Iy;case"listing":case"pre":f.push(cf(u));var Lu=null,Yu=null,Mu;for(Mu in c)if(k.call(c,Mu)){var Ju=c[Mu];if(Ju!=null)switch(Mu){case"children":Lu=Ju;break;case"dangerouslySetInnerHTML":Yu=Ju;break;default:V(f,Mu,Ju)}}if(f.push(">"),Yu!=null){if(Lu!=null)throw Error(n(60));if(typeof Yu!=="object"||!("__html"in Yu))throw Error(n(61));var rf=Yu.__html;rf!==null&&rf!==void 0&&(typeof rf==="string"&&0_.highImagePreloads.size)Tc.delete(tf),_.highImagePreloads.add(Df)}else if(!y.imageResources.hasOwnProperty(tf)){y.imageResources[tf]=sf;var Rc=c.crossOrigin,Ly=typeof Rc==="string"?Rc==="use-credentials"?Rc:"":void 0,$f=_.headers,Cc;$f&&0<$f.remainingCapacity&&typeof c.srcSet!=="string"&&(c.fetchPriority==="high"||500>$f.highImagePreloads.length)&&(Cc=r_(Z,"image",{imageSrcSet:c.srcSet,imageSizes:c.sizes,crossOrigin:Ly,integrity:c.integrity,nonce:c.nonce,type:c.type,fetchPriority:c.fetchPriority,referrerPolicy:c.refererPolicy}),0<=($f.remainingCapacity-=Cc.length+2))?(_.resets.image[tf]=sf,$f.highImagePreloads&&($f.highImagePreloads+=", "),$f.highImagePreloads+=Cc):(Df=[],wf(Df,{rel:"preload",as:"image",href:Q?void 0:Z,imageSrcSet:Q,imageSizes:Uy,crossOrigin:Ly,integrity:c.integrity,type:c.type,fetchPriority:c.fetchPriority,referrerPolicy:c.referrerPolicy}),c.fetchPriority==="high"||10>_.highImagePreloads.size?_.highImagePreloads.add(Df):(_.bulkPreloads.add(Df),Tc.set(tf,Df)))}}return jc(f,c,"img");case"base":case"area":case"br":case"col":case"embed":case"hr":case"keygen":case"param":case"source":case"track":case"wbr":return jc(f,c,u);case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":break;case"head":if(2>T.insertionMode){var gc=E||_.preamble;if(gc.headChunks)throw Error(n(545,"``"));E!==null&&f.push(""),gc.headChunks=[];var Yy=KE(gc.headChunks,c,"head")}else Yy=g_(f,c,"head");return Yy;case"body":if(2>T.insertionMode){var Oc=E||_.preamble;if(Oc.bodyChunks)throw Error(n(545,"``"));E!==null&&f.push(""),Oc.bodyChunks=[];var My=KE(Oc.bodyChunks,c,"body")}else My=g_(f,c,"body");return My;case"html":if(T.insertionMode===0){var Ac=E||_.preamble;if(Ac.htmlChunks)throw Error(n(545,"``"));E!==null&&f.push(""),Ac.htmlChunks=[""];var ny=KE(Ac.htmlChunks,c,"html")}else ny=g_(f,c,"html");return ny;default:if(u.indexOf("-")!==-1){f.push(cf(u));var wc=null,Ny=null,Kf;for(Kf in c)if(k.call(c,Kf)){var p=c[Kf];if(p!=null){var ry=Kf;switch(Kf){case"children":wc=p;break;case"dangerouslySetInnerHTML":Ny=p;break;case"style":QT(f,p);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"ref":break;case"className":ry="class";default:if(Rv(Kf)&&typeof p!=="function"&&typeof p!=="symbol"&&p!==!1){if(p===!0)p="";else if(typeof p==="object")continue;f.push(" ",ry,'="',X(p),'"')}}}}return f.push(">"),kf(f,Ny,wc),wc}}return g_(f,c,u)}var cT=new Map;function Ic(f){var u=cT.get(f);return u===void 0&&(u="",cT.set(f,u)),u}function yT(f,u){f=f.preamble,f.htmlChunks===null&&u.htmlChunks&&(f.htmlChunks=u.htmlChunks),f.headChunks===null&&u.headChunks&&(f.headChunks=u.headChunks),f.bodyChunks===null&&u.bodyChunks&&(f.bodyChunks=u.bodyChunks)}function ST(f,u){u=u.bootstrapChunks;for(var c=0;c')}function gO(f,u,c,y){switch(c.insertionMode){case 0:case 1:case 3:case 2:return f.push('