From 812288eb7226941dca9be848f7ab1c54a83ef3cd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 04:43:58 -0800 Subject: [PATCH 01/11] [internal] Add problem matcher for Zig --- .vscode/tasks.json | 93 +++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index faf1dc0d22400c..5ead1864250c4b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,50 +2,57 @@ "version": "2.0.0", "tasks": [ { - "type": "process", - "label": "Install Dependencies", - "command": "scripts/all-dependencies.sh", - "windows": { - "command": "scripts/all-dependencies.ps1", - }, - "icon": { - "id": "arrow-down", - }, - "options": { - "cwd": "${workspaceFolder}", - }, - }, - { - "type": "process", - "label": "Setup Environment", - "dependsOn": ["Install Dependencies"], - "command": "scripts/setup.sh", - "windows": { - "command": "scripts/setup.ps1", - }, - "icon": { - "id": "check", - }, - "options": { - "cwd": "${workspaceFolder}", - }, - }, - { - "type": "process", "label": "Build Bun", - "dependsOn": ["Setup Environment"], - "command": "bun", - "args": ["run", "build"], - "icon": { - "id": "gear", - }, - "options": { - "cwd": "${workspaceFolder}", - }, - "isBuildCommand": true, - "runOptions": { - "instanceLimit": 1, - "reevaluateOnRerun": true, + "type": "shell", + "command": "bun run build", + "group": { + "kind": "build", + "isDefault": true, + }, + "problemMatcher": [ + { + "owner": "zig", + "fileLocation": ["relative", "${workspaceFolder}"], + "pattern": [ + { + "regexp": "^(.+?):(\\d+):(\\d+): (error|warning|note): (.+)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5, + }, + { + "regexp": "^\\s+(.+)$", + "message": 1, + "loop": true, + }, + ], + }, + { + "owner": "clang", + "fileLocation": ["relative", "${workspaceFolder}"], + "pattern": [ + { + "regexp": "^([^:]+):(\\d+):(\\d+):\\s+(warning|error|note|remark):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5, + }, + { + "regexp": "^\\s*(.*)$", + "message": 1, + "loop": true, + }, + ], + }, + ], + "presentation": { + "reveal": "always", + "panel": "shared", + "clear": true, }, }, ], From 7f6bb308772bb1faa0debb1d6ccffd64c74ac443 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 04:59:04 -0800 Subject: [PATCH 02/11] Fixes #15403 --- src/node-fallbacks/path.js | 63 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/node-fallbacks/path.js b/src/node-fallbacks/path.js index c7977af8ebfe4c..66e4d05ea2e05f 100644 --- a/src/node-fallbacks/path.js +++ b/src/node-fallbacks/path.js @@ -3,5 +3,64 @@ * * Imported on usage in `bun build --target=browser` */ -export * from "path-browserify"; -export * as default from "path-browserify"; +import * as PathModule from "path-browserify"; + +const bindingPosix = PathModule; +const bindingWin32 = PathModule; + +// path-browserify doesn't implement toNamespacedPath +const toNamespacedPathPosix = function (a) { + return a; +}; +// path-browserify doesn't implement parse +const parseFn = function () { + throw new Error("Not implemented"); +}; + +bindingPosix.parse ??= parseFn; +bindingWin32.parse ??= parseFn; + +export const posix = { + resolve: bindingPosix.resolve.bind(bindingPosix), + normalize: bindingPosix.normalize.bind(bindingPosix), + isAbsolute: bindingPosix.isAbsolute.bind(bindingPosix), + join: bindingPosix.join.bind(bindingPosix), + relative: bindingPosix.relative.bind(bindingPosix), + toNamespacedPath: toNamespacedPathPosix, + dirname: bindingPosix.dirname.bind(bindingPosix), + basename: bindingPosix.basename.bind(bindingPosix), + extname: bindingPosix.extname.bind(bindingPosix), + format: bindingPosix.format.bind(bindingPosix), + parse: bindingPosix.parse.bind(bindingPosix), + sep: "/", + delimiter: ":", + win32: undefined, + posix: undefined, + _makeLong: toNamespacedPathPosix, +}; +export const win32 = { + sep: "\\", + delimiter: ";", + win32: undefined, + ...posix, + posix, +}; +posix.win32 = win32.win32 = win32; +posix.posix = posix; + +export default posix; +export const { + resolve, + normalize, + isAbsolute, + join, + relative, + toNamespacedPath, + dirname, + basename, + extname, + format, + parse, + sep, + delimiter, +} = posix; From 39af2a0a56ea9e64dd1e732f832aef98de934902 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 25 Nov 2024 12:43:46 -0800 Subject: [PATCH 03/11] Fix VSCode extension hanging (#15407) --- .vscode/launch.json | 3 ++ .../src/debugger/adapter.ts | 10 +++- .../src/inspector/node-socket.ts | 1 - .../src/inspector/websocket.ts | 3 +- packages/bun-vscode/src/features/debug.ts | 6 +-- .../src/features/diagnostics/diagnostics.ts | 52 +++++++++++++----- packages/bun-vscode/src/global-state.ts | 25 +++++++++ src/bun.js/javascript.zig | 53 ++++++++++++++----- 8 files changed, 120 insertions(+), 33 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 191c0a815e7639..00f72d4ddf531a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -224,8 +224,11 @@ "cwd": "${fileDirname}", "env": { "FORCE_COLOR": "1", + // "BUN_DEBUG_DEBUGGER": "1", + // "BUN_DEBUG_INTERNAL_DEBUGGER": "1", "BUN_DEBUG_QUIET_LOGS": "1", "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + // "BUN_INSPECT": "ws+unix:///var/folders/jk/8fzl9l5119598vsqrmphsw7m0000gn/T/tl15npi7qtf.sock?report=1", }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index 50af2bfa40d3ce..87bdedea0c4f15 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -294,7 +294,7 @@ export abstract class BaseDebugAdapter /** * Gets the inspector url. This is deprecated and exists for compat. - * @deprecated You should get the inspector directly, and if it's a WebSocketInspector you can access `.url` direclty. + * @deprecated You should get the inspector directly (with .getInspector()), and if it's a WebSocketInspector you can access `.url` direclty. */ get url(): string { // This code has been migrated from a time when the inspector was always a WebSocketInspector. @@ -305,6 +305,10 @@ export abstract class BaseDebugAdapter throw new Error("Inspector does not offer a URL"); } + public getInspector() { + return this.inspector; + } + abstract start(...args: unknown[]): Promise; /** @@ -2064,7 +2068,7 @@ export class NodeSocketDebugAdapter extends BaseDebugAdapter { +export class WebSocketDebugAdapter extends BaseDebugAdapter { #process?: ChildProcess; public constructor(url?: string | URL, untitledDocPath?: string, bunEvalPath?: string) { @@ -2331,6 +2335,8 @@ export class DebugAdapter extends BaseDebugAdapter { } } +export const DebugAdapter = WebSocketDebugAdapter; + function stoppedReason(reason: JSC.Debugger.PausedEvent["reason"]): DAP.StoppedEvent["reason"] { switch (reason) { case "Breakpoint": diff --git a/packages/bun-inspector-protocol/src/inspector/node-socket.ts b/packages/bun-inspector-protocol/src/inspector/node-socket.ts index 4cd108db82afa4..06bac6ac3c3ee0 100644 --- a/packages/bun-inspector-protocol/src/inspector/node-socket.ts +++ b/packages/bun-inspector-protocol/src/inspector/node-socket.ts @@ -35,7 +35,6 @@ export class NodeSocketInspector extends EventEmitter impleme this.#pendingResponses = new Map(); this.#framer = new SocketFramer(socket, message => { - // console.log(message); if (Array.isArray(message)) { for (const m of message) { this.#accept(m); diff --git a/packages/bun-inspector-protocol/src/inspector/websocket.ts b/packages/bun-inspector-protocol/src/inspector/websocket.ts index fbe26418f1c1bc..08be60537819bc 100644 --- a/packages/bun-inspector-protocol/src/inspector/websocket.ts +++ b/packages/bun-inspector-protocol/src/inspector/websocket.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; import { WebSocket } from "ws"; -import type { Inspector, InspectorEventMap } from "./index"; import type { JSC } from "../protocol"; +import type { Inspector, InspectorEventMap } from "./index"; /** * An inspector that communicates with a debugger over a WebSocket. @@ -170,6 +170,7 @@ export class WebSocketInspector extends EventEmitter implemen #accept(message: string): void { let data: JSC.Event | JSC.Response; + try { data = JSON.parse(message); } catch (cause) { diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts index 32c54f6a393400..ee7c2ca91fa0b1 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -4,11 +4,11 @@ import { join } from "node:path"; import * as vscode from "vscode"; import { type DAP, - DebugAdapter, getAvailablePort, getRandomId, TCPSocketSignal, UnixSignal, + WebSocketDebugAdapter, } from "../../../bun-debug-adapter-protocol"; export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = { @@ -239,7 +239,7 @@ class FileDebugSession extends DebugSession { // If these classes are moved/published, we should make sure // we remove these non-null assertions so consumers of // this lib are not running into these hard - adapter!: DebugAdapter; + adapter!: WebSocketDebugAdapter; sessionId?: string; untitledDocPath?: string; bunEvalPath?: string; @@ -263,7 +263,7 @@ class FileDebugSession extends DebugSession { : `ws+unix://${tmpdir()}/${uniqueId}.sock`; const { untitledDocPath, bunEvalPath } = this; - this.adapter = new DebugAdapter(url, untitledDocPath, bunEvalPath); + this.adapter = new WebSocketDebugAdapter(url, untitledDocPath, bunEvalPath); if (untitledDocPath) { this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => { diff --git a/packages/bun-vscode/src/features/diagnostics/diagnostics.ts b/packages/bun-vscode/src/features/diagnostics/diagnostics.ts index 03cc2b52473a56..931cf8d72b3110 100644 --- a/packages/bun-vscode/src/features/diagnostics/diagnostics.ts +++ b/packages/bun-vscode/src/features/diagnostics/diagnostics.ts @@ -1,15 +1,16 @@ import * as fs from "node:fs/promises"; -import { Socket } from "node:net"; import * as os from "node:os"; +import { inspect } from "node:util"; import * as vscode from "vscode"; import { getAvailablePort, - NodeSocketDebugAdapter, + getRandomId, TCPSocketSignal, UnixSignal, + WebSocketDebugAdapter, } from "../../../../bun-debug-adapter-protocol"; import type { JSC } from "../../../../bun-inspector-protocol"; -import { typedGlobalState } from "../../global-state"; +import { createGlobalStateGenerationFn, typedGlobalState } from "../../global-state"; const output = vscode.window.createOutputChannel("Bun - Diagnostics"); @@ -69,8 +70,9 @@ class BunDiagnosticsManager { private readonly editorState: EditorStateManager; private readonly signal: UnixSignal | TCPSocketSignal; private readonly context: vscode.ExtensionContext; + public readonly inspectUrl: string; - public get signalUrl() { + public get notifyUrl() { return this.signal.url; } @@ -122,19 +124,30 @@ class BunDiagnosticsManager { } } + private static getOrCreateOldVersionInspectURL = createGlobalStateGenerationFn( + "DIAGNOSTICS_BUN_INSPECT", + async () => { + const url = + process.platform === "win32" + ? `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}` + : `ws+unix://${os.tmpdir()}/${getRandomId()}.sock`; + + return url; + }, + ); + public static async initialize(context: vscode.ExtensionContext) { const signal = await BunDiagnosticsManager.getOrRecreateSignal(context); + const oldVersionInspectURL = await BunDiagnosticsManager.getOrCreateOldVersionInspectURL(context.globalState); - await signal.ready; - - return new BunDiagnosticsManager(context, signal); + return new BunDiagnosticsManager(context, signal, oldVersionInspectURL); } /** * Called when Bun pings BUN_INSPECT_NOTIFY (indicating a program has started). */ - private async handleSocketConnection(socket: Socket) { - const debugAdapter = new NodeSocketDebugAdapter(socket); + private async handleSocketConnection() { + const debugAdapter = new WebSocketDebugAdapter(this.inspectUrl); this.editorState.clearAll("A new socket connected"); @@ -146,6 +159,10 @@ class BunDiagnosticsManager { output.appendLine(`Received inspector event: ${e.method}`); }); + debugAdapter.on("Inspector.error", e => { + output.appendLine(inspect(e, true, null)); + }); + debugAdapter.on("LifecycleReporter.error", event => this.handleLifecycleError(event)); const ok = await debugAdapter.start(); @@ -203,8 +220,6 @@ class BunDiagnosticsManager { const [line = null, col = null] = event.lineColumns.slice(i * 2, i * 2 + 2); - output.appendLine(`Adding related information for ${url} at ${line}:${col}`); - if (line === null || col === null) { return []; } @@ -231,10 +246,15 @@ class BunDiagnosticsManager { }); } - private constructor(context: vscode.ExtensionContext, signal: UnixSignal | TCPSocketSignal) { + private constructor( + context: vscode.ExtensionContext, + signal: UnixSignal | TCPSocketSignal, + oldVersionInspectURL: string, + ) { this.editorState = new EditorStateManager(); this.signal = signal; this.context = context; + this.inspectUrl = oldVersionInspectURL; this.context.subscriptions.push( // on did type @@ -243,7 +263,9 @@ class BunDiagnosticsManager { }), ); - this.signal.on("Signal.Socket.connect", this.handleSocketConnection.bind(this)); + this.signal.on("Signal.received", () => { + this.handleSocketConnection(); + }); } } @@ -255,7 +277,9 @@ export async function registerDiagnosticsSocket(context: vscode.ExtensionContext context.environmentVariableCollection.description = description; const manager = await BunDiagnosticsManager.initialize(context); - context.environmentVariableCollection.replace("BUN_INSPECT_NOTIFY", manager.signalUrl); + + context.environmentVariableCollection.replace("BUN_INSPECT_NOTIFY", manager.notifyUrl); + context.environmentVariableCollection.replace("BUN_INSPECT", `${manager.inspectUrl}?report=1?wait=1`); // Intentionally invalid query params context.subscriptions.push(manager); } diff --git a/packages/bun-vscode/src/global-state.ts b/packages/bun-vscode/src/global-state.ts index f0b7756ba1026f..87bda9659bb796 100644 --- a/packages/bun-vscode/src/global-state.ts +++ b/packages/bun-vscode/src/global-state.ts @@ -1,5 +1,7 @@ import { ExtensionContext } from "vscode"; +export const GLOBAL_STATE_VERSION = 1; + export type GlobalStateTypes = { BUN_INSPECT_NOTIFY: | { @@ -10,8 +12,16 @@ export type GlobalStateTypes = { type: "unix"; url: string; }; + + DIAGNOSTICS_BUN_INSPECT: string; }; +export async function clearGlobalState(gs: ExtensionContext["globalState"]) { + const tgs = typedGlobalState(gs); + + await Promise.all(tgs.keys().map(key => tgs.update(key, undefined as never))); +} + export function typedGlobalState(state: ExtensionContext["globalState"]) { return state as { get(key: K): GlobalStateTypes[K] | undefined; @@ -37,4 +47,19 @@ export function typedGlobalState(state: ExtensionContext["globalState"]) { }; } +export function createGlobalStateGenerationFn( + key: T, + resolve: () => Promise, +) { + return async (gs: ExtensionContext["globalState"]) => { + const value = (gs as TypedGlobalState).get(key); + if (value) return value; + + const next = await resolve(); + await (gs as TypedGlobalState).update(key, next); + + return next; + }; +} + export type TypedGlobalState = ReturnType; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 9b78740a1f8570..bd2197511ca156 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1560,7 +1560,8 @@ pub const VirtualMachine = struct { script_execution_context_id: u32 = 0, next_debugger_id: u64 = 1, poll_ref: Async.KeepAlive = .{}, - wait_for_connection: bool = false, + wait_for_connection: Wait = .off, + // wait_for_connection: bool = false, set_breakpoint_on_first_line: bool = false, mode: enum { /// Bun acts as the server. https://debug.bun.sh/ uses this @@ -1573,6 +1574,8 @@ pub const VirtualMachine = struct { lifecycle_reporter_agent: LifecycleAgent = .{}, must_block_until_connected: bool = false, + pub const Wait = enum { off, shortly, forever }; + pub const log = Output.scoped(.debugger, false); extern "C" fn Bun__createJSDebugger(*JSC.JSGlobalObject) u32; @@ -1597,11 +1600,24 @@ pub const VirtualMachine = struct { .duration_ns = @truncate(@as(u128, @intCast(std.time.nanoTimestamp() - bun.CLI.start_time))), }}); - Bun__ensureDebugger(debugger.script_execution_context_id, debugger.wait_for_connection); - while (debugger.wait_for_connection) { + Bun__ensureDebugger(debugger.script_execution_context_id, debugger.wait_for_connection != .off); + var deadline: bun.timespec = if (debugger.wait_for_connection == .shortly) bun.timespec.now().addMs(30) else undefined; + + while (debugger.wait_for_connection != .off) { this.eventLoop().tick(); - if (debugger.wait_for_connection) - this.eventLoop().autoTickActive(); + switch (debugger.wait_for_connection) { + .forever => { + this.eventLoop().autoTickActive(); + }, + .shortly => { + this.uwsLoop().tickWithTimeout(&deadline); + if (bun.timespec.now().order(&deadline) != .lt) { + log("Timed out waiting for the debugger", .{}); + break; + } + }, + .off => {}, + } } } @@ -1624,7 +1640,7 @@ pub const VirtualMachine = struct { } this.eventLoop().ensureWaker(); - if (debugger.wait_for_connection) { + if (debugger.wait_for_connection != .off) { debugger.poll_ref.ref(this); debugger.must_block_until_connected = true; } @@ -1654,8 +1670,8 @@ pub const VirtualMachine = struct { pub export fn Debugger__didConnect() void { var this = VirtualMachine.get(); - if (this.debugger.?.wait_for_connection) { - this.debugger.?.wait_for_connection = false; + if (this.debugger.?.wait_for_connection != .off) { + this.debugger.?.wait_for_connection = .off; this.debugger.?.poll_ref.unref(this); } } @@ -1999,8 +2015,21 @@ pub const VirtualMachine = struct { } const notify = bun.getenvZ("BUN_INSPECT_NOTIFY") orelse ""; const unix = bun.getenvZ("BUN_INSPECT") orelse ""; - const set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1"); - const wait_for_connection = set_breakpoint_on_first_line or (unix.len > 0 and strings.endsWith(unix, "?wait=1")); + + 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 (either 30ms if report is passed, forever otherwise) for the debugger to connect + const report = unix.len > 0 and strings.includes(unix, "?report=1"); // If either `break=1` or `wait=1` are specified, passing this will make the wait be 30ms and act like it's reporting to clients like the VSCode extension + + // NOTE: + // It's possible (and likely!) that the unix url will end like `?report=1?wait=1`. + // This is done because we needed to support the BUN_INSPECT url in versions of bun before we introduced `report=1` mode. + // Report mode is used for the VSCode extension (and other clients), it just tells bun to timeout connecting quickly rather + // than waiting forever. + + const wait_for_connection: Debugger.Wait = switch (set_breakpoint_on_first_line or wait_for_debugger) { + true => if (report) .shortly else .forever, + false => .off, + }; switch (cli_flag) { .unspecified => { @@ -2015,7 +2044,7 @@ pub const VirtualMachine = struct { this.debugger = Debugger{ .path_or_port = null, .from_environment_variable = notify, - .wait_for_connection = true, + .wait_for_connection = wait_for_connection, .set_breakpoint_on_first_line = set_breakpoint_on_first_line, .mode = .connect, }; @@ -2025,7 +2054,7 @@ pub const VirtualMachine = struct { this.debugger = Debugger{ .path_or_port = cli_flag.enable.path_or_port, .from_environment_variable = unix, - .wait_for_connection = wait_for_connection or cli_flag.enable.wait_for_connection, + .wait_for_connection = if (cli_flag.enable.wait_for_connection) .forever else wait_for_connection, .set_breakpoint_on_first_line = set_breakpoint_on_first_line or cli_flag.enable.set_breakpoint_on_first_line, }; }, From a6f37b398c3d5e1a52f69051ac0510e9ffdb2717 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 12:58:30 -0800 Subject: [PATCH 04/11] Fix bug with --eval & --print (#15379) --- .../bindings/ExposeNodeModuleGlobals.cpp | 69 ++++++++++++------- src/bun.js/bindings/ZigGlobalObject.cpp | 28 ++++---- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp index d8723e2ba9a731..a99b6624881755 100644 --- a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp +++ b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp @@ -48,38 +48,38 @@ v(worker_threads, Bun::InternalModuleRegistry::NodeWorkerThreads) \ v(zlib, Bun::InternalModuleRegistry::NodeZlib) \ -#define FOREACH_EXPOSED_BUILTIN_NATIVE(v) \ - v(constants, SyntheticModuleType::NodeConstants) \ - v(string_decoder, SyntheticModuleType::NodeStringDecoder) \ - v(buffer, SyntheticModuleType::NodeBuffer) \ - v(jsc, SyntheticModuleType::BunJSC) \ + namespace ExposeNodeModuleGlobalGetters { #define DECL_GETTER(id, field) \ JSC_DEFINE_CUSTOM_GETTER(id, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) \ { \ - Zig::GlobalObject* thisObject = JSC::jsCast(lexicalGlobalObject); \ + Zig::GlobalObject* thisObject = defaultGlobalObject(lexicalGlobalObject); \ JSC::VM& vm = thisObject->vm(); \ return JSC::JSValue::encode(thisObject->internalModuleRegistry()->requireId(thisObject, vm, field)); \ } FOREACH_EXPOSED_BUILTIN_IMR(DECL_GETTER) -#undef DECL_GETTER +#undef DECL_GETTER -#define DECL_GETTER(id, field) \ - JSC_DEFINE_CUSTOM_GETTER(id, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) \ - { \ - Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); \ - JSC::VM& vm = globalObject->vm(); \ - auto& builtinNames = WebCore::builtinNames(vm); \ - JSC::JSFunction* function = jsCast(globalObject->getDirect(vm, builtinNames.requireNativeModulePrivateName())); \ - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); \ - arguments.append(JSC::jsString(vm, WTF::String(#id##_s))); \ - auto callData = JSC::getCallData(function); \ - return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); \ - } -FOREACH_EXPOSED_BUILTIN_NATIVE(DECL_GETTER) -#undef DECL_GETTER + +JSC_DEFINE_CUSTOM_GETTER(jsCustomGetterGetNativeModule, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName propertyName)) +{ + Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject); + JSC::VM& vm = globalObject->vm(); + + JSC::JSValue key = JSC::identifierToJSValue(vm, propertyName == "jsc"_s ? JSC::Identifier::fromString(vm, "bun:jsc"_s) : JSC::Identifier::fromUid(vm, propertyName.uid())); + JSC::JSValue result = globalObject->requireMap()->get(globalObject, key); + if (!result || result.isUndefinedOrNull()) { + auto& builtinNames = WebCore::builtinNames(vm); + JSC::JSFunction* function = jsCast(globalObject->getDirect(vm, builtinNames.requireNativeModulePrivateName())); + JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); + arguments.append(key); + auto callData = JSC::getCallData(function); + return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); + } + return JSC::JSValue::encode(result); +} } // namespace ExposeNodeModuleGlobalGetters @@ -95,11 +95,32 @@ extern "C" void Bun__ExposeNodeModuleGlobals(Zig::GlobalObject* globalObject) vm, \ ExposeNodeModuleGlobalGetters::id, \ nullptr), \ - 0 | JSC::PropertyAttribute::CustomAccessorOrValue \ + 0 | JSC::PropertyAttribute::CustomValue \ ); FOREACH_EXPOSED_BUILTIN_IMR(PUT_CUSTOM_GETTER_SETTER) - // FOREACH_EXPOSED_BUILTIN_NATIVE(PUT_CUSTOM_GETTER_SETTER) #undef PUT_CUSTOM_GETTER_SETTER -} + + JSC::CustomGetterSetter *nativeModuleGetter = JSC::CustomGetterSetter::create( + vm, + ExposeNodeModuleGlobalGetters::jsCustomGetterGetNativeModule, + nullptr + ); + + static constexpr ASCIILiteral nativeModuleNames[] = { + "constants"_s, + "string_decoder"_s, + "buffer"_s, + "jsc"_s, + }; + + for (auto name : nativeModuleNames) { + globalObject->putDirectCustomAccessor( + vm, + JSC::Identifier::fromString(vm, name), + nativeModuleGetter, + 0 | JSC::PropertyAttribute::CustomValue + ); + } +} \ No newline at end of file diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 2080aa324c95fa..671158126ea6c4 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3658,20 +3658,20 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) NoIntrinsic, PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete | 0); - putDirectCustomAccessor(vm, static_cast(vm.clientData)->builtinNames().BufferPrivateName(), JSC::CustomGetterSetter::create(vm, JSBuffer_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.lazyStreamPrototypeMapPrivateName(), JSC::CustomGetterSetter::create(vm, functionLazyLoadStreamPrototypeMap_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.TransformStreamPrivateName(), CustomGetterSetter::create(vm, TransformStream_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.TransformStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, TransformStreamDefaultController_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableByteStreamControllerPrivateName(), CustomGetterSetter::create(vm, ReadableByteStreamController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableStreamPrivateName(), CustomGetterSetter::create(vm, ReadableStream_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableStreamBYOBReaderPrivateName(), CustomGetterSetter::create(vm, ReadableStreamBYOBReader_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableStreamBYOBRequestPrivateName(), CustomGetterSetter::create(vm, ReadableStreamBYOBRequest_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, ReadableStreamDefaultController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.ReadableStreamDefaultReaderPrivateName(), CustomGetterSetter::create(vm, ReadableStreamDefaultReader_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.WritableStreamPrivateName(), CustomGetterSetter::create(vm, WritableStream_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.WritableStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, WritableStreamDefaultController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.WritableStreamDefaultWriterPrivateName(), CustomGetterSetter::create(vm, WritableStreamDefaultWriter_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomAccessorOrValue); - putDirectCustomAccessor(vm, builtinNames.AbortSignalPrivateName(), CustomGetterSetter::create(vm, AbortSignal_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessorOrValue); + putDirectCustomAccessor(vm, static_cast(vm.clientData)->builtinNames().BufferPrivateName(), JSC::CustomGetterSetter::create(vm, JSBuffer_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.lazyStreamPrototypeMapPrivateName(), JSC::CustomGetterSetter::create(vm, functionLazyLoadStreamPrototypeMap_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.TransformStreamPrivateName(), CustomGetterSetter::create(vm, TransformStream_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.TransformStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, TransformStreamDefaultController_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableByteStreamControllerPrivateName(), CustomGetterSetter::create(vm, ReadableByteStreamController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableStreamPrivateName(), CustomGetterSetter::create(vm, ReadableStream_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableStreamBYOBReaderPrivateName(), CustomGetterSetter::create(vm, ReadableStreamBYOBReader_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableStreamBYOBRequestPrivateName(), CustomGetterSetter::create(vm, ReadableStreamBYOBRequest_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, ReadableStreamDefaultController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.ReadableStreamDefaultReaderPrivateName(), CustomGetterSetter::create(vm, ReadableStreamDefaultReader_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.WritableStreamPrivateName(), CustomGetterSetter::create(vm, WritableStream_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.WritableStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, WritableStreamDefaultController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.WritableStreamDefaultWriterPrivateName(), CustomGetterSetter::create(vm, WritableStreamDefaultWriter_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); + putDirectCustomAccessor(vm, builtinNames.AbortSignalPrivateName(), CustomGetterSetter::create(vm, AbortSignal_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomValue); // ----- Public Properties ----- From bb3d570ad03dde930ce3e8ba10299c7626140f1e Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 25 Nov 2024 15:19:02 -0800 Subject: [PATCH 05/11] zig: assert there is an exception when .zero is returned (#15362) Co-authored-by: Jarred Sumner --- src/bun.js/api/BunObject.zig | 42 ++++++++--------- src/bun.js/api/JSBundler.zig | 2 +- src/bun.js/api/bun/h2_frame_parser.zig | 2 +- src/bun.js/api/bun/socket.zig | 2 + src/bun.js/api/filesystem_router.zig | 2 +- src/bun.js/api/glob.zig | 12 ++--- src/bun.js/api/server.zig | 2 +- src/bun.js/bindings/bindings.zig | 27 +++++++++-- src/bun.js/node/node_os.zig | 21 ++++----- src/bun.js/test/expect.zig | 47 ++++++++++--------- src/bun.js/webcore.zig | 6 +-- src/bun.js/webcore/blob.zig | 2 +- src/bun.js/webcore/streams.zig | 63 ++++++++++---------------- src/codegen/generate-classes.ts | 15 ++---- src/codegen/generate-js2native.ts | 5 +- src/css/css_internals.zig | 22 ++++----- src/css/values/color_js.zig | 8 ++-- src/install/semver.zig | 9 ++-- src/patch.zig | 12 ++--- src/shell/interpreter.zig | 13 +++--- src/shell/shell.zig | 32 ++++++------- 21 files changed, 167 insertions(+), 179 deletions(-) diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index ba1b1636aa19b0..f89c6f08b271e0 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -263,7 +263,7 @@ pub fn shellEscape(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b const arguments = callframe.arguments_old(1); if (arguments.len < 1) { globalThis.throw("shell escape expected at least 1 argument", .{}); - return .undefined; + return .zero; } const jsval = arguments.ptr[0]; @@ -277,11 +277,11 @@ pub fn shellEscape(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b if (bun.shell.needsEscapeBunstr(bunstr)) { const result = bun.shell.escapeBunStr(bunstr, &outbuf, true) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; if (!result) { globalThis.throw("String has invalid utf-16: {s}", .{bunstr.byteSlice()}); - return .undefined; + return .zero; } var str = bun.String.createUTF8(outbuf.items[0..]); return str.transferToJS(globalThis); @@ -297,7 +297,7 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS const brace_str_js = arguments.nextEat() orelse { globalThis.throw("braces: expected at least 1 argument, got 0", .{}); - return .undefined; + return .zero; }; const brace_str = brace_str_js.toBunString(globalThis); defer brace_str.deref(); @@ -337,7 +337,7 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS if (tokenize) { const str = std.json.stringifyAlloc(globalThis.bunVM().allocator, lexer_output.tokens.items[0..], .{}) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; defer globalThis.bunVM().allocator.free(str); var bun_str = bun.String.fromBytes(str); @@ -350,7 +350,7 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS }; const str = std.json.stringifyAlloc(globalThis.bunVM().allocator, ast_node, .{}) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; defer globalThis.bunVM().allocator.free(str); var bun_str = bun.String.fromBytes(str); @@ -363,7 +363,7 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS var expanded_strings = arena.allocator().alloc(std.ArrayList(u8), expansion_count) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; for (0..expansion_count) |i| { @@ -377,12 +377,12 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS lexer_output.contains_nested, ) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; var out_strings = arena.allocator().alloc(bun.String, expansion_count) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; for (0..expansion_count) |i| { out_strings[i] = bun.String.fromBytes(expanded_strings[i].items[0..]); @@ -398,7 +398,7 @@ pub fn which(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE defer arguments.deinit(); const path_arg = arguments.nextEat() orelse { globalThis.throw("which: expected 1 argument, got 0", .{}); - return .undefined; + return .zero; }; var path_str: ZigString.Slice = ZigString.Slice.empty; @@ -421,7 +421,7 @@ pub fn which(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE if (bin_str.len >= bun.MAX_PATH_BYTES) { globalThis.throw("bin path is too long", .{}); - return .undefined; + return .zero; } if (bin_str.len == 0) { @@ -611,7 +611,7 @@ pub fn registerMacro(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFram if (!arguments[1].isCell() or !arguments[1].isCallable(globalObject.vm())) { // TODO: add "toTypeOf" helper globalObject.throw("Macro must be a function", .{}); - return .undefined; + return .zero; } const get_or_put_result = VirtualMachine.get().macros.getOrPut(id) catch unreachable; @@ -755,7 +755,7 @@ pub fn openInEditor(globalThis: js.JSContextRef, callframe: *JSC.CallFrame) bun. if (editor_choice == null) { edit.* = prev; globalThis.throw("Could not find editor \"{s}\"", .{sliced.slice()}); - return .undefined; + return .zero; } else if (edit.name.ptr == edit.path.ptr) { edit.name = arguments.arena.allocator().dupe(u8, edit.path) catch unreachable; edit.path = edit.path; @@ -858,7 +858,7 @@ pub fn sleepSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b // The argument must be a number if (!arg.isNumber()) { globalObject.throwInvalidArgumentType("sleepSync", "milliseconds", "number"); - return .undefined; + return .zero; } //NOTE: if argument is > max(i32) then it will be truncated @@ -2078,7 +2078,7 @@ pub const Crypto = struct { .err => { const error_instance = value.toErrorInstance(globalObject); globalObject.throwValue(error_instance); - return JSC.JSValue.undefined; + return .zero; }, .pass => |pass| { return JSC.JSValue.jsBoolean(pass); @@ -2316,7 +2316,7 @@ pub const Crypto = struct { if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) { if (!arguments[2].isString()) { globalObject.throwInvalidArgumentType("verify", "algorithm", "string"); - return JSC.JSValue.undefined; + return .zero; } const algorithm_string = arguments[2].getZigString(globalObject); @@ -2325,7 +2325,7 @@ pub const Crypto = struct { if (!globalObject.hasException()) { globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message); } - return JSC.JSValue.undefined; + return .zero; }; } @@ -2333,7 +2333,7 @@ pub const Crypto = struct { if (!globalObject.hasException()) { globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); } - return JSC.JSValue.undefined; + return .zero; }; var hash_ = JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { @@ -2341,7 +2341,7 @@ pub const Crypto = struct { if (!globalObject.hasException()) { globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); } - return JSC.JSValue.undefined; + return .zero; }; defer password.deinit(); @@ -3225,7 +3225,7 @@ pub export fn Bun__escapeHTML16(globalObject: *JSC.JSGlobalObject, input_value: const input_slice = ptr[0..len]; const escaped = strings.escapeHTMLForUTF16Input(globalObject.bunVM().allocator, input_slice) catch { globalObject.vm().throwError(globalObject, bun.String.static("Out of memory").toJS(globalObject)); - return .undefined; + return .zero; }; switch (escaped) { @@ -3248,7 +3248,7 @@ pub export fn Bun__escapeHTML8(globalObject: *JSC.JSGlobalObject, input_value: J const escaped = strings.escapeHTMLForLatin1Input(allocator, input_slice) catch { globalObject.vm().throwError(globalObject, bun.String.static("Out of memory").toJS(globalObject)); - return .undefined; + return .zero; }; switch (escaped) { diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 45bd817209e859..9a7a276d76f410 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -858,7 +858,7 @@ pub const JSBundler = struct { ) JSValue { if (this.called_defer) { globalObject.throw("can't call .defer() more than once within an onLoad plugin", .{}); - return .undefined; + return .zero; } this.called_defer = true; diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 3c5fd21b8e59ff..5f99735d252752 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3254,7 +3254,7 @@ pub const H2FrameParser = struct { const args_list = callframe.arguments_old(1); if (args_list.len < 1) { globalObject.throw("Expected error argument", .{}); - return .undefined; + return .zero; } var it = StreamResumableIterator.init(this); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 2865d920744030..5aeb892712623e 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -3465,11 +3465,13 @@ fn NewSocket(comptime ssl: bool) type { // If BoringSSL gave us an error code, let's use it. if (err != 0 and !globalObject.hasException()) { globalObject.throwValue(BoringSSL.ERR_toJS(globalObject, err)); + return .zero; } // If BoringSSL did not give us an error code, let's throw a generic error. if (!globalObject.hasException()) { globalObject.throw("Failed to upgrade socket from TCP -> TLS. Is the TLS config correct?", .{}); + return .zero; } return JSValue.jsUndefined(); diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig index 2baa031f5cf199..5f45a118dad0a3 100644 --- a/src/bun.js/api/filesystem_router.zig +++ b/src/bun.js/api/filesystem_router.zig @@ -287,7 +287,7 @@ pub const FileSystemRouter = struct { const url_path = URLPath.parse(path.slice()) catch |err| { globalThis.throw("{s} parsing path: {s}", .{ @errorName(err), path.slice() }); - return JSValue.zero; + return .zero; }; var params = Router.Param.List{}; defer params.deinit(globalThis.allocator()); diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index 3c5115ac97a1f9..b40b005f011247 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -113,7 +113,7 @@ const ScanOpts = struct { return out; } globalThis.throw("{s}: expected first argument to be an object", .{fnName}); - return null; + return error.JSError; } if (try optsObj.getTruthy(globalThis, "onlyFiles")) |only_files| { @@ -135,7 +135,7 @@ const ScanOpts = struct { if (try optsObj.getTruthy(globalThis, "cwd")) |cwdVal| { if (!cwdVal.isString()) { globalThis.throw("{s}: invalid `cwd`, not a string", .{fnName}); - return null; + return error.JSError; } { @@ -428,12 +428,12 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame defer arguments.deinit(); const str_arg = arguments.nextEat() orelse { globalThis.throw("Glob.matchString: expected 1 arguments, got 0", .{}); - return .undefined; + return .zero; }; if (!str_arg.isString()) { globalThis.throw("Glob.matchString: first argument is not a string", .{}); - return .undefined; + return .zero; } var str = str_arg.toSlice(globalThis, arena.allocator()); @@ -446,13 +446,13 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame var codepoints = std.ArrayList(u32).initCapacity(alloc, this.pattern.len * 2) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; errdefer codepoints.deinit(); convertUtf8(&codepoints, this.pattern) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; this.pattern_codepoints = codepoints; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 12ba5cd8e9109e..87ab2838a3ea0c 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5070,7 +5070,7 @@ pub const ServerWebSocket = struct { if (result.isAnyError()) { globalThis.throwValue(result); - return JSValue.jsUndefined(); + return .zero; } return result; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index af62ff0b897e48..63b462787b494f 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -737,7 +737,7 @@ pub const ZigString = extern struct { if (len > String.max_length()) { bun.default_allocator.free(ptr[0..len]); global.ERR_STRING_TOO_LONG("Cannot create a string longer than 2^32-1 characters", .{}).throw(); - return JSValue.zero; + return .zero; } return shim.cppFn("toExternalU16", .{ ptr, len, global }); } @@ -5723,8 +5723,10 @@ pub const JSValue = enum(i64) { } /// same as `JSValue.deepEquals`, but with jest asymmetric matchers enabled - pub fn jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool { - return cppFn("jestDeepEquals", .{ this, other, global }); + pub fn jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bun.JSError!bool { + const result = cppFn("jestDeepEquals", .{ this, other, global }); + if (global.hasException()) return error.JSError; + return result; } pub fn strictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool { @@ -6483,7 +6485,7 @@ pub const VM = extern struct { // TODO: rewrite all `throwError` to use `JSError` pub fn throwError2(vm: *VM, global_object: *JSGlobalObject, value: JSValue) JSError { vm.throwError(global_object, value); - return JSError.JSError; + return error.JSError; } pub fn releaseWeakRefs(vm: *VM) void { @@ -6770,6 +6772,14 @@ pub fn toJSHostFunction(comptime Function: JSHostZigFunction) JSC.JSHostFunction globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(JSC.conv) JSC.JSValue { + if (bun.Environment.allow_assert and bun.Environment.is_canary) { + const value = Function(globalThis, callframe) catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; + bun.assert((value == .zero) == globalThis.hasException()); + return value; + } return @call(.always_inline, Function, .{ globalThis, callframe }) catch |err| switch (err) { error.JSError => .zero, error.OutOfMemory => globalThis.throwOutOfMemoryValue(), @@ -6778,8 +6788,15 @@ pub fn toJSHostFunction(comptime Function: JSHostZigFunction) JSC.JSHostFunction }.function; } -// XXX: temporary pub fn toJSHostValue(globalThis: *JSGlobalObject, value: error{ OutOfMemory, JSError }!JSValue) JSValue { + if (bun.Environment.allow_assert and bun.Environment.is_canary) { + const normal = value catch |err| switch (err) { + error.JSError => .zero, + error.OutOfMemory => globalThis.throwOutOfMemoryValue(), + }; + bun.assert((normal == .zero) == globalThis.hasException()); + return normal; + } return value catch |err| switch (err) { error.JSError => .zero, error.OutOfMemory => globalThis.throwOutOfMemoryValue(), diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 5ae74ade311737..7d8ffc223ffcc4 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -66,7 +66,7 @@ pub const OS = struct { }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); - return .undefined; + return .zero; }; } @@ -313,11 +313,8 @@ pub const OS = struct { const arguments: []const JSC.JSValue = args_.ptr[0..args_.len]; if (arguments.len > 0 and !arguments[0].isNumber()) { - globalThis.ERR_INVALID_ARG_TYPE( - "getPriority() expects a number", - .{}, - ).throw(); - return .undefined; + globalThis.ERR_INVALID_ARG_TYPE("getPriority() expects a number", .{}).throw(); + return .zero; } const pid = if (arguments.len > 0) arguments[0].asInt32() else 0; @@ -339,7 +336,7 @@ pub const OS = struct { }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); - return .undefined; + return .zero; } return JSC.JSValue.jsNumberFromInt32(priority); @@ -422,7 +419,7 @@ pub const OS = struct { }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); - return .undefined; + return .zero; } defer C.freeifaddrs(interface_start); @@ -733,7 +730,7 @@ pub const OS = struct { globalThis, ); globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } const pid = if (arguments.len == 2) arguments[0].coerce(i32, globalThis) else 0; @@ -747,7 +744,7 @@ pub const OS = struct { globalThis, ); globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } const errcode = C.setProcessPriority(pid, priority); @@ -762,7 +759,7 @@ pub const OS = struct { }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); - return .undefined; + return .zero; }, .ACCES => { const err = JSC.SystemError{ @@ -774,7 +771,7 @@ pub const OS = struct { }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); - return .undefined; + return .zero; }, else => {}, } diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 92eab545e0991a..34fc02a4b5037e 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -677,7 +677,7 @@ pub const Expect = struct { var itr = list_value.arrayIterator(globalThis); while (itr.next()) |item| { // Confusingly, jest-extended uses `deepEqual`, instead of `toBe` - if (item.jestDeepEquals(expected, globalThis)) { + if (try item.jestDeepEquals(expected, globalThis)) { pass = true; break; } @@ -697,7 +697,7 @@ pub const Expect = struct { ) callconv(.C) void { const entry = bun.cast(*ExpectedEntry, entry_.?); // Confusingly, jest-extended uses `deepEqual`, instead of `toBe` - if (item.jestDeepEquals(entry.expected, entry.globalThis)) { + if (item.jestDeepEquals(entry.expected, entry.globalThis) catch return) { entry.pass.* = true; // TODO(perf): break out of the `forEach` when a match is found } @@ -991,7 +991,7 @@ pub const Expect = struct { var i: u32 = 0; while (i < count) : (i += 1) { const key = expected.getIndex(globalObject, i); - if (item.jestDeepEquals(key, globalObject)) break; + if (try item.jestDeepEquals(key, globalObject)) break; } else break :outer; } pass = true; @@ -1114,7 +1114,7 @@ pub const Expect = struct { const values = value.values(globalObject); var itr = values.arrayIterator(globalObject); while (itr.next()) |item| { - if (item.jestDeepEquals(expected, globalObject)) { + if (try item.jestDeepEquals(expected, globalObject)) { pass = true; break; } @@ -1179,7 +1179,7 @@ pub const Expect = struct { var i: u32 = 0; while (i < count) : (i += 1) { const key = values.getIndex(globalObject, i); - if (key.jestDeepEquals(item, globalObject)) break; + if (try key.jestDeepEquals(item, globalObject)) break; } else { pass = false; break; @@ -1247,7 +1247,7 @@ pub const Expect = struct { var i: u32 = 0; while (i < count) : (i += 1) { const key = values.getIndex(globalObject, i); - if (key.jestDeepEquals(item, globalObject)) { + if (try key.jestDeepEquals(item, globalObject)) { pass = true; break; } @@ -1317,7 +1317,7 @@ pub const Expect = struct { var i: u32 = 0; while (i < count) : (i += 1) { const key = values.getIndex(globalObject, i); - if (key.jestDeepEquals(item, globalObject)) { + if (try key.jestDeepEquals(item, globalObject)) { pass = true; break :outer; } @@ -1382,7 +1382,7 @@ pub const Expect = struct { if (value_type.isArrayLike()) { var itr = value.arrayIterator(globalThis); while (itr.next()) |item| { - if (item.jestDeepEquals(expected, globalThis)) { + if (try item.jestDeepEquals(expected, globalThis)) { pass = true; break; } @@ -1420,7 +1420,7 @@ pub const Expect = struct { item: JSValue, ) callconv(.C) void { const entry = bun.cast(*ExpectedEntry, entry_.?); - if (item.jestDeepEquals(entry.expected, entry.globalThis)) { + if (item.jestDeepEquals(entry.expected, entry.globalThis) catch return) { entry.pass.* = true; // TODO(perf): break out of the `forEach` when a match is found } @@ -1657,7 +1657,7 @@ pub const Expect = struct { const value: JSValue = try this.getValue(globalThis, thisValue, "toEqual", "expected"); const not = this.flags.not; - var pass = value.jestDeepEquals(expected, globalThis); + var pass = try value.jestDeepEquals(expected, globalThis); if (not) pass = !pass; if (pass) return .undefined; @@ -1755,7 +1755,7 @@ pub const Expect = struct { } if (pass and expected_property != null) { - pass = received_property.jestDeepEquals(expected_property.?, globalThis); + pass = try received_property.jestDeepEquals(expected_property.?, globalThis); } if (not) pass = !pass; @@ -2250,19 +2250,23 @@ pub const Expect = struct { pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); + const vm = globalThis.bunVM(); const thisValue = callFrame.this(); - const _arguments = callFrame.arguments_old(1); - const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + const arguments = callFrame.argumentsAsArray(1); incrementExpectCallCounter(); - const expected_value: JSValue = if (arguments.len > 0) brk: { + const expected_value: JSValue = brk: { + if (callFrame.argumentsCount() == 0) { + break :brk .zero; + } const value = arguments[0]; - if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) { + if (value.isUndefinedOrNull() or !value.isObject() and !value.isString()) { var fmt = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; globalThis.throw("Expected value must be string or Error: {any}", .{value.toFmt(&fmt)}); return .zero; - } else if (value.isObject()) { + } + if (value.isObject()) { if (ExpectAny.fromJSDirect(value)) |_| { if (ExpectAny.constructorValueGetCached(value)) |innerConstructorValue| { break :brk innerConstructorValue; @@ -2270,7 +2274,7 @@ pub const Expect = struct { } } break :brk value; - } else .zero; + }; expected_value.ensureStillAlive(); const value: JSValue = try this.getValue(globalThis, thisValue, "toThrow", "expected"); @@ -2288,7 +2292,6 @@ pub const Expect = struct { return .zero; } - var vm = globalThis.bunVM(); var return_value: JSValue = .zero; // Drain existing unhandled rejections @@ -4174,7 +4177,7 @@ pub const Expect = struct { var callItr = callItem.arrayIterator(globalThis); var match = true; while (callItr.next()) |callArg| { - if (!callArg.jestDeepEquals(arguments[callItr.i - 1], globalThis)) { + if (!try callArg.jestDeepEquals(arguments[callItr.i - 1], globalThis)) { match = false; break; } @@ -4238,7 +4241,7 @@ pub const Expect = struct { } else { var itr = lastCallValue.arrayIterator(globalThis); while (itr.next()) |callArg| { - if (!callArg.jestDeepEquals(arguments[itr.i - 1], globalThis)) { + if (!try callArg.jestDeepEquals(arguments[itr.i - 1], globalThis)) { pass = false; break; } @@ -4305,7 +4308,7 @@ pub const Expect = struct { } else { var itr = nthCallValue.arrayIterator(globalThis); while (itr.next()) |callArg| { - if (!callArg.jestDeepEquals(arguments[itr.i], globalThis)) { + if (!try callArg.jestDeepEquals(arguments[itr.i], globalThis)) { pass = false; break; } @@ -5420,7 +5423,7 @@ pub const ExpectMatcherContext = struct { return .zero; } const args = arguments.slice(); - return JSValue.jsBoolean(args[0].jestDeepEquals(args[1], globalThis)); + return JSValue.jsBoolean(try args[0].jestDeepEquals(args[1], globalThis)); } }; diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index e899de489de879..ba5e3cfad8443f 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -508,7 +508,7 @@ pub const Crypto = struct { // i don't think its a real scenario, but just in case buf = globalThis.allocator().alloc(u8, keylen) catch { globalThis.throw("Failed to allocate memory", .{}); - return .undefined; + return .zero; }; needs_deinit = true; } else { @@ -565,7 +565,7 @@ pub const Crypto = struct { const len = a.len; if (b.len != len) { globalThis.throw("Input buffers must have the same byte length", .{}); - return .undefined; + return .zero; } return JSC.jsBoolean(len == 0 or bun.BoringSSL.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); } @@ -667,7 +667,7 @@ pub const Crypto = struct { encoding_value = arguments[0]; break :brk JSC.Node.Encoding.fromJS(encoding_value, globalThis) orelse { globalThis.ERR_UNKNOWN_ENCODING("Encoding must be one of base64, base64url, hex, or buffer", .{}).throw(); - return .undefined; + return .zero; }; } } diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index a475d6b968dd4a..2b6e85b5086fbf 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -4409,7 +4409,7 @@ pub const Blob = struct { .share => { if (buf.len > JSC.synthetic_allocation_limit and TypedArrayView != .ArrayBuffer) { global.throwOutOfMemory(); - return JSValue.zero; + return .zero; } this.store.?.ref(); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 4947c7e97e247a..2ab7e4ddd6161d 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -512,7 +512,7 @@ pub const StreamStart = union(Tag) { }, .err => |err| { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - return .undefined; + return .zero; }, .owned_and_done => |list| { return JSC.ArrayBuffer.fromBytes(list.slice(), .Uint8Array).toJS(globalThis, null); @@ -1685,15 +1685,13 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { .code = bun.String.static(@tagName(.ERR_ILLEGAL_CONSTRUCTOR)), }; globalThis.throwValue(err.toErrorInstance(globalThis)); - return .undefined; + return .zero; } var allocator = globalThis.bunVM().allocator; var this = allocator.create(ThisSink) catch { - globalThis.vm().throwError(globalThis, Syscall.Error.oom.toJSC( - globalThis, - )); - return .undefined; + globalThis.vm().throwError(globalThis, Syscall.Error.oom.toJSC(globalThis)); + return .zero; }; this.sink.construct(allocator); return createObject(globalThis, this, 0); @@ -1737,7 +1735,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { fn invalidThis(globalThis: *JSGlobalObject) JSValue { const err = JSC.toTypeError(.ERR_INVALID_THIS, "Expected Sink", .{}, globalThis); globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } pub fn unprotect(this: *@This()) void { @@ -1752,7 +1750,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1760,13 +1758,8 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { const args = args_list.ptr[0..args_list.len]; if (args.len == 0) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - .ERR_MISSING_ARGS, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return .undefined; + globalThis.vm().throwError(globalThis, JSC.toTypeError(.ERR_MISSING_ARGS, "write() expects a string, ArrayBufferView, or ArrayBuffer", .{}, globalThis)); + return .zero; } const arg = args[0]; @@ -1774,13 +1767,8 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { defer arg.ensureStillAlive(); if (arg.isEmptyOrUndefinedOrNull()) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - .ERR_STREAM_NULL_VALUES, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return .undefined; + globalThis.vm().throwError(globalThis, JSC.toTypeError(.ERR_STREAM_NULL_VALUES, "write() expects a string, ArrayBufferView, or ArrayBuffer", .{}, globalThis)); + return .zero; } if (arg.asArrayBuffer(globalThis)) |buffer| { @@ -1793,13 +1781,8 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { } if (!arg.isString()) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - .ERR_INVALID_ARG_TYPE, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return .undefined; + globalThis.vm().throwError(globalThis, JSC.toTypeError(.ERR_INVALID_ARG_TYPE, "write() expects a string, ArrayBufferView, or ArrayBuffer", .{}, globalThis)); + return .zero; } const str = arg.getZigString(globalThis); @@ -1822,7 +1805,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1836,7 +1819,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { globalThis, ); globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } const arg = args[0]; @@ -1860,7 +1843,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1875,7 +1858,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1894,7 +1877,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { .result => |value| value, .err => |err| blk: { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - break :blk .undefined; + break :blk .zero; }, }; } @@ -1910,7 +1893,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1939,7 +1922,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -1962,7 +1945,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { if (comptime @hasDecl(SinkType, "getPendingError")) { if (this.sink.getPendingError()) |err| { globalThis.vm().throwError(globalThis, err); - return .undefined; + return .zero; } } @@ -2023,7 +2006,7 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { // var this = @ptrCast(*ThisSocket, @alignCast( fromJS(globalThis, callframe.this()) orelse { // const err = JSC.toTypeError(.ERR_INVALID_THIS, "Expected Socket", .{}, globalThis); // globalThis.vm().throwError(globalThis, err); -// return .undefined; +// return .zero; // })); // } // }; @@ -2862,7 +2845,7 @@ pub fn ReadableStreamSource( .chunk_size => |size| return JSValue.jsNumber(size), .err => |err| { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - return .undefined; + return .zero; }, else => |rc| { return rc.toJS(globalThis); @@ -2886,7 +2869,7 @@ pub fn ReadableStreamSource( js_err.unprotect(); globalThis.vm().throwError(globalThis, js_err); } - return JSValue.jsUndefined(); + return .zero; }, .pending => { const out = result.toJS(globalThis); diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 12c78ff158470f..36f85fdcdee138 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1754,10 +1754,7 @@ const JavaScriptCoreBindings = struct { output += ` pub fn ${classSymbolName(typeName, "call")}(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { if (comptime Environment.enable_logs) zig("${typeName}({})", .{callFrame}); - return @call(.always_inline, ${typeName}.call, .{globalObject, callFrame}) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalObject.throwOutOfMemoryValue(), - }; + return @call(.always_inline, JSC.toJSHostFunction(${typeName}.call), .{globalObject, callFrame}); } `; } @@ -1810,10 +1807,7 @@ const JavaScriptCoreBindings = struct { output += ` pub fn ${names.fn}(thisValue: *${typeName}, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame${proto[name].passThis ? ", js_this_value: JSC.JSValue" : ""}) callconv(JSC.conv) JSC.JSValue { if (comptime Environment.enable_logs) zig("${typeName}.${name}({})", .{callFrame}); - return @call(.always_inline, ${typeName}.${fn}, .{thisValue, globalObject, callFrame${proto[name].passThis ? ", js_this_value" : ""}}) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalObject.throwOutOfMemoryValue(), - }; + return @call(.always_inline, JSC.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.${fn}, .{thisValue, globalObject, callFrame${proto[name].passThis ? ", js_this_value" : ""}})}); } `; } @@ -1860,10 +1854,7 @@ const JavaScriptCoreBindings = struct { output += ` pub fn ${names.fn}(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { if (comptime Environment.enable_logs) JSC.markBinding(@src()); - return @call(.always_inline, ${typeName}.${fn}, .{globalObject, callFrame}) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => globalObject.throwOutOfMemoryValue(), - }; + return @call(.always_inline, JSC.toJSHostFunction(${typeName}.${fn}), .{globalObject, callFrame}); } `; } diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index e3b3c06c33b374..f1443a2df4d4b1 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -216,10 +216,7 @@ export function getJS2NativeZig(gs2NativeZigPath: string) { })}(global: *JSC.JSGlobalObject, call_frame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue {`, ` const function = @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), x.filename))}); - return @call(.always_inline, function.${x.symbol_target}, .{global, call_frame}) catch |err| switch (err) { - error.JSError => .zero, - error.OutOfMemory => global.throwOutOfMemoryValue(), - };`, + return @call(.always_inline, JSC.toJSHostFunction(function.${x.symbol_target}), .{global, call_frame});`, "}", ]), ].join("\n"); diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index fbe8aab8d562d7..9445c8ab7c0762 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -40,11 +40,11 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const source_arg: JSC.JSValue = arguments.nextEat() orelse { globalThis.throw("minifyTestWithOptions: expected 2 arguments, got 0", .{}); - return .undefined; + return .zero; }; if (!source_arg.isString()) { globalThis.throw("minifyTestWithOptions: expected source to be a string", .{}); - return .undefined; + return .zero; } const source_bunstr = source_arg.toBunString(globalThis); defer source_bunstr.deref(); @@ -53,11 +53,11 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c const expected_arg = arguments.nextEat() orelse { globalThis.throw("minifyTestWithOptions: expected 2 arguments, got 1", .{}); - return .undefined; + return .zero; }; if (!expected_arg.isString()) { globalThis.throw("minifyTestWithOptions: expected `expected` arg to be a string", .{}); - return .undefined; + return .zero; } const expected_bunstr = expected_arg.toBunString(globalThis); defer expected_bunstr.deref(); @@ -120,7 +120,7 @@ pub fn testingImpl(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, c return log.toJS(globalThis, bun.default_allocator, "parsing failed:"); } globalThis.throw("parsing failed: {}", .{err.kind}); - return .undefined; + return .zero; }, } } @@ -206,11 +206,11 @@ pub fn attrTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun. var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const source_arg: JSC.JSValue = arguments.nextEat() orelse { globalThis.throw("attrTest: expected 3 arguments, got 0", .{}); - return .undefined; + return .zero; }; if (!source_arg.isString()) { globalThis.throw("attrTest: expected source to be a string", .{}); - return .undefined; + return .zero; } const source_bunstr = source_arg.toBunString(globalThis); defer source_bunstr.deref(); @@ -219,11 +219,11 @@ pub fn attrTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun. const expected_arg = arguments.nextEat() orelse { globalThis.throw("attrTest: expected 3 arguments, got 1", .{}); - return .undefined; + return .zero; }; if (!expected_arg.isString()) { globalThis.throw("attrTest: expected `expected` arg to be a string", .{}); - return .undefined; + return .zero; } const expected_bunstr = expected_arg.toBunString(globalThis); defer expected_bunstr.deref(); @@ -232,7 +232,7 @@ pub fn attrTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun. const minify_arg: JSC.JSValue = arguments.nextEat() orelse { globalThis.throw("attrTest: expected 3 arguments, got 2", .{}); - return .undefined; + return .zero; }; const minify = minify_arg.isBoolean() and minify_arg.toBoolean(); @@ -271,7 +271,7 @@ pub fn attrTest(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun. return log.toJS(globalThis, bun.default_allocator, "parsing failed:"); } globalThis.throw("parsing failed: {}", .{err.kind}); - return .undefined; + return .zero; }, } } diff --git a/src/css/values/color_js.zig b/src/css/values/color_js.zig index 8c6e176cee9ab7..cf7662994d276e 100644 --- a/src/css/values/color_js.zig +++ b/src/css/values/color_js.zig @@ -151,7 +151,7 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram const args = callFrame.argumentsAsArray(2); if (args[0].isUndefined()) { globalThis.throwInvalidArgumentType("color", "input", "string, number, or object"); - return JSC.JSValue.jsUndefined(); + return .zero; } var arena = std.heap.ArenaAllocator.init(bun.default_allocator); @@ -166,7 +166,7 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram if (!args[1].isEmptyOrUndefinedOrNull()) { if (!args[1].isString()) { globalThis.throwInvalidArgumentType("color", "format", "string"); - return JSC.JSValue.jsUndefined(); + return .zero; } break :brk try args[1].toEnum(globalThis, "format", OutputColorFormat); @@ -228,7 +228,7 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram }, else => { globalThis.throw("Expected array length 3 or 4", .{}); - return JSC.JSValue.jsUndefined(); + return .zero; }, } } else if (args[0].isObject()) { @@ -284,7 +284,7 @@ pub fn jsFunctionColor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFram } globalThis.throw("color() failed to parse {s}", .{@tagName(err.basic().kind)}); - return JSC.JSValue.jsUndefined(); + return .zero; }, .result => |*result| { const format: OutputColorFormat = if (unresolved_format == .ansi) switch (bun.Output.Source.colorDepth()) { diff --git a/src/install/semver.zig b/src/install/semver.zig index 316d0cf451185a..0768bfb0c1389f 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -2700,10 +2700,7 @@ pub const SemverObject = struct { }; } - pub fn satisfies( - globalThis: *JSC.JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { + pub fn satisfies(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { var arena = std.heap.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); var stack_fallback = std.heap.stackFallback(512, arena.allocator()); @@ -2718,8 +2715,8 @@ pub const SemverObject = struct { const left_arg = arguments[0]; const right_arg = arguments[1]; - const left_string = left_arg.toStringOrNull(globalThis) orelse return .false; - const right_string = right_arg.toStringOrNull(globalThis) orelse return .false; + const left_string = left_arg.toStringOrNull(globalThis) orelse return .zero; + const right_string = right_arg.toStringOrNull(globalThis) orelse return .zero; const left = left_string.toSlice(globalThis, allocator); defer left.deinit(); diff --git a/src/patch.zig b/src/patch.zig index 21a7464a00d8e7..2ba0819628068b 100644 --- a/src/patch.zig +++ b/src/patch.zig @@ -1100,14 +1100,14 @@ pub const TestingAPIs = struct { const old_folder_jsval = arguments.nextEat() orelse { globalThis.throw("expected 2 strings", .{}); - return .undefined; + return .zero; }; const old_folder_bunstr = old_folder_jsval.toBunString(globalThis); defer old_folder_bunstr.deref(); const new_folder_jsval = arguments.nextEat() orelse { globalThis.throw("expected 2 strings", .{}); - return .undefined; + return .zero; }; const new_folder_bunstr = new_folder_jsval.toBunString(globalThis); defer new_folder_bunstr.deref(); @@ -1128,7 +1128,7 @@ pub const TestingAPIs = struct { .err => |e| { defer e.deinit(); globalThis.throw("failed to make diff: {s}", .{e.items}); - return .undefined; + return .zero; }, }; } @@ -1154,7 +1154,7 @@ pub const TestingAPIs = struct { if (args.patchfile.apply(bun.default_allocator, args.dirfd)) |err| { globalThis.throwValue(err.toErrorInstance(globalThis)); - return .undefined; + return .zero; } return .true; @@ -1166,7 +1166,7 @@ pub const TestingAPIs = struct { const patchfile_src_js = arguments.nextEat() orelse { globalThis.throw("TestingAPIs.parse: expected at least 1 argument, got 0", .{}); - return .undefined; + return .zero; }; const patchfile_src_bunstr = patchfile_src_js.toBunString(globalThis); const patchfile_src = patchfile_src_bunstr.toUTF8(bun.default_allocator); @@ -1182,7 +1182,7 @@ pub const TestingAPIs = struct { const str = std.json.stringifyAlloc(bun.default_allocator, patchfile, .{}) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; const outstr = bun.String.fromUTF8(str); return outstr.toJS(globalThis); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 35e48af852d1d3..a54941f0288c4b 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -708,7 +708,7 @@ pub const ParsedShellScript = struct { var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const str_js = arguments.nextEat() orelse { globalThis.throw("$`...`.cwd(): expected a string argument", .{}); - return .undefined; + return .zero; }; const str = bun.String.fromJS(str_js, globalThis); this.cwd = str; @@ -779,7 +779,7 @@ pub const ParsedShellScript = struct { var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, shargs.arena_allocator()); var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; defer { for (jsstrings.items[0..]) |bunstr| { @@ -791,7 +791,7 @@ pub const ParsedShellScript = struct { var script = std.ArrayList(u8).init(shargs.arena_allocator()); if (!(bun.shell.shellCmdFromJS(globalThis, string_args, &template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; })) { return .undefined; } @@ -810,7 +810,7 @@ pub const ParsedShellScript = struct { assert(lex_result != null); const str = lex_result.?.combineErrors(shargs.arena_allocator()); globalThis.throwPretty("{s}", .{str}); - return .undefined; + return .zero; } if (parser) |*p| { @@ -819,7 +819,7 @@ pub const ParsedShellScript = struct { } const errstr = p.combineErrors(); globalThis.throwPretty("{s}", .{errstr}); - return .undefined; + return .zero; } return globalThis.throwError(err, "failed to lex/parse shell"); @@ -1628,6 +1628,7 @@ pub const Interpreter = struct { var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy()); this.started.store(true, .seq_cst); root.start(); + if (globalThis.hasException()) return error.JSError; return .undefined; } @@ -1773,7 +1774,7 @@ pub const Interpreter = struct { switch (this.root_shell.changeCwd(this, slice.slice())) { .err => |e| { globalThis.throwValue(e.toJSC(globalThis)); - return .undefined; + return .zero; }, .result => {}, } diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 33a34d6a273398..748fdbe32c146f 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -4320,7 +4320,7 @@ pub const TestingAPIs = struct { var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const string = arguments.nextEat() orelse { globalThis.throw("shellInternals.disabledOnPosix: expected 1 arguments, got 0", .{}); - return .undefined; + return .zero; }; const bunstr = string.toBunString(globalThis); @@ -4344,7 +4344,7 @@ pub const TestingAPIs = struct { var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const string_args = arguments.nextEat() orelse { globalThis.throw("shell_parse: expected 2 arguments, got 0", .{}); - return .undefined; + return .zero; }; var arena = std.heap.ArenaAllocator.init(bun.default_allocator); @@ -4352,13 +4352,13 @@ pub const TestingAPIs = struct { const template_args_js = arguments.nextEat() orelse { globalThis.throw("shell: expected 2 arguments, got 0", .{}); - return .undefined; + return .zero; }; var template_args = template_args_js.arrayIterator(globalThis); var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; defer { for (jsstrings.items[0..]) |bunstr| { @@ -4376,7 +4376,7 @@ pub const TestingAPIs = struct { var script = std.ArrayList(u8).init(arena.allocator()); if (!(shellCmdFromJS(globalThis, string_args, &template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; })) { return .undefined; } @@ -4399,24 +4399,24 @@ pub const TestingAPIs = struct { if (lex_result.errors.len > 0) { const str = lex_result.combineErrors(arena.allocator()); globalThis.throwPretty("{s}", .{str}); - return .undefined; + return .zero; } var test_tokens = std.ArrayList(Test.TestToken).initCapacity(arena.allocator(), lex_result.tokens.len) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; for (lex_result.tokens) |tok| { const test_tok = Test.TestToken.from_real(tok, lex_result.strpool); test_tokens.append(test_tok) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; } const str = std.json.stringifyAlloc(globalThis.bunVM().allocator, test_tokens.items[0..], .{}) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; defer globalThis.bunVM().allocator.free(str); @@ -4432,7 +4432,7 @@ pub const TestingAPIs = struct { var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const string_args = arguments.nextEat() orelse { globalThis.throw("shell_parse: expected 2 arguments, got 0", .{}); - return .undefined; + return .zero; }; var arena = bun.ArenaAllocator.init(bun.default_allocator); @@ -4440,13 +4440,13 @@ pub const TestingAPIs = struct { const template_args_js = arguments.nextEat() orelse { globalThis.throw("shell: expected 2 arguments, got 0", .{}); - return .undefined; + return .zero; }; var template_args = template_args_js.arrayIterator(globalThis); var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { globalThis.throwOutOfMemory(); - return .undefined; + return .zero; }; defer { for (jsstrings.items[0..]) |bunstr| { @@ -4463,7 +4463,7 @@ pub const TestingAPIs = struct { var script = std.ArrayList(u8).init(arena.allocator()); if (!(shellCmdFromJS(globalThis, string_args, &template_args, &jsobjs, &jsstrings, &script) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; })) { return .undefined; } @@ -4476,13 +4476,13 @@ pub const TestingAPIs = struct { if (bun.Environment.allow_assert) assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(arena.allocator()); globalThis.throwPretty("{s}", .{str}); - return .undefined; + return .zero; } if (out_parser) |*p| { const errstr = p.combineErrors(); globalThis.throwPretty("{s}", .{errstr}); - return .undefined; + return .zero; } return globalThis.throwError(err, "failed to lex/parse shell"); @@ -4490,7 +4490,7 @@ pub const TestingAPIs = struct { const str = std.json.stringifyAlloc(globalThis.bunVM().allocator, script_ast, .{}) catch { globalThis.throwOutOfMemory(); - return JSValue.undefined; + return .zero; }; defer globalThis.bunVM().allocator.free(str); From b19f13f5c49b5f21735ac70394285634afe2cbcc Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Mon, 25 Nov 2024 15:19:48 -0800 Subject: [PATCH 06/11] bun-vscode: Bump version [no ci] --- packages/bun-vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json index e872c00b1008b2..dcf4307f6b5087 100644 --- a/packages/bun-vscode/package.json +++ b/packages/bun-vscode/package.json @@ -1,6 +1,6 @@ { "name": "bun-vscode", - "version": "0.0.15", + "version": "0.0.18", "author": "oven", "repository": { "type": "git", From 8ca0eb831d6739c6a94b3f4d484bbfe71ee97226 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 15:42:02 -0800 Subject: [PATCH 07/11] Clean up some error handling code (#15368) Co-authored-by: Dylan Conway --- src/bun.js/api/bun/h2_frame_parser.zig | 39 +- src/bun.js/api/bun/socket.zig | 39 +- src/bun.js/api/server.zig | 16 +- .../bindings/ExposeNodeModuleGlobals.cpp | 2 +- src/bun.js/bindings/bindings.cpp | 5 +- src/bun.js/bindings/bindings.zig | 170 +++-- src/crash_handler.zig | 111 +-- src/string.zig | 12 +- test/harness.ts | 18 + .../bun/http/bun-listen-connect-args.test.ts | 74 ++ test/js/bun/http/bun-serve-args.test.ts | 654 ++++++++++++++++++ 11 files changed, 977 insertions(+), 163 deletions(-) create mode 100644 test/js/bun/http/bun-listen-connect-args.test.ts create mode 100644 test/js/bun/http/bun-serve-args.test.ts diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 5f99735d252752..ab7c421e48bbfa 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3353,14 +3353,16 @@ pub const H2FrameParser = struct { if (this.isServer) { if (!ValidPseudoHeaders.has(name)) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_PSEUDOHEADER, "\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_PSEUDOHEADER("\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}).throw(); + } return .zero; } } else { if (!ValidRequestPseudoHeaders.has(name)) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_PSEUDOHEADER, "\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_PSEUDOHEADER("\"{s}\" is an invalid pseudoheader or is used incorrectly", .{name}).throw(); + } return .zero; } } @@ -3368,9 +3370,10 @@ pub const H2FrameParser = struct { continue; } - var js_value = try headers_arg.getTruthy(globalObject, name) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + const js_value: JSC.JSValue = try headers_arg.get(globalObject, name) orelse { + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; @@ -3380,21 +3383,24 @@ pub const H2FrameParser = struct { var value_iter = js_value.arrayIterator(globalObject); if (SingleValueHeaders.has(name) and value_iter.len > 1) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Header field \"{s}\" must only have a single value", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Header field \"{s}\" must only have a single value", .{name}).throw(); + } return .zero; } while (value_iter.next()) |item| { if (item.isEmptyOrUndefinedOrNull()) { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; } const value_str = item.toStringOrNull(globalObject) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; @@ -3417,11 +3423,12 @@ pub const H2FrameParser = struct { return .undefined; }; } - } else { + } else if (!js_value.isEmptyOrUndefinedOrNull()) { log("single header {s}", .{name}); const value_str = js_value.toStringOrNull(globalObject) orelse { - const exception = JSC.toTypeError(.ERR_HTTP2_INVALID_HEADER_VALUE, "Invalid value for header \"{s}\"", .{name}, globalObject); - globalObject.throwValue(exception); + if (!globalObject.hasException()) { + globalObject.ERR_HTTP2_INVALID_HEADER_VALUE("Invalid value for header \"{s}\"", .{name}).throw(); + } return .zero; }; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 5aeb892712623e..861d5d002bfe3e 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -313,6 +313,7 @@ pub const SocketConfig = struct { pub fn fromJS(vm: *JSC.VirtualMachine, opts: JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!SocketConfig { var hostname_or_unix: JSC.ZigString.Slice = JSC.ZigString.Slice.empty; + errdefer hostname_or_unix.deinit(); var port: ?u16 = null; var exclusive = false; var allowHalfOpen = false; @@ -332,6 +333,12 @@ pub const SocketConfig = struct { } } + errdefer { + if (ssl != null) { + ssl.?.deinit(); + } + } + hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { if (fd_.isNumber()) { @@ -339,16 +346,18 @@ pub const SocketConfig = struct { } } - if (try opts.getTruthy(globalObject, "unix")) |unix_socket| { - if (!unix_socket.isString()) { - return globalObject.throwInvalidArguments("Expected \"unix\" to be a string", .{}); - } + if (try opts.getStringish(globalObject, "unix")) |unix_socket| { + defer unix_socket.deref(); - hostname_or_unix = unix_socket.getZigString(globalObject).toSlice(bun.default_allocator); + hostname_or_unix = try unix_socket.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); if (strings.hasPrefixComptime(hostname_or_unix.slice(), "file://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "unix://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "sock://")) { - hostname_or_unix.ptr += 7; - hostname_or_unix.len -|= 7; + // The memory allocator relies on the pointer address to + // free it, so if we simply moved the pointer up it would + // cause an issue when freeing it later. + const moved_bytes = try bun.default_allocator.dupeZ(u8, hostname_or_unix.slice()[7..]); + hostname_or_unix.deinit(); + hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); } if (hostname_or_unix.len > 0) { @@ -363,20 +372,21 @@ pub const SocketConfig = struct { allowHalfOpen = true; } - if (try opts.getTruthy(globalObject, "hostname") orelse try opts.getTruthy(globalObject, "host")) |hostname| { - if (!hostname.isString()) { - return globalObject.throwInvalidArguments("Expected \"hostname\" to be a string", .{}); - } + if (try opts.getStringish(globalObject, "hostname") orelse try opts.getStringish(globalObject, "host")) |hostname| { + defer hostname.deref(); var port_value = try opts.get(globalObject, "port") orelse JSValue.zero; - hostname_or_unix = hostname.getZigString(globalObject).toSlice(bun.default_allocator); + hostname_or_unix = try hostname.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); if (port_value.isEmptyOrUndefinedOrNull() and hostname_or_unix.len > 0) { const parsed_url = bun.URL.parse(hostname_or_unix.slice()); if (parsed_url.getPort()) |port_num| { port_value = JSValue.jsNumber(port_num); - hostname_or_unix.ptr = parsed_url.hostname.ptr; - hostname_or_unix.len = @as(u32, @truncate(parsed_url.hostname.len)); + if (parsed_url.hostname.len > 0) { + const moved_bytes = try bun.default_allocator.dupeZ(u8, parsed_url.hostname); + hostname_or_unix.deinit(); + hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); + } } } @@ -410,7 +420,6 @@ pub const SocketConfig = struct { return globalObject.throwInvalidArguments("Expected either \"hostname\" or \"unix\"", .{}); } - errdefer hostname_or_unix.deinit(); var handlers = try Handlers.fromJS(globalObject, try opts.get(globalObject, "socket") orelse JSValue.zero); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 87ab2838a3ea0c..4648fafbc9b45d 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1304,11 +1304,9 @@ pub const ServerConfig = struct { } if (global.hasException()) return error.JSError; - if (try arg.getTruthy(global, "hostname") orelse try arg.getTruthy(global, "host")) |host| { - const host_str = host.toSlice( - global, - bun.default_allocator, - ); + if (try arg.getStringish(global, "hostname") orelse try arg.getStringish(global, "host")) |host| { + defer host.deref(); + const host_str = host.toUTF8(bun.default_allocator); defer host_str.deinit(); if (host_str.len > 0) { @@ -1318,11 +1316,9 @@ pub const ServerConfig = struct { } if (global.hasException()) return error.JSError; - if (try arg.getTruthy(global, "unix")) |unix| { - const unix_str = unix.toSlice( - global, - bun.default_allocator, - ); + if (try arg.getStringish(global, "unix")) |unix| { + defer unix.deref(); + const unix_str = unix.toUTF8(bun.default_allocator); defer unix_str.deinit(); if (unix_str.len > 0) { if (has_hostname) { diff --git a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp index a99b6624881755..0497c1c6a6d360 100644 --- a/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp +++ b/src/bun.js/bindings/ExposeNodeModuleGlobals.cpp @@ -123,4 +123,4 @@ extern "C" void Bun__ExposeNodeModuleGlobals(Zig::GlobalObject* globalObject) 0 | JSC::PropertyAttribute::CustomValue ); } -} \ No newline at end of file +} diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index f80dd361480077..b813c614cfec43 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3793,8 +3793,9 @@ JSC__JSValue JSC__JSValue__getIfPropertyExistsImpl(JSC__JSValue JSValue0, JSC::VM& vm = globalObject->vm(); JSC::JSObject* object = value.getObject(); - if (UNLIKELY(!object)) - return JSValue::encode({}); + if (UNLIKELY(!object)) { + return JSValue::encode(JSValue::decode(JSC::JSValue::ValueDeleted)); + } // Since Identifier might not ref' the string, we need to ensure it doesn't get deref'd until this function returns const auto propertyString = String(StringImpl::createWithoutCopying({ arg1, arg2 })); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 63b462787b494f..cfeaee4b8983c0 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3084,13 +3084,19 @@ pub const JSGlobalObject = opaque { ); if (possible_errors.OutOfMemory and err == error.OutOfMemory) { - bun.assert(!global.hasException()); // dual exception - global.throwOutOfMemory(); + if (global.hasException()) { + if (comptime bun.Environment.isDebug) bun.Output.panic("attempted to throw OutOfMemory without an exception", .{}); + } else { + global.throwOutOfMemory(); + } return null_value; } if (possible_errors.JSError and err == error.JSError) { - bun.assert(global.hasException()); // Exception was cleared, yet returned. + if (!global.hasException()) { + if (comptime bun.Environment.isDebug) bun.Output.panic("attempted to throw JSError without an exception", .{}); + global.throwOutOfMemory(); + } return null_value; } @@ -3736,11 +3742,19 @@ pub const JSValueReprInt = i64; /// ABI-compatible with EncodedJSValue /// In the future, this type will exclude `zero`, encoding it as `error.JSError` instead. pub const JSValue = enum(i64) { - zero = 0, undefined = 0xa, null = 0x2, true = FFI.TrueI64, false = 0x6, + + /// Typically means an exception was thrown. + zero = 0, + + /// JSValue::ValueDeleted + /// + /// Deleted is a special encoding used in JSC hash map internals used for + /// the null state. It is re-used here for encoding the "not present" state. + property_does_not_exist_on_object = 0x4, _, /// When JavaScriptCore throws something, it returns a null cell (0). The @@ -5270,17 +5284,25 @@ pub const JSValue = enum(i64) { // `this` must be known to be an object // intended to be more lightweight than ZigString. pub fn fastGet(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { - if (bun.Environment.allow_assert) + if (bun.Environment.isDebug) bun.assert(this.isObject()); - const result = JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name)).legacyUnwrap(); - if (result == .zero or - // JS APIs treat {}.a as mostly the same as though it was not defined - result == .undefined) - { - return null; - } - return result; + return switch (JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name))) { + .zero, .undefined, .property_does_not_exist_on_object => null, + else => |val| val, + }; + } + + pub fn fastGetWithError(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) JSError!?JSValue { + if (bun.Environment.isDebug) + bun.assert(this.isObject()); + + return switch (JSC__JSValue__fastGet(this, global, @intFromEnum(builtin_name))) { + .zero => error.JSError, + .undefined => null, + .property_does_not_exist_on_object => null, + else => |val| val, + }; } pub fn fastGetDirect(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { @@ -5292,7 +5314,7 @@ pub const JSValue = enum(i64) { return result; } - extern fn JSC__JSValue__fastGet(value: JSValue, global: *JSGlobalObject, builtin_id: u8) GetResult; + extern fn JSC__JSValue__fastGet(value: JSValue, global: *JSGlobalObject, builtin_id: u8) JSValue; extern fn JSC__JSValue__fastGetOwn(value: JSValue, globalObject: *JSGlobalObject, property: BuiltinName) JSValue; pub fn fastGetOwn(this: JSValue, global: *JSGlobalObject, builtin_name: BuiltinName) ?JSValue { const result = JSC__JSValue__fastGetOwn(this, global, builtin_name); @@ -5307,42 +5329,7 @@ pub const JSValue = enum(i64) { return cppFn("fastGetDirect_", .{ this, global, builtin_name }); } - /// Problem: The `get` needs to model !?JSValue - /// - null -> the property does not exist - /// - error -> the get operation threw - /// - any other JSValue -> success. this could be jsNull() or jsUndefined() - /// - /// `.zero` is already used for the error state - /// - /// Deleted is a special encoding used in JSC hash map internals used for - /// the null state. It is re-used here for encoding the "not present" state. - const GetResult = enum(i64) { - thrown_exception = 0, - does_not_exist = 0x4, // JSC::JSValue::ValueDeleted - _, - - fn legacyUnwrap(value: GetResult) ?JSValue { - return switch (value) { - // footgun! caller must check hasException on every `get` or else Bun will crash - .thrown_exception => null, - - .does_not_exist => null, - else => @enumFromInt(@intFromEnum(value)), - }; - } - - fn unwrap(value: GetResult, global: *JSGlobalObject) JSError!?JSValue { - return switch (value) { - .thrown_exception => { - bun.assert(global.hasException()); - return error.JSError; - }, - .does_not_exist => null, - else => @enumFromInt(@intFromEnum(value)), - }; - } - }; - extern fn JSC__JSValue__getIfPropertyExistsImpl(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) GetResult; + extern fn JSC__JSValue__getIfPropertyExistsImpl(target: JSValue, global: *JSGlobalObject, ptr: [*]const u8, len: u32) JSValue; pub fn getIfPropertyExistsFromPath(this: JSValue, global: *JSGlobalObject, path: JSValue) JSValue { return cppFn("getIfPropertyExistsFromPath", .{ this, global, path }); @@ -5391,7 +5378,10 @@ pub const JSValue = enum(i64) { } } - return JSC__JSValue__getIfPropertyExistsImpl(this, global, property.ptr, @intCast(property.len)).legacyUnwrap(); + return switch (JSC__JSValue__getIfPropertyExistsImpl(this, global, property.ptr, @intCast(property.len))) { + .undefined, .zero, .property_does_not_exist_on_object => null, + else => |val| val, + }; } /// Equivalent to `target[property]`. Calls userland getters/proxies. Can @@ -5403,17 +5393,21 @@ pub const JSValue = enum(i64) { /// marked `inline` to allow Zig to determine if `fastGet` should be used /// per invocation. pub inline fn get(target: JSValue, global: *JSGlobalObject, property: anytype) JSError!?JSValue { - if (bun.Environment.allow_assert) bun.assert(target.isObject()); + if (bun.Environment.isDebug) bun.assert(target.isObject()); const property_slice: []const u8 = property; // must be a slice! // This call requires `get2` to be `inline` if (bun.isComptimeKnown(property_slice)) { - if (comptime BuiltinName.get(property_slice)) |builtin| { - return target.fastGet(global, builtin); + if (comptime BuiltinName.get(property_slice)) |builtin_name| { + return target.fastGetWithError(global, builtin_name); } } - return JSC__JSValue__getIfPropertyExistsImpl(target, global, property_slice.ptr, @intCast(property_slice.len)).unwrap(global); + return switch (JSC__JSValue__getIfPropertyExistsImpl(target, global, property_slice.ptr, @intCast(property_slice.len))) { + .zero => error.JSError, + .undefined, .property_does_not_exist_on_object => null, + else => |val| val, + }; } extern fn JSC__JSValue__getOwn(value: JSValue, globalObject: *JSGlobalObject, propertyName: *const bun.String) JSValue; @@ -5459,15 +5453,33 @@ pub const JSValue = enum(i64) { return getOwnTruthy(this, global, property); } - // TODO: replace calls to this function with `getOptional` - pub fn getTruthyComptime(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) bun.JSError!?JSValue { - if (comptime bun.ComptimeEnumMap(BuiltinName).has(property)) { - if (fastGet(this, global, @field(BuiltinName, property))) |prop| { - if (prop.isEmptyOrUndefinedOrNull()) return null; + pub fn truthyPropertyValue(prop: JSValue) ?JSValue { + return switch (prop) { + .null => null, + + // Handled by get() and fastGet(). + .zero, .undefined => unreachable, + + // false, 0, are deliberately not included in this list. + // That would prevent you from passing `0` or `false` to various Bun APIs. + + else => { + // Ignore empty string. + if (prop.isString()) { + if (!prop.toBoolean()) { + return null; + } + } + return prop; - } + }, + }; + } - return null; + // TODO: replace calls to this function with `getOptional` + pub fn getTruthyComptime(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) bun.JSError!?JSValue { + if (comptime BuiltinName.has(property)) { + return truthyPropertyValue(fastGet(this, global, @field(BuiltinName, property)) orelse return null); } return getTruthy(this, global, property); @@ -5476,13 +5488,43 @@ pub const JSValue = enum(i64) { // TODO: replace calls to this function with `getOptional` pub fn getTruthy(this: JSValue, global: *JSGlobalObject, property: []const u8) bun.JSError!?JSValue { if (try get(this, global, property)) |prop| { - if (prop.isEmptyOrUndefinedOrNull()) return null; - return prop; + return truthyPropertyValue(prop); } return null; } + /// Get a value that can be coerced to a string. + /// + /// Returns null when the value is: + /// - JSValue.null + /// - JSValue.false + /// - JSValue.undefined + /// - an empty string + pub fn getStringish(this: JSValue, global: *JSGlobalObject, property: []const u8) bun.JSError!?bun.String { + const prop = try get(this, global, property) orelse return null; + if (prop.isNull() or prop == .false) { + return null; + } + + if (prop.isSymbol()) { + _ = global.throwInvalidPropertyTypeValue(property, "string", prop); + return error.JSError; + } + + const str = prop.toBunString(global); + if (global.hasException()) { + str.deref(); + return error.JSError; + } + + if (str.isEmpty()) { + return null; + } + + return str; + } + pub fn toEnumFromMap( this: JSValue, globalThis: *JSGlobalObject, diff --git a/src/crash_handler.zig b/src/crash_handler.zig index d9cb08f9899240..c0ea3aafaf60cf 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -1449,66 +1449,73 @@ fn crash() noreturn { pub var verbose_error_trace = false; -fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { - if (!builtin.have_error_return_tracing) return; - if (!verbose_error_trace and !is_root) return; - - if (maybe_trace) |trace| { - // The format of the panic trace is slightly different in debug - // builds Mainly, we demangle the backtrace immediately instead - // of using a trace string. - // - // To make the release-mode behavior easier to demo, debug mode - // checks for this CLI flag. - const is_debug = bun.Environment.isDebug and check_flag: { - for (bun.argv) |arg| { - if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { - break :check_flag false; - } +noinline fn coldHandleErrorReturnTrace(err_int_workaround_for_zig_ccall_bug: std.meta.Int(.unsigned, @bitSizeOf(anyerror)), trace: *std.builtin.StackTrace, comptime is_root: bool) void { + @setCold(true); + const err = @errorFromInt(err_int_workaround_for_zig_ccall_bug); + + // The format of the panic trace is slightly different in debug + // builds Mainly, we demangle the backtrace immediately instead + // of using a trace string. + // + // To make the release-mode behavior easier to demo, debug mode + // checks for this CLI flag. + const is_debug = bun.Environment.isDebug and check_flag: { + for (bun.argv) |arg| { + if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { + break :check_flag false; } - break :check_flag true; - }; + } + break :check_flag true; + }; - if (is_debug) { - if (is_root) { - if (verbose_error_trace) { - Output.note("Release build will not have this trace by default:", .{}); - } - } else { - Output.note( - "caught error.{s}:", - .{@errorName(err)}, - ); + if (is_debug) { + if (is_root) { + if (verbose_error_trace) { + Output.note("Release build will not have this trace by default:", .{}); } - Output.flush(); - dumpStackTrace(trace.*); } else { - const ts = TraceString{ - .trace = trace, - .reason = .{ .zig_error = err }, - .action = .view_trace, - }; - if (is_root) { - Output.prettyErrorln( - \\ - \\To send a redacted crash report to Bun's team, - \\please file a GitHub issue using the link below: - \\ - \\ {} - \\ - , - .{ts}, - ); - } else { - Output.prettyErrorln( - "trace: error.{s}: {}", - .{ @errorName(err), ts }, - ); - } + Output.note( + "caught error.{s}:", + .{@errorName(err)}, + ); + } + Output.flush(); + dumpStackTrace(trace.*); + } else { + const ts = TraceString{ + .trace = trace, + .reason = .{ .zig_error = err }, + .action = .view_trace, + }; + if (is_root) { + Output.prettyErrorln( + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ {} + \\ + , + .{ts}, + ); + } else { + Output.prettyErrorln( + "trace: error.{s}: {}", + .{ @errorName(err), ts }, + ); } } } +inline fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { + if (!builtin.have_error_return_tracing) return; + if (!verbose_error_trace and !is_root) return; + + if (maybe_trace) |trace| { + coldHandleErrorReturnTrace(@intFromError(err), trace, is_root); + } +} + /// In many places we catch errors, the trace for them is absorbed and only a /// single line (the error name) is printed. When this is set, we will print /// trace strings for those errors (or full stacks in debug builds). diff --git a/src/string.zig b/src/string.zig index d47cc49cf0a9ef..3b2dab9fcb3c15 100644 --- a/src/string.zig +++ b/src/string.zig @@ -706,10 +706,14 @@ pub const String = extern struct { pub fn fromJS2(value: bun.JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!String { var out: String = String.dead; if (BunString__fromJS(globalObject, value, &out)) { - bun.assert(out.tag != .Dead); + if (comptime bun.Environment.isDebug) { + bun.assert(out.tag != .Dead); + } return out; } else { - bun.assert(globalObject.hasException()); + if (comptime bun.Environment.isDebug) { + bun.assert(globalObject.hasException()); + } return error.JSError; } } @@ -721,7 +725,9 @@ pub const String = extern struct { if (BunString__fromJSRef(globalObject, value, &out)) { return out; } else { - bun.assert(globalObject.hasException()); + if (comptime bun.Environment.isDebug) { + bun.assert(globalObject.hasException()); + } return error.JSError; } } diff --git a/test/harness.ts b/test/harness.ts index 82d3231b6fc914..15a3e8da031d8d 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1383,3 +1383,21 @@ export function libcPathForDlopen() { throw new Error("TODO"); } } + +export function cwdScope(cwd: string) { + const original = process.cwd(); + process.chdir(cwd); + return { + [Symbol.dispose]() { + process.chdir(original); + }, + }; +} + +export function rmScope(path: string) { + return { + [Symbol.dispose]() { + fs.rmSync(path, { recursive: true, force: true }); + }, + }; +} diff --git a/test/js/bun/http/bun-listen-connect-args.test.ts b/test/js/bun/http/bun-listen-connect-args.test.ts new file mode 100644 index 00000000000000..21e62f2d2c1904 --- /dev/null +++ b/test/js/bun/http/bun-listen-connect-args.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import { cwdScope, isWindows, rmScope, tempDirWithFiles } from "harness"; + +describe.if(!isWindows)("unix socket", () => { + test("valid", () => { + using server = Bun.listen({ + unix: Math.random().toString(32).slice(2, 15) + ".sock", + socket: { + open() {}, + close() {}, + data() {}, + drain() {}, + }, + }); + server.stop(); + }); + + describe("allows", () => { + const permutations = [ + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + port: 0, + hostname: "", + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: undefined, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: null, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: false, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.from(""), + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.alloc(0), + }, + { + unix: "unix://" + Math.random().toString(32).slice(2, 15) + ".sock", + hostname: Buffer.alloc(0), + }, + ]; + + for (const args of permutations) { + test(`${JSON.stringify(args)}`, async () => { + const tempdir = tempDirWithFiles("test-socket", { + "foo.txt": "bar", + }); + using cwd = cwdScope(tempdir); + using rm = rmScope(tempdir); + for (let i = 0; i < 100; i++) { + using server = Bun.listen({ + ...args, + unix: args.unix.startsWith("unix://") ? "unix://" + i + args.unix.slice(7) : i + args.unix, + socket: { + open() {}, + close() {}, + data() {}, + drain() {}, + }, + }); + server.stop(); + } + }); + } + }); +}); diff --git a/test/js/bun/http/bun-serve-args.test.ts b/test/js/bun/http/bun-serve-args.test.ts new file mode 100644 index 00000000000000..8414c32d60a6e0 --- /dev/null +++ b/test/js/bun/http/bun-serve-args.test.ts @@ -0,0 +1,654 @@ +import { serve } from "bun"; +import { describe, expect, test } from "bun:test"; +import { tmpdirSync } from "../../../harness"; + +const defaultHostname = "localhost"; + +describe("Bun.serve basic options", () => { + test("minimal valid config", () => { + using server = serve({ + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); // Default port + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("port as string", () => { + using server = serve({ + port: "0", + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + server.stop(); + }); +}); + +describe("unix socket", () => { + const permutations = [ + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: "", + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: undefined, + }, + { + unix: Math.random().toString(32).slice(2, 15) + ".sock", + hostname: null, + }, + { + unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"), + hostname: null, + }, + { + unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"), + hostname: Buffer.from(""), + }, + ] as const; + + for (const { unix, hostname } of permutations) { + test(`unix: ${unix} and hostname: ${hostname}`, () => { + using server = serve({ + // @ts-expect-error - Testing invalid combination + unix, + // @ts-expect-error - Testing invalid combination + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + // @ts-expect-error - Testing invalid property + expect(server.address + "").toBe(unix + ""); + expect(server.port).toBeUndefined(); + expect(server.hostname).toBeUndefined(); + server.stop(); + }); + } +}); + +describe("hostname and port works", () => { + const permutations = [ + { + port: 0, + hostname: defaultHostname, + unix: undefined, + }, + { + port: 0, + hostname: undefined, + unix: "", + }, + { + port: 0, + hostname: null, + unix: "", + }, + { + port: 0, + hostname: null, + unix: Buffer.from(""), + }, + { + port: 0, + hostname: Buffer.from(defaultHostname), + unix: Buffer.from(""), + }, + { + port: 0, + hostname: Buffer.from(defaultHostname), + unix: undefined, + }, + ] as const; + + for (const { port, hostname, unix } of permutations) { + test(`port: ${port} and hostname: ${hostname} and unix: ${unix}`, () => { + using server = serve({ + port, + // @ts-expect-error - Testing invalid combination + hostname, + // @ts-expect-error - Testing invalid combination + unix, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe((hostname || defaultHostname) + ""); + server.stop(); + }); + } +}); + +describe("Bun.serve error handling", () => { + test("missing fetch handler throws", () => { + // @ts-expect-error - Testing runtime behavior + expect(() => serve({})).toThrow(); + }); + + test("custom error handler", () => { + using server = serve({ + port: 0, + error(error) { + return new Response(`Error: ${error.message}`, { status: 500 }); + }, + fetch() { + throw new Error("test error"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve websocket options", () => { + test("basic websocket config", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) { + ws.send(message); + }, + }, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Not a websocket"); + }, + }); + server.stop(); + }); + + test("websocket with all handlers", () => { + using server = serve({ + port: 0, + websocket: { + open(ws) {}, + message(ws, message) {}, + drain(ws) {}, + close(ws, code, reason) {}, + ping(ws, data) {}, + pong(ws, data) {}, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("websocket with custom limits", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) {}, + maxPayloadLength: 1024 * 1024, // 1MB + backpressureLimit: 1024 * 512, // 512KB + closeOnBackpressureLimit: true, + idleTimeout: 60, // 1 minute + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("websocket with compression options", () => { + using server = serve({ + port: 0, + websocket: { + message(ws, message) {}, + perMessageDeflate: { + compress: true, + decompress: "shared", + }, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve development options", () => { + test("development mode", () => { + using server = serve({ + development: true, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.development).toBe(true); + server.stop(); + }); + + test("custom server id", () => { + using server = serve({ + id: "test-server", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.id).toBe("test-server"); + server.stop(); + }); +}); + +describe("Bun.serve static routes", () => { + test("static route handling", () => { + using server = serve({ + port: 0, + static: { + "/": new Response("Home"), + "/about": new Response("About"), + }, + fetch() { + return new Response("Not found"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve unix socket", () => { + test("unix socket config", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + test("unix socket with websocket", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + websocket: { + message(ws, message) {}, + }, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); +}); + +describe("Bun.serve hostname and port validation", () => { + test("hostname with port 0 gets random port", () => { + using server = serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe("127.0.0.1"); + server.stop(); + }); + + test("port with no hostname gets default hostname", () => { + using server = serve({ + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.port).toBeGreaterThan(0); + expect(server.hostname).toBe(defaultHostname); // Default hostname + server.stop(); + }); + + test("hostname with unix should throw", () => { + expect(() => + serve({ + // @ts-expect-error - Testing invalid combination + hostname: defaultHostname, + unix: "test.sock", + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + test("unix with no hostname/port is valid", () => { + const tmpdir = tmpdirSync(); + using server = serve({ + unix: tmpdir + "/test.sock", + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }); + + describe("various valid hostnames", () => { + const validHostnames = [defaultHostname, "127.0.0.1", "0.0.0.0"]; + + for (const hostname of validHostnames) { + test(hostname, () => { + using server = serve({ + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(hostname); + server.stop(); + }); + } + }); + + describe("various port types", () => { + const validPorts = [ + [0, expect.any(Number)], // random port + ["0", expect.any(Number)], // random port as string + ] as const; + + for (const [input, expected] of validPorts) { + test(JSON.stringify(input), () => { + using server = serve({ + port: input, + fetch() { + return new Response("ok"); + }, + }); + + if (typeof expected === "object") { + expect(server.port).toBeGreaterThan(0); + } else { + expect(server.port).toBe(expected); + } + server.stop(); + }); + } + }); +}); + +describe("Bun.serve hostname coercion", () => { + test.todo("number hostnames coerce to string", () => { + using server = serve({ + // @ts-expect-error - Testing runtime coercion + hostname: 0, // Should coerce to "0" + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe("0"); + server.stop(); + }); + + test("object with toString() coerces to string", () => { + const customHostname = { + toString() { + return defaultHostname; + }, + }; + + using server = serve({ + // @ts-expect-error - Testing runtime coercion + hostname: customHostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("invalid toString() results should throw", () => { + const invalidHostnames = [ + { + toString() { + return {}; + }, + }, + { + toString() { + return []; + }, + }, + { + toString() { + return null; + }, + }, + { + toString() { + return undefined; + }, + }, + { + toString() { + throw new Error("invalid toString"); + }, + }, + { + toString() { + return Symbol("test"); + }, + }, + ]; + + for (const hostname of invalidHostnames) { + expect(() => + serve({ + // @ts-expect-error - Testing runtime coercion + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + } + }); + + test("symbol hostnames should throw", () => { + expect(() => + serve({ + // @ts-expect-error - Testing runtime behavior + hostname: Symbol("test"), + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + test("coerced hostnames must still be valid", () => { + const invalidCoercions = [ + { + toString() { + return "http://example.com"; + }, + }, + { + toString() { + return "example.com:3000"; + }, + }, + { + toString() { + return "-invalid.com"; + }, + }, + ]; + + for (const hostname of invalidCoercions) { + expect(() => + serve({ + // @ts-expect-error - Testing runtime coercion + hostname, + port: 0, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + } + }); + + describe("falsy values should use default or throw", () => { + test("undefined should use default", () => { + using server = serve({ + hostname: undefined, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + server.stop(); + }); + + test("null should NOT throw", () => { + expect(() => { + using server = serve({ + // @ts-expect-error - Testing runtime behavior + hostname: null, + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + }).not.toThrow(); + + test("empty string should be ignored", () => { + expect(() => { + using server = serve({ + hostname: "", + port: 0, + fetch() { + return new Response("ok"); + }, + }); + expect(server.hostname).toBe(defaultHostname); + }).not.toThrow(); + }); + }); + }); +}); + +describe("Bun.serve unix socket validation", () => { + test("unix socket with hostname should throw", () => { + expect(() => + serve({ + unix: "/tmp/test.sock", + // @ts-expect-error - Testing invalid combination + hostname: defaultHostname, // Cannot combine with unix + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + + describe("invalid unix socket paths should throw", () => { + const invalidPaths = [ + { + toString() { + throw new Error("invalid toString"); + }, + toJSON() { + return "invalid toJSON"; + }, + }, + { + toString() { + return Symbol("test"); + }, + toJSON() { + return "Symbol(test)"; + }, + }, + ]; + + for (const unix of invalidPaths) { + test(JSON.stringify(unix), () => { + expect(() => + serve({ + // @ts-expect-error - Testing invalid unix socket path + unix, + fetch() { + return new Response("ok"); + }, + }), + ).toThrow(); + }); + } + }); + + test("unix socket path coercion", () => { + // Number should coerce to string + using server = serve({ + // @ts-expect-error - Testing runtime coercion + unix: Math.ceil(Math.random() * 100000000), + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + + // Object with toString() + const pathObj = { + toString() { + return Math.random().toString(32).slice(2, 15) + ".sock"; + }, + }; + + using server2 = serve({ + // @ts-expect-error - Testing runtime coercion + unix: pathObj, + fetch() { + return new Response("ok"); + }, + }); + server2.stop(); + }); + + test("invalid unix socket path coercion should throw", () => { + const invalidCoercions = [ + { + toString() { + throw new Error("invalid toString"); + }, + }, + ]; + + for (const unix of invalidCoercions) { + expect(() => { + using server = serve({ + port: 0, + // @ts-expect-error - Testing runtime coercion + unix, + fetch() { + return new Response("ok"); + }, + }); + server.stop(); + }).toThrow(); + } + }); +}); From c434b2c191cc46de04728aead0f34abd3ff63bb9 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 25 Nov 2024 18:08:42 -0800 Subject: [PATCH 08/11] zig: make throwPretty use JSError (#15410) --- src/bun.js/api/JSBundler.zig | 6 +- src/bun.js/api/server.zig | 5 +- src/bun.js/bindings/bindings.zig | 27 +- src/bun.js/test/expect.zig | 569 +++++++++++-------------------- src/bun.js/test/jest.zig | 49 +-- src/shell/interpreter.zig | 6 +- src/shell/shell.zig | 9 +- 7 files changed, 224 insertions(+), 447 deletions(-) diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 9a7a276d76f410..fa535763df0859 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -322,15 +322,13 @@ pub const JSBundler = struct { defer path.deinit(); var dir = std.fs.cwd().openDir(path.slice(), .{}) catch |err| { - globalThis.throwPretty("{s}: failed to open root directory: {s}", .{ @errorName(err), path.slice() }); - return error.JSError; + return globalThis.throwPretty("{s}: failed to open root directory: {s}", .{ @errorName(err), path.slice() }); }; defer dir.close(); var rootdir_buf: bun.PathBuffer = undefined; const rootdir = bun.getFdPath(bun.toFD(dir.fd), &rootdir_buf) catch |err| { - globalThis.throwPretty("{s}: failed to get full root directory path: {s}", .{ @errorName(err), path.slice() }); - return error.JSError; + return globalThis.throwPretty("{s}: failed to get full root directory path: {s}", .{ @errorName(err), path.slice() }); }; try this.rootdir.appendSliceExact(rootdir); } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 4648fafbc9b45d..0ee0befe789b9e 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5330,7 +5330,7 @@ pub const ServerWebSocket = struct { callframe: *JSC.CallFrame, comptime name: string, comptime opcode: uws.Opcode, - ) JSValue { + ) bun.JSError!JSValue { const args = callframe.arguments_old(2); if (this.isClosed()) { @@ -5377,8 +5377,7 @@ pub const ServerWebSocket = struct { }, } } else { - globalThis.throwPretty("{s} requires a string or BufferSource", .{name}); - return .zero; + return globalThis.throwPretty("{s} requires a string or BufferSource", .{name}); } } } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index cfeaee4b8983c0..f4885080188a4e 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3303,37 +3303,24 @@ pub const JSGlobalObject = opaque { return err; } - pub fn throw( - this: *JSGlobalObject, - comptime fmt: [:0]const u8, - args: anytype, - ) void { + pub fn throw(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) void { const instance = this.createErrorInstance(fmt, args); - if (instance != .zero) - this.vm().throwError(this, instance); + bun.assert(instance != .zero); + this.vm().throwError(this, instance); } - pub fn throw2( - this: *JSGlobalObject, - comptime fmt: [:0]const u8, - args: anytype, - ) JSError { + pub fn throw2(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) JSError { const instance = this.createErrorInstance(fmt, args); bun.assert(instance != .zero); return this.vm().throwError2(this, instance); } - pub fn throwPretty( - this: *JSGlobalObject, - comptime fmt: [:0]const u8, - args: anytype, - ) void { + pub fn throwPretty(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) bun.JSError { const instance = switch (Output.enable_ansi_colors) { inline else => |enabled| this.createErrorInstance(Output.prettyFmt(fmt, enabled), args), }; - - if (instance != .zero) - this.vm().throwError(this, instance); + bun.assert(instance != .zero); + return this.vm().throwError2(this, instance); } extern fn JSC__JSGlobalObject__queueMicrotaskCallback(*JSGlobalObject, *anyopaque, Function: *const (fn (*anyopaque) callconv(.C) void)) void; pub fn queueMicrotaskCallback( diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 34fc02a4b5037e..6437dcfb4dd15e 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -137,7 +137,7 @@ pub const Expect = struct { return received ++ matcher_name ++ "(" ++ args ++ ")"; } - pub fn throwPrettyMatcherError(globalThis: *JSGlobalObject, custom_label: bun.String, matcher_name: anytype, matcher_params: anytype, flags: Flags, comptime message_fmt: string, message_args: anytype) void { + pub fn throwPrettyMatcherError(globalThis: *JSGlobalObject, custom_label: bun.String, matcher_name: anytype, matcher_params: anytype, flags: Flags, comptime message_fmt: string, message_args: anytype) bun.JSError { switch (Output.enable_ansi_colors) { inline else => |colors| { const chain = switch (flags.promise) { @@ -149,16 +149,10 @@ pub const Expect = struct { inline else => |use_default_label| { if (use_default_label) { const fmt = comptime Output.prettyFmt("expect(received).{s}{s}({s})\n\n" ++ message_fmt, colors); - globalThis.throwPretty(fmt, .{ - chain, - matcher_name, - matcher_params, - } ++ message_args); + return globalThis.throwPretty(fmt, .{ chain, matcher_name, matcher_params } ++ message_args); } else { const fmt = comptime Output.prettyFmt("{}\n\n" ++ message_fmt, colors); - globalThis.throwPretty(fmt, .{ - custom_label, - } ++ message_args); + return globalThis.throwPretty(fmt, .{custom_label} ++ message_args); } }, } @@ -227,7 +221,7 @@ pub const Expect = struct { if (!silent) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; const message = "Expected promise that rejects\nReceived promise that resolved: {any}\n"; - throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); + return throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); } return error.JSError; }, @@ -239,7 +233,7 @@ pub const Expect = struct { if (!silent) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; const message = "Expected promise that resolves\nReceived promise that rejected: {any}\n"; - throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); + return throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); } return error.JSError; }, @@ -254,7 +248,7 @@ pub const Expect = struct { if (!silent) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; const message = "Expected promise\nReceived: {any}\n"; - throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); + return throwPrettyMatcherError(globalThis, custom_label, matcher_name, matcher_params, flags, message, .{value.toFmt(&formatter)}); } return error.JSError; } @@ -402,11 +396,11 @@ pub const Expect = struct { return expect_js_value; } - pub fn throw(this: *Expect, globalThis: *JSGlobalObject, comptime signature: [:0]const u8, comptime fmt: [:0]const u8, args: anytype) void { + pub fn throw(this: *Expect, globalThis: *JSGlobalObject, comptime signature: [:0]const u8, comptime fmt: [:0]const u8, args: anytype) bun.JSError { if (this.custom_label.isEmpty()) { - globalThis.throwPretty(signature ++ fmt, args); + return globalThis.throwPretty(signature ++ fmt, args); } else { - globalThis.throwPretty("{}" ++ fmt, .{this.custom_label} ++ args); + return globalThis.throwPretty("{}" ++ fmt, .{this.custom_label} ++ args); } } @@ -454,8 +448,7 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("pass", "", true); - this.throw(globalThis, signature, "\n\n{s}\n", .{msg.slice()}); - return .zero; + return this.throw(globalThis, signature, "\n\n{s}\n", .{msg.slice()}); } // should never reach here @@ -500,8 +493,7 @@ pub const Expect = struct { defer msg.deinit(); const signature = comptime getSignature("fail", "", true); - this.throw(globalThis, signature, "\n\n{s}\n", .{msg.slice()}); - return .zero; + return this.throw(globalThis, signature, "\n\n{s}\n", .{msg.slice()}); } /// Object.is() @@ -537,8 +529,7 @@ pub const Expect = struct { inline else => |has_custom_label| { if (not) { const signature = comptime getSignature("toBe", "expected", true); - this.throw(globalThis, signature, "\n\nExpected: not {any}\n", .{right.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected: not {any}\n", .{right.toFmt(&formatter)}); } const signature = comptime getSignature("toBe", "expected", false); @@ -547,8 +538,7 @@ pub const Expect = struct { (if (!has_custom_label) "\n\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" else "") ++ "\n\nExpected: {any}\n" ++ "Received: serializes to the same string\n"; - this.throw(globalThis, signature, fmt, .{right.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, fmt, .{right.toFmt(&formatter)}); } if (right.isString() and left.isString()) { @@ -558,15 +548,13 @@ pub const Expect = struct { .globalThis = globalThis, .not = not, }; - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_format}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_format}); } - this.throw(globalThis, signature, "\n\nExpected: {any}\nReceived: {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected: {any}\nReceived: {any}\n", .{ right.toFmt(&formatter), left.toFmt(&formatter), }); - return .zero; }, } } @@ -634,15 +622,13 @@ pub const Expect = struct { if (not) { const expected_line = "Expected length: not {d}\n"; const signature = comptime getSignature("toHaveLength", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{expected_length}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{expected_length}); } const expected_line = "Expected length: {d}\n"; const received_line = "Received length: {d}\n"; const signature = comptime getSignature("toHaveLength", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_length, actual_length }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_length, actual_length }); } pub fn toBeOneOf( @@ -719,15 +705,13 @@ pub const Expect = struct { const received_fmt = list_value.toFmt(&formatter); const expected_line = "Expected to not be one of: {any}\nReceived: {any}\n"; const signature = comptime getSignature("toBeOneOf", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ received_fmt, expected_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ received_fmt, expected_fmt }); } const expected_line = "Expected to be one of: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeOneOf", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ value_fmt, expected_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ value_fmt, expected_fmt }); } pub fn toContain( @@ -816,15 +800,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const signature = comptime getSignature("toContain", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toContain", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toContainKey( @@ -870,15 +852,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const signature = comptime getSignature("toContainKey", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toContainKey", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toContainKeys( @@ -942,15 +922,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const signature = comptime getSignature("toContainKeys", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toContainKeys", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toContainAllKeys( @@ -1009,15 +987,13 @@ pub const Expect = struct { const received_fmt = keys.toFmt(&formatter); const expected_line = "Expected to not contain all keys: {any}\nReceived: {any}\n"; const fmt = "\n\n" ++ expected_line; - this.throw(globalObject, comptime getSignature("toContainAllKeys", "expected", true), fmt, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAllKeys", "expected", true), fmt, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain all keys: {any}\n"; const received_line = "Received: {any}\n"; const fmt = "\n\n" ++ expected_line ++ received_line; - this.throw(globalObject, comptime getSignature("toContainAllKeys", "expected", false), fmt, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAllKeys", "expected", false), fmt, .{ expected_fmt, value_fmt }); } pub fn toContainAnyKeys( @@ -1076,15 +1052,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const signature = comptime getSignature("toContainAnyKeys", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toContainAnyKeys", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toContainValue( @@ -1132,15 +1106,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const fmt = "\n\n" ++ expected_line; - this.throw(globalObject, comptime getSignature("toContainValue", "expected", true), fmt, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainValue", "expected", true), fmt, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const fmt = "\n\n" ++ expected_line ++ received_line; - this.throw(globalObject, comptime getSignature("toContainValue", "expected", false), fmt, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainValue", "expected", false), fmt, .{ expected_fmt, value_fmt }); } pub fn toContainValues( @@ -1198,15 +1170,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain: {any}\nReceived: {any}\n"; const fmt = "\n\n" ++ expected_line; - this.throw(globalObject, comptime getSignature("toContainValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const fmt = "\n\n" ++ expected_line ++ received_line; - this.throw(globalObject, comptime getSignature("toContainValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); } pub fn toContainAllValues( @@ -1270,15 +1240,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain all values: {any}\nReceived: {any}\n"; const fmt = "\n\n" ++ expected_line; - this.throw(globalObject, comptime getSignature("toContainAllValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAllValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain all values: {any}\n"; const received_line = "Received: {any}\n"; const fmt = "\n\n" ++ expected_line ++ received_line; - this.throw(globalObject, comptime getSignature("toContainAllValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAllValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); } pub fn toContainAnyValues( @@ -1336,15 +1304,13 @@ pub const Expect = struct { const received_fmt = value.toFmt(&formatter); const expected_line = "Expected to not contain any of the following values: {any}\nReceived: {any}\n"; const fmt = "\n\n" ++ expected_line; - this.throw(globalObject, comptime getSignature("toContainAnyValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAnyValues", "expected", true), fmt, .{ expected_fmt, received_fmt }); } const expected_line = "Expected to contain any of the following values: {any}\n"; const received_line = "Received: {any}\n"; const fmt = "\n\n" ++ expected_line ++ received_line; - this.throw(globalObject, comptime getSignature("toContainAnyValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalObject, comptime getSignature("toContainAnyValues", "expected", false), fmt, .{ expected_fmt, value_fmt }); } pub fn toContainEqual( @@ -1441,15 +1407,13 @@ pub const Expect = struct { if (not) { const expected_line = "Expected to not contain: {any}\n"; const signature = comptime getSignature("toContainEqual", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line, .{expected_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line, .{expected_fmt}); } const expected_line = "Expected to contain: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toContainEqual", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeTruthy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1474,14 +1438,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeTruthy", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeTruthy", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeUndefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1504,14 +1466,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeUndefined", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeUndefined", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeNaN(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1538,14 +1498,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeNaN", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeNaN", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeNull(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1567,14 +1525,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeNull", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeNull", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeDefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1596,14 +1552,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeDefined", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeDefined", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeFalsy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1630,14 +1584,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeFalsy", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeFalsy", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1672,13 +1624,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toEqual", "expected", true); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } const signature = comptime getSignature("toEqual", "expected", false); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } pub fn toStrictEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1708,13 +1658,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toStrictEqual", "expected", true); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } const signature = comptime getSignature("toStrictEqual", "expected", false); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } pub fn toHaveProperty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1767,20 +1715,18 @@ pub const Expect = struct { if (expected_property != null) { const signature = comptime getSignature("toHaveProperty", "path, value", true); if (received_property != .zero) { - this.throw(globalThis, signature, "\n\nExpected path: {any}\n\nExpected value: not {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected path: {any}\n\nExpected value: not {any}\n", .{ expected_property_path.toFmt(&formatter), expected_property.?.toFmt(&formatter), }); - return .zero; } } const signature = comptime getSignature("toHaveProperty", "path", true); - this.throw(globalThis, signature, "\n\nExpected path: not {any}\n\nReceived value: {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected path: not {any}\n\nReceived value: {any}\n", .{ expected_property_path.toFmt(&formatter), received_property.toFmt(&formatter), }); - return .zero; } if (expected_property != null) { @@ -1793,22 +1739,19 @@ pub const Expect = struct { .globalThis = globalThis, }; - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_format}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_format}); } const fmt = "\n\nExpected path: {any}\n\nExpected value: {any}\n\n" ++ "Unable to find property\n"; - this.throw(globalThis, signature, fmt, .{ + return this.throw(globalThis, signature, fmt, .{ expected_property_path.toFmt(&formatter), expected_property.?.toFmt(&formatter), }); - return .zero; } const signature = comptime getSignature("toHaveProperty", "path", false); - this.throw(globalThis, signature, "\n\nExpected path: {any}\n\nUnable to find property\n", .{expected_property_path.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected path: {any}\n\nUnable to find property\n", .{expected_property_path.toFmt(&formatter)}); } pub fn toBeEven(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1855,14 +1798,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeEven", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeEven", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toBeGreaterThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1916,15 +1857,13 @@ pub const Expect = struct { const expected_line = "Expected: not \\> {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeGreaterThan", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected: \\> {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeGreaterThan", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeGreaterThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -1978,15 +1917,13 @@ pub const Expect = struct { const expected_line = "Expected: not \\>= {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeGreaterThanOrEqual", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected: \\>= {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeGreaterThanOrEqual", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeLessThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2040,15 +1977,13 @@ pub const Expect = struct { const expected_line = "Expected: not \\< {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeLessThan", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected: \\< {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeLessThan", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeLessThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2102,15 +2037,13 @@ pub const Expect = struct { const expected_line = "Expected: not \\<= {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeLessThanOrEqual", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected: \\<= {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeLessThanOrEqual", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeCloseTo(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2186,13 +2119,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeCloseTo", "expected, precision", true); - this.throw(globalThis, signature, suffix_fmt, .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; + return this.throw(globalThis, signature, suffix_fmt, .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); } const signature = comptime getSignature("toBeCloseTo", "expected, precision", false); - this.throw(globalThis, signature, suffix_fmt, .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); - return .zero; + return this.throw(globalThis, signature, suffix_fmt, .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); } pub fn toBeOdd(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2237,14 +2168,12 @@ pub const Expect = struct { if (not) { const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeOdd", "", true); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeOdd", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{value_fmt}); } pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2352,17 +2281,15 @@ pub const Expect = struct { const name = try err.getTruthyComptime(globalThis, "name") orelse JSValue.undefined; const message = try err.getTruthyComptime(globalThis, "message") orelse JSValue.undefined; const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; - globalThis.throwPretty(fmt, .{ + return globalThis.throwPretty(fmt, .{ name.toFmt(&formatter), message.toFmt(&formatter), }); - return .zero; } // non error thrown const fmt = signature_no_args ++ "\n\nThrown value: {any}\n"; - globalThis.throwPretty(fmt, .{result.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{result.toFmt(&formatter)}); } if (expected_value.isString()) { @@ -2384,11 +2311,10 @@ pub const Expect = struct { if (!strings.contains(received_slice.slice(), expected_slice.slice())) return .undefined; } - this.throw(globalThis, signature, "\n\nExpected substring: not {any}\nReceived message: {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected substring: not {any}\nReceived message: {any}\n", .{ expected_value.toFmt(&formatter), received_message.toFmt(&formatter), }); - return .zero; } if (expected_value.isRegExp()) { @@ -2406,11 +2332,10 @@ pub const Expect = struct { if (!matches.toBoolean()) return .undefined; } - this.throw(globalThis, signature, "\n\nExpected pattern: not {any}\nReceived message: {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected pattern: not {any}\nReceived message: {any}\n", .{ expected_value.toFmt(&formatter), received_message.toFmt(&formatter), }); - return .zero; } if (expected_value.fastGet(globalThis, .message)) |expected_message| { @@ -2425,8 +2350,7 @@ pub const Expect = struct { // no partial match for this case if (!expected_message.isSameValue(received_message, globalThis)) return .undefined; - this.throw(globalThis, signature, "\n\nExpected message: not {any}\n", .{expected_message.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected message: not {any}\n", .{expected_message.toFmt(&formatter)}); } if (!result.isInstanceOf(globalThis, expected_value)) return .undefined; @@ -2434,8 +2358,7 @@ pub const Expect = struct { var expected_class = ZigString.Empty; expected_value.getClassName(globalThis, &expected_class); const received_message = result.fastGet(globalThis, .message) orelse .undefined; - this.throw(globalThis, signature, "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n", .{ expected_class, received_message.toFmt(&formatter) }); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n", .{ expected_class, received_message.toFmt(&formatter) }); } if (did_throw) { @@ -2472,15 +2395,12 @@ pub const Expect = struct { if (_received_message) |received_message| { const expected_value_fmt = expected_value.toFmt(&formatter); const received_message_fmt = received_message.toFmt(&formatter); - this.throw(globalThis, signature, "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n", .{ expected_value_fmt, received_message_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected substring: {any}\nReceived message: {any}\n", .{ expected_value_fmt, received_message_fmt }); } const expected_fmt = expected_value.toFmt(&formatter); const received_fmt = result.toFmt(&formatter); - this.throw(globalThis, signature, "\n\n" ++ "Expected substring: {any}\nReceived value: {any}", .{ expected_fmt, received_fmt }); - - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected substring: {any}\nReceived value: {any}", .{ expected_fmt, received_fmt }); } if (expected_value.isRegExp()) { @@ -2500,16 +2420,13 @@ pub const Expect = struct { const received_message_fmt = received_message.toFmt(&formatter); const signature = comptime getSignature("toThrow", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n", .{ expected_value_fmt, received_message_fmt }); - - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected pattern: {any}\nReceived message: {any}\n", .{ expected_value_fmt, received_message_fmt }); } const expected_fmt = expected_value.toFmt(&formatter); const received_fmt = result.toFmt(&formatter); const signature = comptime getSignature("toThrow", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}", .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected pattern: {any}\nReceived value: {any}", .{ expected_fmt, received_fmt }); } if (Expect.isAsymmetricMatcher(expected_value)) { @@ -2527,8 +2444,7 @@ pub const Expect = struct { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; const received_fmt = result.toFmt(&formatter); const expected_fmt = expected_value.toFmt(&formatter); - this.throw(globalThis, signature, "\n\nExpected value: {any}\nReceived value: {any}\n", .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected value: {any}\nReceived value: {any}\n", .{ expected_fmt, received_fmt }); } // If it's not an object, we are going to crash here. @@ -2547,14 +2463,12 @@ pub const Expect = struct { if (_received_message) |received_message| { const expected_fmt = expected_message.toFmt(&formatter); const received_fmt = received_message.toFmt(&formatter); - this.throw(globalThis, signature, "\n\nExpected message: {any}\nReceived message: {any}\n", .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected message: {any}\nReceived message: {any}\n", .{ expected_fmt, received_fmt }); } const expected_fmt = expected_message.toFmt(&formatter); const received_fmt = result.toFmt(&formatter); - this.throw(globalThis, signature, "\n\nExpected message: {any}\nReceived value: {any}\n", .{ expected_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected message: {any}\nReceived value: {any}\n", .{ expected_fmt, received_fmt }); } if (result.isInstanceOf(globalThis, expected_value)) return .undefined; @@ -2572,23 +2486,21 @@ pub const Expect = struct { const message_fmt = fmt ++ "Received message: {any}\n"; const received_message_fmt = received_message.toFmt(&formatter); - globalThis.throwPretty(message_fmt, .{ + return globalThis.throwPretty(message_fmt, .{ expected_class, received_class, received_message_fmt, }); - return .zero; } const received_fmt = result.toFmt(&formatter); const value_fmt = fmt ++ "Received value: {any}\n"; - globalThis.throwPretty(value_fmt, .{ + return globalThis.throwPretty(value_fmt, .{ expected_class, received_class, received_fmt, }); - return .zero; } // did not throw @@ -2598,35 +2510,30 @@ pub const Expect = struct { if (expected_value == .zero or expected_value.isUndefined()) { const signature = comptime getSignature("toThrow", "", false); - this.throw(globalThis, signature, "\n\n" ++ received_line, .{result.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ received_line, .{result.toFmt(&formatter)}); } const signature = comptime getSignature("toThrow", "expected", false); if (expected_value.isString()) { const expected_fmt = "\n\nExpected substring: {any}\n\n" ++ received_line; - this.throw(globalThis, signature, expected_fmt, .{ expected_value.toFmt(&formatter), result.toFmt(&formatter) }); - return .zero; + return this.throw(globalThis, signature, expected_fmt, .{ expected_value.toFmt(&formatter), result.toFmt(&formatter) }); } if (expected_value.isRegExp()) { const expected_fmt = "\n\nExpected pattern: {any}\n\n" ++ received_line; - this.throw(globalThis, signature, expected_fmt, .{ expected_value.toFmt(&formatter), result.toFmt(&formatter) }); - return .zero; + return this.throw(globalThis, signature, expected_fmt, .{ expected_value.toFmt(&formatter), result.toFmt(&formatter) }); } if (expected_value.fastGet(globalThis, .message)) |expected_message| { const expected_fmt = "\n\nExpected message: {any}\n\n" ++ received_line; - this.throw(globalThis, signature, expected_fmt, .{ expected_message.toFmt(&formatter), result.toFmt(&formatter) }); - return .zero; + return this.throw(globalThis, signature, expected_fmt, .{ expected_message.toFmt(&formatter), result.toFmt(&formatter) }); } const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; var expected_class = ZigString.Empty; expected_value.getClassName(globalThis, &expected_class); - this.throw(globalThis, signature, expected_fmt, .{ expected_class, result.toFmt(&formatter) }); - return .zero; + return this.throw(globalThis, signature, expected_fmt, .{ expected_class, result.toFmt(&formatter) }); } pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); @@ -2639,13 +2546,12 @@ pub const Expect = struct { const not = this.flags.not; if (not) { const signature = comptime getSignature("toMatchSnapshot", "", true); - this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used with not\n", .{}); } if (this.testScope() == null) { const signature = comptime getSignature("toMatchSnapshot", "", true); - this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); - return .zero; + return this.throw(globalThis, signature, "\n\nMatcher error: Snapshot matchers cannot be used outside of a test\n", .{}); } var hint_string: ZigString = ZigString.Empty; @@ -2662,8 +2568,7 @@ pub const Expect = struct { else => { if (!arguments[0].isObject()) { const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - this.throw(globalThis, signature, "\n\nMatcher error: Expected properties must be an object\n", .{}); - return .zero; + return this.throw(globalThis, signature, "\n\nMatcher error: Expected properties must be an object\n", .{}); } property_matchers = arguments[0]; @@ -2681,8 +2586,7 @@ pub const Expect = struct { if (!value.isObject() and property_matchers != null) { const signature = comptime getSignature("toMatchSnapshot", "properties, hint", false); - this.throw(globalThis, signature, "\n\nMatcher error: received values must be an object when the matcher has properties\n", .{}); - return .zero; + return this.throw(globalThis, signature, "\n\nMatcher error: received values must be an object when the matcher has properties\n", .{}); } if (property_matchers) |_prop_matchers| { @@ -2695,8 +2599,7 @@ pub const Expect = struct { "\n\nReceived: {any}\n"; var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); } } @@ -2736,8 +2639,7 @@ pub const Expect = struct { .globalThis = globalThis, }; - globalThis.throwPretty(fmt, .{diff_format}); - return .zero; + return globalThis.throwPretty(fmt, .{diff_format}); } return .undefined; @@ -2785,8 +2687,7 @@ pub const Expect = struct { const signature = comptime getSignature("toBeEmpty", "", false); const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++ "\n\nReceived: {any}\n"; - globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); } } else if (std.math.isNan(actual_length)) { globalThis.throw("Received value has non-number length property: {}", .{actual_length}); @@ -2799,8 +2700,7 @@ pub const Expect = struct { const signature = comptime getSignature("toBeEmpty", "", true); const fmt = signature ++ "\n\nExpected value not to be a string, object, or iterable" ++ "\n\nReceived: {any}\n"; - globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); } if (not) pass = !pass; @@ -2810,15 +2710,13 @@ pub const Expect = struct { const signature = comptime getSignature("toBeEmpty", "", true); const fmt = signature ++ "\n\nExpected value not to be empty" ++ "\n\nReceived: {any}\n"; - globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); } const signature = comptime getSignature("toBeEmpty", "", false); const fmt = signature ++ "\n\nExpected value to be empty" ++ "\n\nReceived: {any}\n"; - globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)}); } pub fn toBeEmptyObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2840,13 +2738,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeEmptyObject", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeEmptyObject", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2867,13 +2763,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeNil", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeNil", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2894,13 +2788,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeArray", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeArray", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2937,13 +2829,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeArrayOfSize", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeArrayOfSize", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -2964,13 +2854,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeBoolean", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeBoolean", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3038,13 +2926,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeTypeOf", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n", .{ expected_str, whatIsTheType, received }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected type: not {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n", .{ expected_str, whatIsTheType, received }); } const signature = comptime getSignature("toBeTypeOf", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n", .{ expected_str, whatIsTheType, received }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected type: {any}\n" ++ "Received type: \"{s}\"\nReceived value: {any}\n", .{ expected_str, whatIsTheType, received }); } pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3065,13 +2951,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeTrue", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeTrue", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3092,13 +2976,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeFalse", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeFalse", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3119,13 +3001,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeNumber", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeNumber", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3146,13 +3026,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeInteger", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeInteger", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3173,13 +3051,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeObject", "", true); - this.throw(globalThis, signature, "\n\nExpected value not to be an object" ++ "\n\nReceived: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected value not to be an object" ++ "\n\nReceived: {any}\n", .{received}); } const signature = comptime getSignature("toBeObject", "", false); - this.throw(globalThis, signature, "\n\nExpected value to be an object" ++ "\n\nReceived: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected value to be an object" ++ "\n\nReceived: {any}\n", .{received}); } pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3206,13 +3082,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeFinite", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeFinite", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3239,13 +3113,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBePositive", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBePositive", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3272,13 +3144,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeNegative", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeNegative", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3332,15 +3202,13 @@ pub const Expect = struct { const expected_line = "Expected: not between {any} (inclusive) and {any} (exclusive)\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeWithin", "start, end", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ start_fmt, end_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ start_fmt, end_fmt, received_fmt }); } const expected_line = "Expected: between {any} (inclusive) and {any} (exclusive)\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toBeWithin", "start, end", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ start_fmt, end_fmt, received_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ start_fmt, end_fmt, received_fmt }); } pub fn toEqualIgnoringWhitespace(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3415,13 +3283,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toEqualIgnoringWhitespace", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ "Expected: not {any}\n" ++ "Received: {any}\n", .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected: not {any}\n" ++ "Received: {any}\n", .{ expected_fmt, value_fmt }); } const signature = comptime getSignature("toEqualIgnoringWhitespace", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected: {any}\n" ++ "Received: {any}\n", .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected: {any}\n" ++ "Received: {any}\n", .{ expected_fmt, value_fmt }); } pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3442,13 +3308,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeSymbol", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeSymbol", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3469,13 +3333,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeFunction", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeFunction", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3496,13 +3358,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeDate", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeDate", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeValidDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3524,13 +3384,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeValidDate", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeValidDate", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3551,13 +3409,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toBeString", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } const signature = comptime getSignature("toBeString", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}\n", .{received}); } pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3605,15 +3461,13 @@ pub const Expect = struct { const expected_line = "Expected to not include: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toInclude", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected to include: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toInclude", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toIncludeRepeated(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3695,35 +3549,31 @@ pub const Expect = struct { if (countAsNum == 0) { const expected_line = "Expected to include: {any} \n"; const signature = comptime getSignature("toIncludeRepeated", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); } else if (countAsNum == 1) { const expected_line = "Expected not to include: {any} \n"; const signature = comptime getSignature("toIncludeRepeated", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); } else { const expected_line = "Expected not to include: {any} {any} times \n"; const signature = comptime getSignature("toIncludeRepeated", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, times_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, times_fmt, expect_string_fmt }); } - - return .zero; } if (countAsNum == 0) { const expected_line = "Expected to not include: {any}\n"; const signature = comptime getSignature("toIncludeRepeated", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); } else if (countAsNum == 1) { const expected_line = "Expected to include: {any}\n"; const signature = comptime getSignature("toIncludeRepeated", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, expect_string_fmt }); } else { const expected_line = "Expected to include: {any} {any} times \n"; const signature = comptime getSignature("toIncludeRepeated", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, times_fmt, expect_string_fmt }); + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ substring_fmt, times_fmt, expect_string_fmt }); } - - return .zero; } pub fn toSatisfy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3769,18 +3619,15 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toSatisfy", "expected", true); - this.throw(globalThis, signature, "\n\nExpected: not {any}\n", .{predicate.toFmt(&formatter)}); - return .zero; + return this.throw(globalThis, signature, "\n\nExpected: not {any}\n", .{predicate.toFmt(&formatter)}); } const signature = comptime getSignature("toSatisfy", "expected", false); - this.throw(globalThis, signature, "\n\nExpected: {any}\nReceived: {any}\n", .{ + return this.throw(globalThis, signature, "\n\nExpected: {any}\nReceived: {any}\n", .{ predicate.toFmt(&formatter), value.toFmt(&formatter), }); - - return .zero; } pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3828,15 +3675,13 @@ pub const Expect = struct { const expected_line = "Expected to not start with: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toStartWith", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected to start with: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toStartWith", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3884,15 +3729,13 @@ pub const Expect = struct { const expected_line = "Expected to not end with: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toEndWith", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected to end with: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toEndWith", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toBeInstanceOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3930,15 +3773,13 @@ pub const Expect = struct { const expected_line = "Expected constructor: not {any}\n"; const received_line = "Received value: {any}\n"; const signature = comptime getSignature("toBeInstanceOf", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected constructor: {any}\n"; const received_line = "Received value: {any}\n"; const signature = comptime getSignature("toBeInstanceOf", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toMatch(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -3993,15 +3834,13 @@ pub const Expect = struct { const expected_line = "Expected substring or pattern: not {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toMatch", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } const expected_line = "Expected substring or pattern: {any}\n"; const received_line = "Received: {any}\n"; const signature = comptime getSignature("toMatch", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ expected_line ++ received_line, .{ expected_fmt, value_fmt }); } pub fn toHaveBeenCalled(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4028,13 +3867,11 @@ pub const Expect = struct { // handle failure if (not) { const signature = comptime getSignature("toHaveBeenCalled", "", true); - this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 0\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: 0\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); } const signature = comptime getSignature("toHaveBeenCalled", "", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: \\>= 1\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: \\>= 1\n" ++ "Received number of calls: {any}\n", .{calls.getLength(globalThis)}); } pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4070,13 +3907,11 @@ pub const Expect = struct { // handle failure if (not) { const signature = comptime getSignature("toHaveBeenCalledTimes", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: not {any}\n" ++ "Received number of calls: {any}\n", .{ times, calls.getLength(globalThis) }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: not {any}\n" ++ "Received number of calls: {any}\n", .{ times, calls.getLength(globalThis) }); } const signature = comptime getSignature("toHaveBeenCalledTimes", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: {any}\n" ++ "Received number of calls: {any}\n", .{ times, calls.getLength(globalThis) }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of calls: {any}\n" ++ "Received number of calls: {any}\n", .{ times, calls.getLength(globalThis) }); } pub fn toMatchObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { @@ -4096,25 +3931,21 @@ pub const Expect = struct { const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; if (not) { const signature = comptime getSignature("toMatchObject", "expected", true); - this.throw(globalThis, signature, matcher_error, .{}); - return .zero; + return this.throw(globalThis, signature, matcher_error, .{}); } const signature = comptime getSignature("toMatchObject", "expected", false); - this.throw(globalThis, signature, matcher_error, .{}); - return .zero; + return this.throw(globalThis, signature, matcher_error, .{}); } if (args.len < 1 or !args[0].isObject()) { const matcher_error = "\n\nMatcher error: expected value must be a non-null object\n"; if (not) { const signature = comptime getSignature("toMatchObject", "", true); - this.throw(globalThis, signature, matcher_error, .{}); - return .zero; + return this.throw(globalThis, signature, matcher_error, .{}); } const signature = comptime getSignature("toMatchObject", "", false); - this.throw(globalThis, signature, matcher_error, .{}); - return .zero; + return this.throw(globalThis, signature, matcher_error, .{}); } const property_matchers = args[0]; @@ -4134,13 +3965,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toMatchObject", "expected", true); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } const signature = comptime getSignature("toMatchObject", "expected", false); - this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); - return .zero; + return this.throw(globalThis, signature, "\n\n{any}\n", .{diff_formatter}); } pub fn toHaveBeenCalledWith(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4197,13 +4026,11 @@ pub const Expect = struct { // handle failure if (not) { const signature = comptime getSignature("toHaveBeenCalledWith", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ "Number of calls: {any}\n", .{calls.getLength(globalThis)}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Number of calls: {any}\n", .{calls.getLength(globalThis)}); } const signature = comptime getSignature("toHaveBeenCalledWith", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Number of calls: {any}\n", .{calls.getLength(globalThis)}); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Number of calls: {any}\n", .{calls.getLength(globalThis)}); } pub fn toHaveBeenLastCalledWith(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4259,13 +4086,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toHaveBeenLastCalledWith", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ received_fmt, totalCalls }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ received_fmt, totalCalls }); } const signature = comptime getSignature("toHaveBeenLastCalledWith", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ received_fmt, totalCalls }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ received_fmt, totalCalls }); } pub fn toHaveBeenNthCalledWith(this: *Expect, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { @@ -4326,13 +4151,11 @@ pub const Expect = struct { if (not) { const signature = comptime getSignature("toHaveBeenNthCalledWith", "expected", true); - this.throw(globalThis, signature, "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ nthCallNum, received_fmt, totalCalls }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ nthCallNum, received_fmt, totalCalls }); } const signature = comptime getSignature("toHaveBeenNthCalledWith", "expected", false); - this.throw(globalThis, signature, "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ nthCallNum, received_fmt, totalCalls }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n", .{ nthCallNum, received_fmt, totalCalls }); } const ReturnStatus = enum { @@ -4416,15 +4239,13 @@ pub const Expect = struct { .globalThis = globalThis, .quote_strings = true, }; - globalThis.throwPretty(fmt, .{(try times_value.get(globalThis, "value")).?.toFmt(&formatter)}); - return .zero; + return globalThis.throwPretty(fmt, .{(try times_value.get(globalThis, "value")).?.toFmt(&formatter)}); } switch (not) { inline else => |is_not| { const signature = comptime getSignature(name, "expected", is_not); - this.throw(globalThis, signature, "\n\n" ++ "Expected number of successful calls: {d}\n" ++ "Received number of calls: {d}\n", .{ return_count, total_count }); - return .zero; + return this.throw(globalThis, signature, "\n\n" ++ "Expected number of successful calls: {d}\n" ++ "Received number of calls: {d}\n", .{ return_count, total_count }); }, } } @@ -4489,8 +4310,7 @@ pub const Expect = struct { const args = callFrame.arguments_old(1).slice(); if (args.len == 0 or !args[0].isObject()) { - globalThis.throwPretty("expect.extend(matchers)\n\nExpected an object containing matchers\n", .{}); - return .zero; + return globalThis.throwPretty("expect.extend(matchers)\n\nExpected an object containing matchers\n", .{}); } var expect_proto = Expect__getPrototype(globalThis); @@ -4699,7 +4519,7 @@ pub const Expect = struct { .globalThis = globalThis, .matcher_fn = matcher_fn, }; - throwPrettyMatcherError(globalThis, bun.String.empty, matcher_name, matcher_params, .{}, "{s}", .{message_text}); + throwPrettyMatcherError(globalThis, bun.String.empty, matcher_name, matcher_params, .{}, "{s}", .{message_text}) catch {}; return false; } @@ -4998,8 +4818,7 @@ pub const ExpectStringMatching = struct { if (args.len == 0 or (!args[0].isString() and !args[0].isRegExp())) { const fmt = "expect.stringContaining(string)\n\nExpected a string or regular expression\n"; - globalThis.throwPretty(fmt, .{}); - return .zero; + return globalThis.throwPretty(fmt, .{}); } const test_value = args[0]; @@ -5034,8 +4853,7 @@ pub const ExpectCloseTo = struct { const args = callFrame.arguments_old(2).slice(); if (args.len == 0 or !args[0].isNumber()) { - globalThis.throwPretty("expect.closeTo(number, precision?)\n\nExpected a number value", .{}); - return .zero; + return globalThis.throwPretty("expect.closeTo(number, precision?)\n\nExpected a number value", .{}); } const number_value = args[0]; @@ -5044,8 +4862,7 @@ pub const ExpectCloseTo = struct { precision_value = JSValue.jsNumberFromInt32(2); // default value from jest } if (!precision_value.isNumber()) { - globalThis.throwPretty("expect.closeTo(number, precision?)\n\nPrecision must be a number or undefined", .{}); - return .zero; + return globalThis.throwPretty("expect.closeTo(number, precision?)\n\nPrecision must be a number or undefined", .{}); } const instance = globalThis.bunVM().allocator.create(ExpectCloseTo) catch { @@ -5082,8 +4899,7 @@ pub const ExpectObjectContaining = struct { if (args.len == 0 or !args[0].isObject()) { const fmt = "expect.objectContaining(object)\n\nExpected an object\n"; - globalThis.throwPretty(fmt, .{}); - return .zero; + return globalThis.throwPretty(fmt, .{}); } const object_value = args[0]; @@ -5119,8 +4935,7 @@ pub const ExpectStringContaining = struct { if (args.len == 0 or !args[0].isString()) { const fmt = "expect.stringContaining(string)\n\nExpected a string\n"; - globalThis.throwPretty(fmt, .{}); - return .zero; + return globalThis.throwPretty(fmt, .{}); } const string_value = args[0]; @@ -5161,8 +4976,7 @@ pub const ExpectAny = struct { constructor.ensureStillAlive(); if (!constructor.isConstructor()) { const fmt = "expect.any(constructor)\n\nExpected a constructor\n"; - globalThis.throwPretty(fmt, .{}); - return error.JSError; + return globalThis.throwPretty(fmt, .{}); } const asymmetric_matcher_constructor_type = try Expect.Flags.AsymmetricMatcherConstructorType.fromJS(globalThis, constructor); @@ -5210,8 +5024,7 @@ pub const ExpectArrayContaining = struct { if (args.len == 0 or !args[0].jsType().isArray()) { const fmt = "expect.arrayContaining(array)\n\nExpected a array\n"; - globalThis.throwPretty(fmt, .{}); - return .zero; + return globalThis.throwPretty(fmt, .{}); } const array_value = args[0]; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 3031cf21928669..26af8a98481275 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1711,8 +1711,7 @@ inline fn createScope( const args = arguments.slice(); if (args.len == 0) { - globalThis.throwPretty("{s} expects a description or function", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects a description or function", .{signature}); } var description = args[0]; @@ -1726,8 +1725,7 @@ inline fn createScope( if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { if (tag != .todo and tag != .skip) { - globalThis.throwPretty("{s} expects a function", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects a function", .{signature}); } } @@ -1737,28 +1735,24 @@ inline fn createScope( } else if (options.isObject()) { if (try options.get(globalThis, "timeout")) |timeout| { if (!timeout.isNumber()) { - globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); } timeout_ms = @as(u32, @intCast(@max(timeout.coerce(i32, globalThis), 0))); } if (try options.get(globalThis, "retry")) |retries| { if (!retries.isNumber()) { - globalThis.throwPretty("{s} expects retry to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects retry to be a number", .{signature}); } // TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0)); } if (try options.get(globalThis, "repeats")) |repeats| { if (!repeats.isNumber()) { - globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); } // TODO: repeat_count = @intCast(u32, @max(repeats.coerce(i32, globalThis), 0)); } } else if (!options.isEmptyOrUndefinedOrNull()) { - globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); } const parent = DescribeScope.active.?; @@ -1858,13 +1852,12 @@ inline fn createIfScope( comptime signature: string, comptime Scope: type, comptime tag: Tag, -) JSValue { +) bun.JSError!JSValue { const arguments = callframe.arguments_old(1); const args = arguments.slice(); if (args.len == 0) { - globalThis.throwPretty("{s} expects a condition", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects a condition", .{signature}); } const name = ZigString.static(property); @@ -1981,8 +1974,7 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa const args = arguments.slice(); if (args.len < 2) { - globalThis.throwPretty("{s} a description and callback function", .{signature}); - return .zero; + return globalThis.throwPretty("{s} a description and callback function", .{signature}); } var description = args[0]; @@ -1990,8 +1982,7 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa var options = if (args.len > 2) args[2] else .zero; if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(globalThis.vm())) { - globalThis.throwPretty("{s} expects a function", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects a function", .{signature}); } var timeout_ms: u32 = std.math.maxInt(u32); @@ -2000,28 +1991,24 @@ fn eachBind(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSVa } else if (options.isObject()) { if (try options.get(globalThis, "timeout")) |timeout| { if (!timeout.isNumber()) { - globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects timeout to be a number", .{signature}); } timeout_ms = @as(u32, @intCast(@max(timeout.coerce(i32, globalThis), 0))); } if (try options.get(globalThis, "retry")) |retries| { if (!retries.isNumber()) { - globalThis.throwPretty("{s} expects retry to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects retry to be a number", .{signature}); } // TODO: retry_count = @intCast(u32, @max(retries.coerce(i32, globalThis), 0)); } if (try options.get(globalThis, "repeats")) |repeats| { if (!repeats.isNumber()) { - globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects repeats to be a number", .{signature}); } // TODO: repeat_count = @intCast(u32, @max(repeats.coerce(i32, globalThis), 0)); } } else if (!options.isEmptyOrUndefinedOrNull()) { - globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects options to be a number or object", .{signature}); } const parent = DescribeScope.active.?; @@ -2147,19 +2134,17 @@ inline fn createEach( comptime property: [:0]const u8, comptime signature: string, comptime is_test: bool, -) JSValue { +) bun.JSError!JSValue { const arguments = callframe.arguments_old(1); const args = arguments.slice(); if (args.len == 0) { - globalThis.throwPretty("{s} expects an array", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects an array", .{signature}); } var array = args[0]; if (array == .zero or !array.jsType().isArray()) { - globalThis.throwPretty("{s} expects an array", .{signature}); - return .zero; + return globalThis.throwPretty("{s} expects an array", .{signature}); } const allocator = getAllocator(globalThis); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index a54941f0288c4b..1d40b949c18d2a 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -809,8 +809,7 @@ pub const ParsedShellScript = struct { if (err == shell.ParseError.Lex) { assert(lex_result != null); const str = lex_result.?.combineErrors(shargs.arena_allocator()); - globalThis.throwPretty("{s}", .{str}); - return .zero; + return globalThis.throwPretty("{s}", .{str}); } if (parser) |*p| { @@ -818,8 +817,7 @@ pub const ParsedShellScript = struct { assert(p.errors.items.len > 0); } const errstr = p.combineErrors(); - globalThis.throwPretty("{s}", .{errstr}); - return .zero; + return globalThis.throwPretty("{s}", .{errstr}); } return globalThis.throwError(err, "failed to lex/parse shell"); diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 748fdbe32c146f..577936c05daedb 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -4398,8 +4398,7 @@ pub const TestingAPIs = struct { if (lex_result.errors.len > 0) { const str = lex_result.combineErrors(arena.allocator()); - globalThis.throwPretty("{s}", .{str}); - return .zero; + return globalThis.throwPretty("{s}", .{str}); } var test_tokens = std.ArrayList(Test.TestToken).initCapacity(arena.allocator(), lex_result.tokens.len) catch { @@ -4475,14 +4474,12 @@ pub const TestingAPIs = struct { if (err == ParseError.Lex) { if (bun.Environment.allow_assert) assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(arena.allocator()); - globalThis.throwPretty("{s}", .{str}); - return .zero; + return globalThis.throwPretty("{s}", .{str}); } if (out_parser) |*p| { const errstr = p.combineErrors(); - globalThis.throwPretty("{s}", .{errstr}); - return .zero; + return globalThis.throwPretty("{s}", .{errstr}); } return globalThis.throwError(err, "failed to lex/parse shell"); From dc01a5d6a86d42f3424c3875252adbcbfcb98236 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 25 Nov 2024 18:55:47 -0800 Subject: [PATCH 09/11] feat(DevServer): batch bundles & run them asynchronously (#15181) Co-authored-by: Ashcon Partovi --- src/analytics/analytics_thread.zig | 4 +- src/bake/DevServer.zig | 2112 +++++++++++++++-------- src/bake/FrameworkRouter.zig | 253 ++- src/bake/bake.d.ts | 26 +- src/bake/bake.private.d.ts | 8 +- src/bake/bake.zig | 131 +- src/bake/bun-framework-react/client.tsx | 402 ++++- src/bake/bun-framework-react/index.ts | 4 +- src/bake/bun-framework-react/server.tsx | 89 +- src/bake/bun-framework-react/ssr.tsx | 53 +- src/bake/client/css-reloader.ts | 186 ++ src/bake/client/jsx-runtime.ts | 0 src/bake/client/overlay.ts | 18 +- src/bake/client/reader.ts | 6 + src/bake/client/websocket.ts | 44 +- src/bake/hmr-module.ts | 6 +- src/bake/hmr-runtime-client.ts | 167 +- src/bake/hmr-runtime-server.ts | 4 +- src/bake/incremental_visualizer.html | 2 +- src/bake/production.zig | 224 ++- src/bun.js/Strong.zig | 6 +- src/bun.js/api/BunObject.zig | 16 +- src/bun.js/api/JSBundler.zig | 49 +- src/bun.js/api/server.zig | 71 +- src/bun.js/bindings/BunObject.cpp | 8 - src/bun.js/bindings/BunProcess.cpp | 7 +- src/bun.js/bindings/JSBundlerPlugin.cpp | 2 +- src/bun.js/bindings/KeyObject.cpp | 2 - src/bun.js/bindings/ZigGlobalObject.cpp | 1 - src/bun.js/bindings/bindings.cpp | 10 +- src/bun.js/bindings/bindings.zig | 18 +- src/bun.js/event_loop.zig | 13 +- src/bun.js/javascript.zig | 9 +- src/bun.zig | 53 +- src/bundler/bundle_v2.zig | 380 ++-- src/bundler/entry_points.zig | 12 +- src/cli.zig | 17 +- src/cli/outdated_command.zig | 12 +- src/codegen/bake-codegen.ts | 8 +- src/crash_handler.zig | 1 + src/deps/uws.zig | 14 +- src/feature_flags.zig | 16 +- src/fmt.zig | 94 +- src/js/builtins/Bake.ts | 7 +- src/js/builtins/BundlerPlugin.ts | 16 +- src/js/internal-for-testing.ts | 4 +- src/js_parser.zig | 24 +- src/js_printer.zig | 21 +- src/logger.zig | 33 +- src/options.zig | 1 + src/output.zig | 32 +- src/resolver/resolve_path.zig | 2 +- src/watcher.zig | 3 + test/bake/dev-server-harness.ts | 357 ++++ test/bake/dev/bundle.test.ts | 51 + test/bake/dev/css.test.ts | 72 + test/bake/dev/esm.test.ts | 43 + test/bake/framework-router.test.ts | 167 +- test/bake/minimal.server.ts | 17 + test/bundler/bundler_defer.test.ts | 631 +++++++ test/bundler/bundler_plugin.test.ts | 5 +- test/bundler/expectBundled.ts | 14 +- test/harness.ts | 1 + test/js/bun/plugin/plugins.test.ts | 629 ------- 64 files changed, 4476 insertions(+), 2212 deletions(-) create mode 100644 src/bake/client/css-reloader.ts delete mode 100644 src/bake/client/jsx-runtime.ts create mode 100644 test/bake/dev-server-harness.ts create mode 100644 test/bake/dev/bundle.test.ts create mode 100644 test/bake/dev/css.test.ts create mode 100644 test/bake/dev/esm.test.ts create mode 100644 test/bake/minimal.server.ts create mode 100644 test/bundler/bundler_defer.test.ts diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index 18094bcba18b35..3c2c9bbe4d65e5 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -99,8 +99,8 @@ pub const Features = struct { pub var https_server: usize = 0; /// Set right before JSC::initialize is called pub var jsc: usize = 0; - /// Set when kit.DevServer is initialized - pub var kit_dev: usize = 0; + /// Set when bake.DevServer is initialized + pub var dev_server: usize = 0; pub var lifecycle_scripts: usize = 0; pub var loaders: usize = 0; pub var lockfile_migration_from_package_lock: usize = 0; diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 086ab1e41a2b78..64821c5dbba680 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -6,21 +6,21 @@ //! adjusting imports) must always rebundle only that one file. //! //! All work is held in-memory, using manually managed data-oriented design. -//! -//! TODO: Currently does not have a `deinit()`, as it was assumed to be alive for -//! the remainder of this process' lifespan. Later, it will be required to fully -//! clean up server state. pub const DevServer = @This(); pub const debug = bun.Output.Scoped(.Bake, false); pub const igLog = bun.Output.scoped(.IncrementalGraph, false); pub const Options = struct { + /// Arena must live until DevServer.deinit() + arena: Allocator, root: []const u8, + vm: *VirtualMachine, framework: bake.Framework, + + // Debugging features dump_sources: ?[]const u8 = if (Environment.isDebug) ".bake-debug" else null, - dump_state_on_crash: bool = bun.FeatureFlags.bake_debugging_features, + dump_state_on_crash: bool = false, verbose_watcher: bool = false, - vm: *VirtualMachine, }; // The fields `client_graph`, `server_graph`, and `directory_watchers` all @@ -54,12 +54,16 @@ client_graph: IncrementalGraph(.client), server_graph: IncrementalGraph(.server), /// State populated during bundling and hot updates. Often cleared incremental_result: IncrementalResult, +/// Quickly retrieve a route's index from its entry point file. These are +/// populated as the routes are discovered. The route may not be bundled OR +/// navigatable, such as the case where a layout's index is looked up. +route_lookup: AutoArrayHashMapUnmanaged(IncrementalGraph(.server).FileIndex, RouteIndexAndRecurseFlag), /// CSS files are accessible via `/_bun/css/.css` /// Value is bundled code owned by `dev.allocator` css_files: AutoArrayHashMapUnmanaged(u64, []const u8), /// JS files are accessible via `/_bun/client/route..js` /// These are randomly generated to avoid possible browser caching of old assets. -route_js_payloads: AutoArrayHashMapUnmanaged(u64, Route.Index), +route_js_payloads: AutoArrayHashMapUnmanaged(u64, Route.Index.Optional), // /// Assets are accessible via `/_bun/asset/` // assets: bun.StringArrayHashMapUnmanaged(u64, Asset), /// All bundling failures are stored until a file is saved and rebuilt. @@ -80,14 +84,7 @@ server_register_update_callback: JSC.Strong, // Watching bun_watcher: *JSC.Watcher, directory_watchers: DirectoryWatchStore, -/// Only two hot-reload tasks exist ever. Memory is reused by swapping between the two. -/// These items are aligned to cache lines to reduce contention. -watch_events: [2]HotReloadTask.Aligned, -/// 0 - no watch -/// 1 - has fired additional watch -/// 2+ - new events available, watcher is waiting on bundler to finish -watch_state: std.atomic.Value(u32), -watch_current: u1 = 0, +watcher_atomics: WatcherAtomics, /// Number of bundles that have been executed. This is currently not read, but /// will be used later to determine when to invoke graph garbage collection. @@ -95,23 +92,50 @@ generation: usize = 0, /// Displayed in the HMR success indicator bundles_since_last_error: usize = 0, -/// Quickly retrieve a route's index from the entry point file. These are -/// populated as the routes are discovered. The route may not be bundled or -/// navigatable, in the case a layout's index is looked up. -route_lookup: AutoArrayHashMapUnmanaged(IncrementalGraph(.server).FileIndex, RouteIndexAndRecurseFlag), - framework: bake.Framework, // Each logical graph gets its own bundler configuration server_bundler: Bundler, client_bundler: Bundler, ssr_bundler: Bundler, - -// TODO: This being shared state is likely causing a crash -/// Stored and reused for bundling tasks +/// The log used by all `server_bundler`, `client_bundler` and `ssr_bundler`. +/// Note that it is rarely correct to write messages into it. Instead, associate +/// messages with the IncrementalGraph file or Route using `SerializedFailure` log: Log, +/// There is only ever one bundle executing at the same time, since all bundles +/// inevitably share state. This bundle is asynchronous, storing its state here +/// while in-flight. All allocations held by `.bv2.graph.heap`'s arena +current_bundle: ?struct { + bv2: *BundleV2, + /// Information BundleV2 needs to finalize the bundle + start_data: bun.bundle_v2.BakeBundleStart, + /// Started when the bundle was queued + timer: std.time.Timer, + /// If any files in this bundle were due to hot-reloading, some extra work + /// must be done to inform clients to reload routes. When this is false, + /// all entry points do not have bundles yet. + had_reload_event: bool, +}, +/// This is not stored in `current_bundle` so that its memory can be reused when +/// there is no active bundle. After the bundle finishes, these requests will +/// be continued, either calling their handler on success or sending the error +/// page on failure. +current_bundle_requests: ArrayListUnmanaged(DeferredRequest), +/// When `current_bundle` is non-null and new requests to bundle come in, +/// those are temporaried here. When the current bundle is finished, it +/// will immediately enqueue this. +next_bundle: struct { + /// A list of `RouteBundle`s which have active requests to bundle it. + route_queue: AutoArrayHashMapUnmanaged(RouteBundle.Index, void), + /// If a reload event exists and should be drained. The information + /// for this watch event is in one of the `watch_events` + reload_event: ?*HotReloadEvent, + /// The list of requests that are blocked on this bundle. + requests: ArrayListUnmanaged(DeferredRequest), +}, // Debugging -dump_dir: ?std.fs.Dir, + +dump_dir: if (bun.FeatureFlags.bake_debugging_features) ?std.fs.Dir else void, /// Reference count to number of active sockets with the visualizer enabled. emit_visualizer_events: u32, has_pre_crash_handler: bool, @@ -129,8 +153,7 @@ pub const RouteBundle = struct { server_state: State, /// Used to communicate over WebSocket the pattern. The HMR client contains code - /// to match this against the URL bar to determine if a reloading route applies - /// or not. + /// to match this against the URL bar to determine if a reloaded route applies. full_pattern: []const u8, /// Generated lazily when the client JS is requested (HTTP GET /_bun/client/*.js), /// which is only needed when a hard-reload is performed. @@ -154,6 +177,11 @@ pub const RouteBundle = struct { /// Invalidated when the list of CSS files changes. cached_css_file_array: JSC.Strong, + /// Reference count of how many HmrSockets say they are on this route. This + /// allows hot-reloading events to reduce the amount of times it traces the + /// graph. + active_viewers: usize, + /// A union is not used so that `bundler_failure_logs` can re-use memory, as /// this state frequently changes between `loaded` and the failure variants. const State = enum { @@ -170,33 +198,19 @@ pub const RouteBundle = struct { /// imports has to be traced to discover if possible failures still /// exist. possible_bundling_failures, - /// Loading the module at runtime had a failure. + /// Loading the module at runtime had a failure. The error can be + /// cleared by editing any file in the same hot-reloading boundary. evaluation_failure, /// Calling the request function may error, but that error will not be - /// at fault of bundling. + /// at fault of bundling, nor would re-bundling change anything. loaded, }; }; -pub const DeferredRequest = struct { - next: ?*DeferredRequest, - bundle: RouteBundle.Index, - data: Data, - - const Data = union(enum) { - server_handler: bun.JSC.API.SavedRequest, - /// onJsRequestWithBundle - js_payload: *Response, - - const Tag = @typeInfo(Data).Union.tag_type.?; - }; -}; - /// DevServer is stored on the heap, storing its allocator. -// TODO: change the error set to JSOrMemoryError!*DevServer -pub fn init(options: Options) !*DevServer { +pub fn init(options: Options) bun.JSOOM!*DevServer { const allocator = bun.default_allocator; - bun.analytics.Features.kit_dev +|= 1; + bun.analytics.Features.dev_server +|= 1; var dump_dir = if (bun.FeatureFlags.bake_debugging_features) if (options.dump_sources) |dir| @@ -221,12 +235,9 @@ pub fn init(options: Options) !*DevServer { .server_fetch_function_callback = .{}, .server_register_update_callback = .{}, .generation = 0, - .graph_safety_lock = .{}, - .log = Log.init(allocator), + .graph_safety_lock = bun.DebugThreadLock.unlocked, .dump_dir = dump_dir, .framework = options.framework, - .watch_state = .{ .raw = 0 }, - .watch_current = 0, .emit_visualizer_events = 0, .has_pre_crash_handler = options.dump_state_on_crash, .css_files = .{}, @@ -237,19 +248,26 @@ pub fn init(options: Options) !*DevServer { .server_graph = IncrementalGraph(.server).empty, .incremental_result = IncrementalResult.empty, .route_lookup = .{}, + .route_bundles = .{}, + .current_bundle = null, + .current_bundle_requests = .{}, + .next_bundle = .{ + .route_queue = .{}, + .reload_event = null, + .requests = .{}, + }, + + .log = bun.logger.Log.init(allocator), .server_bundler = undefined, .client_bundler = undefined, .ssr_bundler = undefined, - .bun_watcher = undefined, - .watch_events = undefined, - .configuration_hash_key = undefined, - .router = undefined, - .route_bundles = .{}, + .watcher_atomics = undefined, }); + const global = dev.vm.global; errdefer allocator.destroy(dev); assert(dev.server_graph.owner() == dev); @@ -259,45 +277,51 @@ pub fn init(options: Options) !*DevServer { dev.graph_safety_lock.lock(); defer dev.graph_safety_lock.unlock(); - const fs = try bun.fs.FileSystem.init(options.root); + const generic_action = "while initializing development server"; + const fs = bun.fs.FileSystem.init(options.root) catch |err| + return global.throwError(err, generic_action); + + dev.bun_watcher = Watcher.init(DevServer, dev, fs, bun.default_allocator) catch |err| + return global.throwError(err, "while initializing file watcher for development server"); - dev.bun_watcher = try Watcher.init(DevServer, dev, fs, bun.default_allocator); errdefer dev.bun_watcher.deinit(false); - try dev.bun_watcher.start(); + dev.bun_watcher.start() catch |err| + return global.throwError(err, "while initializing file watcher thread for development server"); dev.server_bundler.resolver.watcher = dev.bun_watcher.getResolveWatcher(); dev.client_bundler.resolver.watcher = dev.bun_watcher.getResolveWatcher(); dev.ssr_bundler.resolver.watcher = dev.bun_watcher.getResolveWatcher(); - dev.watch_events = .{ - .{ .aligned = HotReloadTask.initEmpty(dev) }, - .{ .aligned = HotReloadTask.initEmpty(dev) }, - }; - try dev.framework.initBundler(allocator, &dev.log, .development, .server, &dev.server_bundler); + dev.watcher_atomics = WatcherAtomics.init(dev); + + dev.framework.initBundler(allocator, &dev.log, .development, .server, &dev.server_bundler) catch |err| + return global.throwError(err, generic_action); dev.client_bundler.options.dev_server = dev; - try dev.framework.initBundler(allocator, &dev.log, .development, .client, &dev.client_bundler); + dev.framework.initBundler(allocator, &dev.log, .development, .client, &dev.client_bundler) catch |err| + return global.throwError(err, generic_action); dev.server_bundler.options.dev_server = dev; if (separate_ssr_graph) { - try dev.framework.initBundler(allocator, &dev.log, .development, .ssr, &dev.ssr_bundler); + dev.framework.initBundler(allocator, &dev.log, .development, .ssr, &dev.ssr_bundler) catch |err| + return global.throwError(err, generic_action); dev.ssr_bundler.options.dev_server = dev; } - dev.framework = dev.framework.resolve(&dev.server_bundler.resolver, &dev.client_bundler.resolver) catch { - Output.errGeneric("Failed to resolve all imports required by the framework", .{}); - return error.FrameworkInitialization; + dev.framework = dev.framework.resolve(&dev.server_bundler.resolver, &dev.client_bundler.resolver, options.arena) catch { + try bake.Framework.addReactInstallCommandNote(&dev.log); + return global.throwValue2(dev.log.toJSAggregateError(global, "Framework is missing required files!")); }; errdefer dev.route_lookup.clearAndFree(allocator); // errdefer dev.client_graph.deinit(allocator); // errdefer dev.server_graph.deinit(allocator); - dev.vm.global = @ptrCast(dev.vm.global); - dev.configuration_hash_key = hash_key: { var hash = std.hash.Wyhash.init(128); if (bun.Environment.isDebug) { - const stat = try bun.sys.stat(try bun.selfExePath()).unwrap(); + const stat = bun.sys.stat(bun.selfExePath() catch |e| + Output.panic("unhandled {}", .{e})).unwrap() catch |e| + Output.panic("unhandled {}", .{e}); bun.writeAnyToHasher(&hash, stat.mtime()); hash.update(bake.getHmrRuntime(.client)); hash.update(bake.getHmrRuntime(.server)); @@ -305,9 +329,28 @@ pub fn init(options: Options) !*DevServer { hash.update(bun.Environment.git_sha_short); } - // TODO: hash router types - // hash.update(dev.framework.entry_client); - // hash.update(dev.framework.entry_server); + for (dev.framework.file_system_router_types) |fsr| { + bun.writeAnyToHasher(&hash, fsr.allow_layouts); + bun.writeAnyToHasher(&hash, fsr.ignore_underscores); + hash.update(fsr.entry_server); + hash.update(&.{0}); + hash.update(fsr.entry_client orelse ""); + hash.update(&.{0}); + hash.update(fsr.prefix); + hash.update(&.{0}); + hash.update(fsr.root); + hash.update(&.{0}); + for (fsr.extensions) |ext| { + hash.update(ext); + hash.update(&.{0}); + } + hash.update(&.{0}); + for (fsr.ignore_dirs) |dir| { + hash.update(dir); + hash.update(&.{0}); + } + hash.update(&.{0}); + } if (dev.framework.server_components) |sc| { bun.writeAnyToHasher(&hash, true); @@ -331,7 +374,16 @@ pub fn init(options: Options) !*DevServer { bun.writeAnyToHasher(&hash, false); } - // TODO: dev.framework.built_in_modules + for (dev.framework.built_in_modules.keys(), dev.framework.built_in_modules.values()) |k, v| { + hash.update(k); + hash.update(&.{0}); + bun.writeAnyToHasher(&hash, std.meta.activeTag(v)); + hash.update(switch (v) { + inline else => |data| data, + }); + hash.update(&.{0}); + } + hash.update(&.{0}); break :hash_key std.fmt.bytesToHex(std.mem.asBytes(&hash.final()), .lower); }; @@ -342,9 +394,9 @@ pub fn init(options: Options) !*DevServer { assert(try dev.client_graph.insertStale(rfr.import_source, false) == IncrementalGraph(.client).react_refresh_index); } - try dev.initServerRuntime(); + dev.initServerRuntime(); - // Initialize the router + // Initialize FrameworkRouter dev.router = router: { var types = try std.ArrayListUnmanaged(FrameworkRouter.Type).initCapacity(allocator, options.framework.file_system_router_types.len); errdefer types.deinit(allocator); @@ -363,6 +415,7 @@ pub fn init(options: Options) !*DevServer { .ignore_dirs = fsr.ignore_dirs, .extensions = fsr.extensions, .style = fsr.style, + .allow_layouts = fsr.allow_layouts, .server_file = toOpaqueFileId(.server, server_file), .client_file = if (fsr.entry_client) |client| toOpaqueFileId(.client, try dev.client_graph.insertStale(client, false)).toOptional() @@ -377,11 +430,12 @@ pub fn init(options: Options) !*DevServer { }); } - break :router try FrameworkRouter.initEmpty(types.items, allocator); + break :router try FrameworkRouter.initEmpty(dev.root, types.items, allocator); }; - // TODO: move pre-bundling to be one tick after server startup. - // this way the line saying the server is ready shows quicker + // TODO: move scanning to be one tick after server startup. this way the + // line saying the server is ready shows quicker, and route errors show up + // after that line. try dev.scanInitialRoutes(); if (bun.FeatureFlags.bake_debugging_features and options.dump_state_on_crash) @@ -390,7 +444,7 @@ pub fn init(options: Options) !*DevServer { return dev; } -fn initServerRuntime(dev: *DevServer) !void { +fn initServerRuntime(dev: *DevServer) void { const runtime = bun.String.static(bun.bake.getHmrRuntime(.server)); const interface = c.BakeLoadInitialServerCode( @@ -403,10 +457,12 @@ fn initServerRuntime(dev: *DevServer) !void { }; if (!interface.isObject()) @panic("Internal assertion failure: expected interface from HMR runtime to be an object"); - const fetch_function: JSValue = try interface.get(dev.vm.global, "handleRequest") orelse @panic("Internal assertion failure: expected interface from HMR runtime to contain handleRequest"); + const fetch_function = interface.get(dev.vm.global, "handleRequest") catch null orelse + @panic("Internal assertion failure: expected interface from HMR runtime to contain handleRequest"); bun.assert(fetch_function.isCallable(dev.vm.jsc)); dev.server_fetch_function_callback = JSC.Strong.create(fetch_function, dev.vm.global); - const register_update = try interface.get(dev.vm.global, "registerUpdate") orelse @panic("Internal assertion failure: expected interface from HMR runtime to contain registerUpdate"); + const register_update = interface.get(dev.vm.global, "registerUpdate") catch null orelse + @panic("Internal assertion failure: expected interface from HMR runtime to contain registerUpdate"); dev.server_register_update_callback = JSC.Strong.create(register_update, dev.vm.global); fetch_function.ensureStillAlive(); @@ -447,21 +503,25 @@ pub fn attachRoutes(dev: *DevServer, server: anytype) !void { uws.WebSocketBehavior.Wrap(DevServer, HmrSocket, false).apply(.{}), ); - app.get(internal_prefix ++ "/incremental_visualizer", *DevServer, dev, onIncrementalVisualizer); + if (bun.FeatureFlags.bake_debugging_features) + app.get(internal_prefix ++ "/incremental_visualizer", *DevServer, dev, onIncrementalVisualizer); app.any("/*", *DevServer, dev, onRequest); } pub fn deinit(dev: *DevServer) void { + // TODO: Currently deinit is not implemented, as it was assumed to be alive for + // the remainder of this process' lifespan. This isn't always true. const allocator = dev.allocator; if (dev.has_pre_crash_handler) bun.crash_handler.removePreCrashHandler(dev); allocator.destroy(dev); - bun.todoPanic(@src(), "bake.DevServer.deinit()", .{}); + // if (bun.Environment.isDebug) + // bun.todoPanic(@src(), "bake.DevServer.deinit()", .{}); } fn onJsRequest(dev: *DevServer, req: *Request, resp: *Response) void { - const route_bundle = route: { + const maybe_route = route: { const route_id = req.parameter(0); if (!bun.strings.hasSuffixComptime(route_id, ".js")) return req.setYield(true); @@ -473,7 +533,11 @@ fn onJsRequest(dev: *DevServer, req: *Request, resp: *Response) void { return req.setYield(true); }; - dev.ensureRouteIsBundled(route_bundle, .js_payload, req, resp) catch bun.outOfMemory(); + if (maybe_route.unwrap()) |route| { + dev.ensureRouteIsBundled(route, .js_payload, req, resp) catch bun.outOfMemory(); + } else { + @panic("TODO: generate client bundle with no source files"); + } } fn onAssetRequest(dev: *DevServer, req: *Request, resp: *Response) void { @@ -533,87 +597,139 @@ fn ensureRouteIsBundled( req: *Request, resp: *Response, ) bun.OOM!void { - const bundle_index = if (dev.router.routePtr(route_index).bundle.unwrap()) |bundle_index| - bundle_index - else - try dev.insertRouteBundle(route_index); - - switch (dev.routeBundlePtr(bundle_index).server_state) { - .unqueued => { - const server_file_names = dev.server_graph.bundled_files.keys(); - const client_file_names = dev.client_graph.bundled_files.keys(); - - var sfa = std.heap.stackFallback(4096, dev.allocator); - const temp_alloc = sfa.get(); - - var entry_points = std.ArrayList(BakeEntryPoint).init(temp_alloc); - defer entry_points.deinit(); - - // Build a list of all files that have not yet been bundled. - var route = dev.router.routePtr(route_index); - const router_type = dev.router.typePtr(route.type); - try dev.appendOpaqueEntryPoint(server_file_names, &entry_points, .server, router_type.server_file); - try dev.appendOpaqueEntryPoint(client_file_names, &entry_points, .client, router_type.client_file); - try dev.appendOpaqueEntryPoint(server_file_names, &entry_points, .server, route.file_page); - try dev.appendOpaqueEntryPoint(server_file_names, &entry_points, .server, route.file_layout); - while (route.parent.unwrap()) |parent_index| { - route = dev.router.routePtr(parent_index); - try dev.appendOpaqueEntryPoint(server_file_names, &entry_points, .server, route.file_layout); - } + const route_bundle_index = try dev.getOrPutRouteBundle(route_index); - if (entry_points.items.len == 0) { - @panic("TODO: trace graph for possible errors, so DevServer knows what state this should go to"); - } + // TODO: Zig 0.14 gets labelled continue: + // - Remove the `while` + // - Move the code after this switch into `.loaded =>` + // - Replace `break` with `continue :sw .loaded` + // - Replace `continue` with `continue :sw ` + while (true) { + switch (dev.routeBundlePtr(route_bundle_index).server_state) { + .unqueued => { + try dev.next_bundle.requests.ensureUnusedCapacity(dev.allocator, 1); + if (dev.current_bundle != null) { + try dev.next_bundle.route_queue.ensureUnusedCapacity(dev.allocator, 1); + } - const route_bundle = dev.routeBundlePtr(bundle_index); - if (dev.bundle(entry_points.items)) |_| { - route_bundle.server_state = .loaded; - } else |err| switch (err) { - error.OutOfMemory => bun.outOfMemory(), - error.BuildFailed => assert(route_bundle.server_state == .possible_bundling_failures), - error.ServerLoadFailed => route_bundle.server_state = .evaluation_failure, - } - }, - .bundling => { - const prepared = dev.server.?.DebugHTTPServer.prepareJsRequestContext(req, resp) orelse + const deferred: DeferredRequest = .{ + .route_bundle_index = route_bundle_index, + .data = switch (kind) { + .js_payload => .{ .js_payload = resp }, + .server_handler => .{ + .server_handler = (dev.server.?.DebugHTTPServer.prepareJsRequestContext(req, resp) orelse return) + .save(dev.vm.global, req, resp), + }, + }, + }; + errdefer @compileError("cannot error since the request is already stored"); + + dev.next_bundle.requests.appendAssumeCapacity(deferred); + if (dev.current_bundle != null) { + dev.next_bundle.route_queue.putAssumeCapacity(route_bundle_index, {}); + } else { + var sfa = std.heap.stackFallback(4096, dev.allocator); + const temp_alloc = sfa.get(); + + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); + + dev.appendRouteEntryPointsIfNotStale(&entry_points, temp_alloc, route_index) catch bun.outOfMemory(); + + if (entry_points.set.count() == 0) { + if (dev.bundling_failures.count() > 0) { + dev.routeBundlePtr(route_bundle_index).server_state = .possible_bundling_failures; + } else { + dev.routeBundlePtr(route_bundle_index).server_state = .loaded; + } + continue; + } + + dev.startAsyncBundle( + entry_points, + false, + std.time.Timer.start() catch @panic("timers unsupported"), + ) catch |err| { + if (dev.log.hasAny()) { + dev.log.print(Output.errorWriterBuffered()) catch {}; + Output.flush(); + } + Output.panic("Fatal error while initializing bundle job: {}", .{err}); + }; + + dev.routeBundlePtr(route_bundle_index).server_state = .bundling; + } return; - _ = prepared; - @panic("TODO: Async Bundler"); - }, - else => {}, - } - switch (dev.routeBundlePtr(bundle_index).server_state) { - .unqueued => unreachable, - .bundling => @panic("TODO: Async Bundler"), - .possible_bundling_failures => { - // TODO: perform a graph trace to find just the errors that are needed - if (dev.bundling_failures.count() > 0) { + }, + .bundling => { + bun.assert(dev.current_bundle != null); + try dev.current_bundle_requests.ensureUnusedCapacity(dev.allocator, 1); + + const deferred: DeferredRequest = .{ + .route_bundle_index = route_bundle_index, + .data = switch (kind) { + .js_payload => .{ .js_payload = resp }, + .server_handler => .{ + .server_handler = (dev.server.?.DebugHTTPServer.prepareJsRequestContext(req, resp) orelse return) + .save(dev.vm.global, req, resp), + }, + }, + }; + + dev.current_bundle_requests.appendAssumeCapacity(deferred); + return; + }, + .possible_bundling_failures => { + // TODO: perform a graph trace to find just the errors that are needed + if (dev.bundling_failures.count() > 0) { + resp.corked(sendSerializedFailures, .{ + dev, + resp, + dev.bundling_failures.keys(), + .bundler, + }); + return; + } else { + dev.routeBundlePtr(route_bundle_index).server_state = .loaded; + break; + } + }, + .evaluation_failure => { resp.corked(sendSerializedFailures, .{ dev, resp, - dev.bundling_failures.keys(), - .bundler, + (&(dev.routeBundlePtr(route_bundle_index).evaluate_failure orelse @panic("missing error")))[0..1], + .evaluation, }); return; - } else { - dev.routeBundlePtr(bundle_index).server_state = .loaded; - } - }, - .evaluation_failure => { - resp.corked(sendSerializedFailures, .{ - dev, - resp, - (&(dev.routeBundlePtr(bundle_index).evaluate_failure orelse @panic("missing error")))[0..1], - .evaluation, - }); - return; - }, - .loaded => {}, + }, + .loaded => break, + } + + // this error is here to make sure there are no accidental loop exits + @compileError("all branches above should `return`, `break` or `continue`"); } switch (kind) { - .server_handler => dev.onRequestWithBundle(bundle_index, .{ .stack = req }, resp), - .js_payload => dev.onJsRequestWithBundle(bundle_index, resp), + .server_handler => dev.onRequestWithBundle(route_bundle_index, .{ .stack = req }, resp), + .js_payload => dev.onJsRequestWithBundle(route_bundle_index, resp), + } +} + +fn appendRouteEntryPointsIfNotStale(dev: *DevServer, entry_points: *EntryPointList, alloc: Allocator, route_index: Route.Index) bun.OOM!void { + const server_file_names = dev.server_graph.bundled_files.keys(); + const client_file_names = dev.client_graph.bundled_files.keys(); + + // Build a list of all files that have not yet been bundled. + var route = dev.router.routePtr(route_index); + const router_type = dev.router.typePtr(route.type); + try dev.appendOpaqueEntryPoint(server_file_names, entry_points, alloc, .server, router_type.server_file); + try dev.appendOpaqueEntryPoint(client_file_names, entry_points, alloc, .client, router_type.client_file); + try dev.appendOpaqueEntryPoint(server_file_names, entry_points, alloc, .server, route.file_page); + try dev.appendOpaqueEntryPoint(server_file_names, entry_points, alloc, .server, route.file_layout); + while (route.parent.unwrap()) |parent_index| { + route = dev.router.routePtr(parent_index); + try dev.appendOpaqueEntryPoint(server_file_names, entry_points, alloc, .server, route.file_layout); } } @@ -639,7 +755,7 @@ fn onRequestWithBundle( // routerTypeMain router_type.server_file_string.get() orelse str: { const name = dev.server_graph.bundled_files.keys()[fromOpaqueFileId(.server, router_type.server_file).get()]; - const str = bun.String.createUTF8(name); + const str = bun.String.createUTF8(dev.relativePath(name)); defer str.deref(); const js = str.toJS(dev.vm.global); router_type.server_file_string = JSC.Strong.create(js, dev.vm.global); @@ -673,8 +789,14 @@ fn onRequestWithBundle( }, // clientId route_bundle.cached_client_bundle_url.get() orelse str: { - const id = std.crypto.random.int(u64); - dev.route_js_payloads.put(dev.allocator, id, route_bundle.route) catch bun.outOfMemory(); + const id, const route_index: Route.Index.Optional = if (router_type.client_file != .none) + .{ std.crypto.random.int(u64), route_bundle.route.toOptional() } + else + // When there is no framework-provided client code, generate + // a JS file so that the hot-reloading code can reload the + // page on server-side changes and show errors in-browser. + .{ 0, .none }; + dev.route_js_payloads.put(dev.allocator, id, route_index) catch bun.outOfMemory(); const str = bun.String.createFormat(client_prefix ++ "/route.{}.js", .{std.fmt.fmtSliceHexLower(std.mem.asBytes(&id))}) catch bun.outOfMemory(); defer str.deref(); const js = str.toJS(dev.vm.global); @@ -683,7 +805,7 @@ fn onRequestWithBundle( }, // styles route_bundle.cached_css_file_array.get() orelse arr: { - const js = dev.generateCssList(route_bundle) catch bun.outOfMemory(); + const js = dev.generateCssJSArray(route_bundle) catch bun.outOfMemory(); route_bundle.cached_css_file_array = JSC.Strong.create(js, dev.vm.global); break :arr js; }, @@ -731,33 +853,34 @@ pub fn onSrcRequest(dev: *DevServer, req: *uws.Request, resp: *App.Response) voi } } -const BundleError = error{ - OutOfMemory, - /// Graph entry points will be annotated with failures to display. - BuildFailed, - - ServerLoadFailed, -}; +const DeferredRequest = struct { + route_bundle_index: RouteBundle.Index, + data: Data, -fn bundle(dev: *DevServer, files: []const BakeEntryPoint) BundleError!void { - defer dev.emitVisualizerMessageIfNeeded() catch bun.outOfMemory(); + const Data = union(enum) { + server_handler: bun.JSC.API.SavedRequest, + js_payload: *Response, - assert(files.len > 0); + const Tag = @typeInfo(Data).Union.tag_type.?; + }; +}; - const bundle_file_list = bun.Output.Scoped(.bundle_file_list, false); +fn startAsyncBundle( + dev: *DevServer, + entry_points: EntryPointList, + had_reload_event: bool, + timer: std.time.Timer, +) bun.OOM!void { + assert(dev.current_bundle == null); + assert(entry_points.set.count() > 0); + dev.log.clearAndFree(); - if (bundle_file_list.isVisible()) { - bundle_file_list.log("Start bundle {d} files", .{files.len}); - for (files) |f| { - bundle_file_list.log("- {s} (.{s})", .{ f.path, @tagName(f.graph) }); - } - } + dev.incremental_result.reset(); var heap = try ThreadlocalArena.init(); - defer heap.deinit(); - + errdefer heap.deinit(); const allocator = heap.allocator(); - var ast_memory_allocator = try allocator.create(bun.JSAst.ASTMemoryAllocator); + const ast_memory_allocator = try allocator.create(bun.JSAst.ASTMemoryAllocator); ast_memory_allocator.* = .{ .allocator = allocator }; ast_memory_allocator.reset(); ast_memory_allocator.push(); @@ -769,11 +892,6 @@ fn bundle(dev: *DevServer, files: []const BakeEntryPoint) BundleError!void { bun.todoPanic(@src(), "support non-server components build", .{}); } - var timer = if (Environment.enable_logs) std.time.Timer.start() catch unreachable; - - dev.graph_safety_lock.lock(); - defer dev.graph_safety_lock.unlock(); - const bv2 = try BundleV2.init( &dev.server_bundler, if (dev.framework.server_components != null) .{ @@ -782,123 +900,34 @@ fn bundle(dev: *DevServer, files: []const BakeEntryPoint) BundleError!void { .ssr_bundler = &dev.ssr_bundler, } else @panic("TODO: support non-server components"), allocator, - JSC.AnyEventLoop.init(allocator), + .{ .js = dev.vm.eventLoop() }, false, // reloading is handled separately JSC.WorkPool.get(), heap, ); bv2.bun_watcher = dev.bun_watcher; - // this.plugins = completion.plugins; - - defer { - if (bv2.graph.pool.pool.threadpool_context == @as(?*anyopaque, @ptrCast(bv2.graph.pool))) { - bv2.graph.pool.pool.threadpool_context = null; - } - ast_memory_allocator.pop(); - bv2.deinit(); - } - - dev.client_graph.reset(); - dev.server_graph.reset(); + bv2.asynchronous = true; - const bundle_result = bv2.runFromBakeDevServer(files) catch |err| { - bun.handleErrorReturnTrace(err, @errorReturnTrace()); - - bv2.bundler.log.print(Output.errorWriter()) catch {}; - - Output.warn("BundleV2.runFromBakeDevServer returned error.{s}", .{@errorName(err)}); - - return; - }; - - bv2.bundler.log.print(Output.errorWriter()) catch {}; - - try dev.finalizeBundle(bv2, bundle_result); - - try dev.client_graph.ensureStaleBitCapacity(false); - try dev.server_graph.ensureStaleBitCapacity(false); - - dev.generation +%= 1; - if (Environment.enable_logs) { - debug.log("Bundle Round {d}: {d} server, {d} client, {d} ms", .{ - dev.generation, - dev.server_graph.current_chunk_parts.items.len, - dev.client_graph.current_chunk_parts.items.len, - @divFloor(timer.read(), std.time.ns_per_ms), - }); - } - - const is_first_server_chunk = !dev.server_fetch_function_callback.has(); - - if (dev.server_graph.current_chunk_len > 0) { - const server_bundle = try dev.server_graph.takeBundle( - if (is_first_server_chunk) .initial_response else .hmr_chunk, - "", - ); - defer dev.allocator.free(server_bundle); - - const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.createLatin1(server_bundle)) catch |err| { - // No user code has been evaluated yet, since everything is to - // be wrapped in a function clousure. This means that the likely - // error is going to be a syntax error, or other mistake in the - // bundler. - dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); - @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); - }; - const errors = dev.server_register_update_callback.get().?.call( - dev.vm.global, - dev.vm.global.toJSValue(), - &.{ - server_modules, - dev.makeArrayForServerComponentsPatch(dev.vm.global, dev.incremental_result.client_components_added.items), - dev.makeArrayForServerComponentsPatch(dev.vm.global, dev.incremental_result.client_components_removed.items), - }, - ) catch |err| { - // One module replacement error should NOT prevent follow-up - // module replacements to fail. It is the HMR runtime's - // responsibility to collect all module load errors, and - // bubble them up. - dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); - @panic("Error thrown in Hot-module-replacement code. This is always a bug in the HMR runtime."); - }; - _ = errors; // TODO: - } - - const css_chunks = bundle_result.cssChunks(); - if ((dev.client_graph.current_chunk_len > 0 or - css_chunks.len > 0) and - dev.numSubscribers(HmrSocket.global_topic) > 0) { - var sfb2 = std.heap.stackFallback(65536, bun.default_allocator); - var payload = std.ArrayList(u8).initCapacity(sfb2.get(), 65536) catch - unreachable; // enough space - defer payload.deinit(); - payload.appendAssumeCapacity(MessageId.hot_update.char()); - const w = payload.writer(); - - const css_values = dev.css_files.values(); - try w.writeInt(u32, @intCast(css_chunks.len), .little); - const sources = bv2.graph.input_files.items(.source); - for (css_chunks) |chunk| { - const abs_path = sources[chunk.entry_point.source_index].path.text; - - try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&bun.hash(abs_path)), .lower)); - - const css_data = css_values[chunk.entry_point.entry_point_id]; - try w.writeInt(u32, @intCast(css_data.len), .little); - try w.writeAll(css_data); - } - - if (dev.client_graph.current_chunk_len > 0) - try dev.client_graph.takeBundleToList(.hmr_chunk, &payload, ""); + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); - dev.publish(HmrSocket.global_topic, payload.items, .binary); + dev.client_graph.reset(); + dev.server_graph.reset(); } - if (dev.incremental_result.failures_added.items.len > 0) { - dev.bundles_since_last_error = 0; - return error.BuildFailed; - } + const start_data = try bv2.startFromBakeDevServer(entry_points); + + dev.current_bundle = .{ + .bv2 = bv2, + .timer = timer, + .start_data = start_data, + .had_reload_event = had_reload_event, + }; + const old_current_requests = dev.current_bundle_requests; + bun.assert(old_current_requests.items.len == 0); + dev.current_bundle_requests = dev.next_bundle.requests; + dev.next_bundle.requests = old_current_requests; } fn indexFailures(dev: *DevServer) !void { @@ -914,11 +943,8 @@ fn indexFailures(dev: *DevServer) !void { total_len += dev.incremental_result.failures_removed.items.len * @sizeOf(u32); - dev.server_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); - defer dev.server_graph.affected_by_trace.deinit(sfa); - - dev.client_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.client_graph.bundled_files.count()); - defer dev.client_graph.affected_by_trace.deinit(sfa); + var gts = try dev.initGraphTraceState(sfa); + defer gts.deinit(sfa); var payload = try std.ArrayList(u8).initCapacity(sfa, total_len); defer payload.deinit(); @@ -937,41 +963,34 @@ fn indexFailures(dev: *DevServer) !void { switch (added.getOwner()) { .none, .route => unreachable, - .server => |index| try dev.server_graph.traceDependencies(index, .no_stop), - .client => |index| try dev.client_graph.traceDependencies(index, .no_stop), + .server => |index| try dev.server_graph.traceDependencies(index, >s, .no_stop), + .client => |index| try dev.client_graph.traceDependencies(index, >s, .no_stop), } } - { - @panic("TODO: revive"); + for (dev.incremental_result.routes_affected.items) |entry| { + if (dev.router.routePtr(entry.route_index).bundle.unwrap()) |index| { + dev.routeBundlePtr(index).server_state = .possible_bundling_failures; + } + if (entry.should_recurse_when_visiting) + dev.markAllRouteChildrenFailed(entry.route_index); } - // for (dev.incremental_result.routes_affected.items) |route_index| { - // const route = &dev.routes[route_index.get()]; - // route.server_state = .possible_bundling_failures; - // } - dev.publish(HmrSocket.global_topic, payload.items, .binary); + dev.publish(.errors, payload.items, .binary); } else if (dev.incremental_result.failures_removed.items.len > 0) { - if (dev.bundling_failures.count() == 0) { - dev.publish(HmrSocket.global_topic, &.{MessageId.errors_cleared.char()}, .binary); - for (dev.incremental_result.failures_removed.items) |removed| { - removed.deinit(); - } - } else { - var payload = try std.ArrayList(u8).initCapacity(sfa, @sizeOf(MessageId) + @sizeOf(u32) + dev.incremental_result.failures_removed.items.len * @sizeOf(u32)); - defer payload.deinit(); - payload.appendAssumeCapacity(MessageId.errors.char()); - const w = payload.writer(); - - try w.writeInt(u32, @intCast(dev.incremental_result.failures_removed.items.len), .little); + var payload = try std.ArrayList(u8).initCapacity(sfa, @sizeOf(MessageId) + @sizeOf(u32) + dev.incremental_result.failures_removed.items.len * @sizeOf(u32)); + defer payload.deinit(); + payload.appendAssumeCapacity(MessageId.errors.char()); + const w = payload.writer(); - for (dev.incremental_result.failures_removed.items) |removed| { - try w.writeInt(u32, @bitCast(removed.getOwner().encode()), .little); - removed.deinit(); - } + try w.writeInt(u32, @intCast(dev.incremental_result.failures_removed.items.len), .little); - dev.publish(HmrSocket.global_topic, payload.items, .binary); + for (dev.incremental_result.failures_removed.items) |removed| { + try w.writeInt(u32, @bitCast(removed.getOwner().encode()), .little); + removed.deinit(); } + + dev.publish(.errors, payload.items, .binary); } dev.incremental_result.failures_removed.clearRetainingCapacity(); @@ -989,17 +1008,12 @@ fn generateClientBundle(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM![]c // Prepare bitsets var sfa_state = std.heap.stackFallback(65536, dev.allocator); const sfa = sfa_state.get(); - // const gts = try dev.initGraphTraceState(sfa); - // defer gts.deinit(sfa); - dev.server_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); - defer dev.server_graph.affected_by_trace.deinit(sfa); - - dev.client_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.client_graph.bundled_files.count()); - defer dev.client_graph.affected_by_trace.deinit(sfa); + var gts = try dev.initGraphTraceState(sfa); + defer gts.deinit(sfa); // Run tracing dev.client_graph.reset(); - try dev.traceAllRouteImports(route_bundle, .{ .find_client_modules = true }); + try dev.traceAllRouteImports(route_bundle, >s, .{ .find_client_modules = true }); const client_file = dev.router.typePtr(dev.router.routePtr(route_bundle.route).type).client_file.unwrap() orelse @panic("No client side entrypoint in client bundle"); @@ -1010,7 +1024,7 @@ fn generateClientBundle(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM![]c ); } -fn generateCssList(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM!JSC.JSValue { +fn generateCssJSArray(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM!JSC.JSValue { if (Environment.allow_assert) assert(!route_bundle.cached_css_file_array.has()); assert(route_bundle.server_state == .loaded); // page is unfit to load @@ -1021,15 +1035,12 @@ fn generateCssList(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM!JSC.JSVa var sfa_state = std.heap.stackFallback(65536, dev.allocator); const sfa = sfa_state.get(); - dev.server_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); - defer dev.server_graph.affected_by_trace.deinit(sfa); - - dev.client_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.client_graph.bundled_files.count()); - defer dev.client_graph.affected_by_trace.deinit(sfa); + var gts = try dev.initGraphTraceState(sfa); + defer gts.deinit(sfa); // Run tracing dev.client_graph.reset(); - try dev.traceAllRouteImports(route_bundle, .{ .find_css = true }); + try dev.traceAllRouteImports(route_bundle, >s, .{ .find_css = true }); const names = dev.client_graph.current_css_files.items; const arr = JSC.JSArray.createEmpty(dev.vm.global, names.len); @@ -1041,25 +1052,25 @@ fn generateCssList(dev: *DevServer, route_bundle: *RouteBundle) bun.OOM!JSC.JSVa return arr; } -fn traceAllRouteImports(dev: *DevServer, route_bundle: *RouteBundle, goal: TraceImportGoal) !void { +fn traceAllRouteImports(dev: *DevServer, route_bundle: *RouteBundle, gts: *GraphTraceState, goal: TraceImportGoal) !void { var route = dev.router.routePtr(route_bundle.route); const router_type = dev.router.typePtr(route.type); // Both framework entry points are considered - try dev.server_graph.traceImports(fromOpaqueFileId(.server, router_type.server_file), .{ .find_css = true }); + try dev.server_graph.traceImports(fromOpaqueFileId(.server, router_type.server_file), gts, .{ .find_css = true }); if (router_type.client_file.unwrap()) |id| { - try dev.client_graph.traceImports(fromOpaqueFileId(.client, id), goal); + try dev.client_graph.traceImports(fromOpaqueFileId(.client, id), gts, goal); } // The route file is considered if (route.file_page.unwrap()) |id| { - try dev.server_graph.traceImports(fromOpaqueFileId(.server, id), goal); + try dev.server_graph.traceImports(fromOpaqueFileId(.server, id), gts, goal); } // For all parents, the layout is considered while (true) { if (route.file_layout.unwrap()) |id| { - try dev.server_graph.traceImports(fromOpaqueFileId(.server, id), goal); + try dev.server_graph.traceImports(fromOpaqueFileId(.server, id), gts, goal); } route = dev.router.routePtr(route.parent.unwrap() orelse break); } @@ -1097,6 +1108,7 @@ pub const HotUpdateContext = struct { resolved_index_cache: []u32, /// Used to tell if the server should replace or append import records. server_seen_bit_set: DynamicBitSetUnmanaged, + gts: *GraphTraceState, pub fn getCachedIndex( rc: *const HotUpdateContext, @@ -1117,18 +1129,25 @@ pub const HotUpdateContext = struct { }; /// Called at the end of BundleV2 to index bundle contents into the `IncrementalGraph`s +/// This function does not recover DevServer state if it fails (allocation failure) pub fn finalizeBundle( dev: *DevServer, bv2: *bun.bundle_v2.BundleV2, result: bun.bundle_v2.BakeBundleOutput, -) !void { +) bun.OOM!void { + defer dev.startNextBundleIfPresent(); + const current_bundle = &dev.current_bundle.?; + + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); + const js_chunk = result.jsPseudoChunk(); const input_file_sources = bv2.graph.input_files.items(.source); const import_records = bv2.graph.ast.items(.import_records); const targets = bv2.graph.ast.items(.target); const scbs = bv2.graph.server_component_boundaries.slice(); - var sfa = std.heap.stackFallback(4096, bv2.graph.allocator); + var sfa = std.heap.stackFallback(65536, bv2.graph.allocator); const stack_alloc = sfa.get(); var scb_bitset = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(stack_alloc, input_file_sources.len); for ( @@ -1151,6 +1170,7 @@ pub fn finalizeBundle( .server_to_client_bitset = scb_bitset, .resolved_index_cache = resolved_index_cache, .server_seen_bit_set = undefined, + .gts = undefined, }; // Pass 1, update the graph's nodes, resolving every bundler source @@ -1166,7 +1186,8 @@ pub fn finalizeBundle( .client => try dev.client_graph.receiveChunk(&ctx, index, compile_result.code(), .js, false), } } - for (result.cssChunks(), result.css_file_list.metas) |*chunk, metadata| { + + for (result.cssChunks(), result.css_file_list.values()) |*chunk, metadata| { const index = bun.JSAst.Index.init(chunk.entry_point.source_index); const code = try chunk.intermediate_output.code( @@ -1180,7 +1201,7 @@ pub fn finalizeBundle( false, // TODO: sourcemaps true ); - // Create an asset entry for this file. + // Create an entry for this file. const abs_path = ctx.sources[index.get()].path.text; // Later code needs to retrieve the CSS content // The hack is to use `entry_point_id`, which is otherwise unused, to store an index. @@ -1200,13 +1221,13 @@ pub fn finalizeBundle( } } - dev.client_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(bv2.graph.allocator, dev.client_graph.bundled_files.count()); - defer dev.client_graph.affected_by_trace = .{}; - dev.server_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(bv2.graph.allocator, dev.server_graph.bundled_files.count()); - defer dev.client_graph.affected_by_trace = .{}; - + var gts = try dev.initGraphTraceState(bv2.graph.allocator); + defer gts.deinit(bv2.graph.allocator); + ctx.gts = >s; ctx.server_seen_bit_set = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(bv2.graph.allocator, dev.server_graph.bundled_files.count()); + dev.incremental_result.had_adjusted_edges = false; + // Pass 2, update the graph's edges by performing import diffing on each // changed file, removing dependencies. This pass also flags what routes // have been modified. @@ -1216,15 +1237,345 @@ pub fn finalizeBundle( .client => try dev.client_graph.processChunkDependencies(&ctx, part_range.source_index, bv2.graph.allocator), } } - for (result.cssChunks(), result.css_file_list.metas) |*chunk, metadata| { + for (result.cssChunks(), result.css_file_list.values()) |*chunk, metadata| { const index = bun.JSAst.Index.init(chunk.entry_point.source_index); // TODO: index css deps - _ = index; // autofix - _ = metadata; // autofix + _ = index; + _ = metadata; } // Index all failed files now that the incremental graph has been updated. try dev.indexFailures(); + + try dev.client_graph.ensureStaleBitCapacity(false); + try dev.server_graph.ensureStaleBitCapacity(false); + + dev.generation +%= 1; + if (Environment.enable_logs) { + debug.log("Bundle Round {d}: {d} server, {d} client, {d} ms", .{ + dev.generation, + dev.server_graph.current_chunk_parts.items.len, + dev.client_graph.current_chunk_parts.items.len, + @divFloor(current_bundle.timer.read(), std.time.ns_per_ms), + }); + } + + // Load all new chunks into the server runtime. + if (dev.server_graph.current_chunk_len > 0) { + const server_bundle = try dev.server_graph.takeBundle(.hmr_chunk, ""); + defer dev.allocator.free(server_bundle); + + const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.createLatin1(server_bundle)) catch |err| { + // No user code has been evaluated yet, since everything is to + // be wrapped in a function clousure. This means that the likely + // error is going to be a syntax error, or other mistake in the + // bundler. + dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); + @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); + }; + const errors = dev.server_register_update_callback.get().?.call( + dev.vm.global, + dev.vm.global.toJSValue(), + &.{ + server_modules, + dev.makeArrayForServerComponentsPatch(dev.vm.global, dev.incremental_result.client_components_added.items), + dev.makeArrayForServerComponentsPatch(dev.vm.global, dev.incremental_result.client_components_removed.items), + }, + ) catch |err| { + // One module replacement error should NOT prevent follow-up + // module replacements to fail. It is the HMR runtime's + // responsibility to collect all module load errors, and + // bubble them up. + dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); + @panic("Error thrown in Hot-module-replacement code. This is always a bug in the HMR runtime."); + }; + _ = errors; // TODO: + } + + var route_bits = try DynamicBitSetUnmanaged.initEmpty(stack_alloc, dev.route_bundles.items.len); + defer route_bits.deinit(stack_alloc); + var route_bits_client = try DynamicBitSetUnmanaged.initEmpty(stack_alloc, dev.route_bundles.items.len); + defer route_bits_client.deinit(stack_alloc); + + var has_route_bits_set = false; + + var hot_update_payload_sfa = std.heap.stackFallback(65536, bun.default_allocator); + var hot_update_payload = std.ArrayList(u8).initCapacity(hot_update_payload_sfa.get(), 65536) catch + unreachable; // enough space + defer hot_update_payload.deinit(); + hot_update_payload.appendAssumeCapacity(MessageId.hot_update.char()); + + // The writer used for the hot_update payload + const w = hot_update_payload.writer(); + + // It was discovered that if a tree falls with nobody around it, it does not + // make any sound. Let's avoid writing into `w` if no sockets are open. + const will_hear_hot_update = dev.numSubscribers(.hot_update) > 0; + + // This list of routes affected excludes client code. This means changing + // a client component wont count as a route to trigger a reload on. + // + // A second trace is required to determine what routes had changed bundles, + // since changing a layout affects all child routes. Additionally, routes + // that do not have a bundle will not be cleared (as there is nothing to + // clear for those) + if (will_hear_hot_update and + current_bundle.had_reload_event and + dev.incremental_result.routes_affected.items.len > 0 and + dev.bundling_failures.count() == 0) + { + has_route_bits_set = true; + + // A bit-set is used to avoid duplicate entries. This is not a problem + // with `dev.incremental_result.routes_affected` + for (dev.incremental_result.routes_affected.items) |request| { + const route = dev.router.routePtr(request.route_index); + if (route.bundle.unwrap()) |id| route_bits.set(id.get()); + if (request.should_recurse_when_visiting) { + markAllRouteChildren(&dev.router, 1, .{&route_bits}, request.route_index); + } + } + + // List 1 + var it = route_bits.iterator(.{ .kind = .set }); + while (it.next()) |bundled_route_index| { + const bundle = &dev.route_bundles.items[bundled_route_index]; + if (bundle.active_viewers == 0) continue; + try w.writeInt(i32, @intCast(bundled_route_index), .little); + } + } + try w.writeInt(i32, -1, .little); + + // When client component roots get updated, the `client_components_affected` + // list contains the server side versions of these roots. These roots are + // traced to the routes so that the client-side bundles can be properly + // invalidated. + if (dev.incremental_result.client_components_affected.items.len > 0) { + has_route_bits_set = true; + + dev.incremental_result.routes_affected.clearRetainingCapacity(); + gts.clear(); + + for (dev.incremental_result.client_components_affected.items) |index| { + try dev.server_graph.traceDependencies(index, >s, .no_stop); + } + + // A bit-set is used to avoid duplicate entries. This is not a problem + // with `dev.incremental_result.routes_affected` + for (dev.incremental_result.routes_affected.items) |request| { + const route = dev.router.routePtr(request.route_index); + if (route.bundle.unwrap()) |id| { + route_bits.set(id.get()); + route_bits_client.set(id.get()); + } + if (request.should_recurse_when_visiting) { + markAllRouteChildren(&dev.router, 2, .{ &route_bits, &route_bits_client }, request.route_index); + } + } + + // Free old bundles + var it = route_bits_client.iterator(.{ .kind = .set }); + while (it.next()) |bundled_route_index| { + const bundle = &dev.route_bundles.items[bundled_route_index]; + if (bundle.client_bundle) |old| { + dev.allocator.free(old); + } + bundle.client_bundle = null; + } + } + + // `route_bits` will have all of the routes that were modified. If any of + // these have active viewers, DevServer should inform them of CSS attachments. These + // route bundles also need to be invalidated of their css attachments. + if (has_route_bits_set and + (will_hear_hot_update or dev.incremental_result.had_adjusted_edges)) + { + var it = route_bits.iterator(.{ .kind = .set }); + // List 2 + while (it.next()) |i| { + const bundle = dev.routeBundlePtr(RouteBundle.Index.init(@intCast(i))); + if (dev.incremental_result.had_adjusted_edges) { + bundle.cached_css_file_array.clear(); + } + if (bundle.active_viewers == 0 or !will_hear_hot_update) continue; + try w.writeInt(i32, @intCast(i), .little); + try w.writeInt(u32, @intCast(bundle.full_pattern.len), .little); + try w.writeAll(bundle.full_pattern); + + // If no edges were changed, then it is impossible to + // change the list of CSS files. + if (dev.incremental_result.had_adjusted_edges) { + gts.clear(); + try dev.traceAllRouteImports(bundle, >s, .{ .find_css = true }); + const names = dev.client_graph.current_css_files.items; + + try w.writeInt(i32, @intCast(names.len), .little); + for (names) |name| { + const css_prefix_slash = css_prefix ++ "/"; + // These slices are url pathnames. The ID can be extracted + bun.assert(name.len == (css_prefix_slash ++ ".css").len + 16); + bun.assert(bun.strings.hasPrefix(name, css_prefix_slash)); + try w.writeAll(name[css_prefix_slash.len..][0..16]); + } + } else { + try w.writeInt(i32, -1, .little); + } + } + } + try w.writeInt(i32, -1, .little); + + // Send CSS mutations + const css_chunks = result.cssChunks(); + if (will_hear_hot_update) { + if (dev.client_graph.current_chunk_len > 0 or css_chunks.len > 0) { + const css_values = dev.css_files.values(); + try w.writeInt(u32, @intCast(css_chunks.len), .little); + const sources = bv2.graph.input_files.items(.source); + for (css_chunks) |chunk| { + const abs_path = sources[chunk.entry_point.source_index].path.text; + try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&bun.hash(abs_path)), .lower)); + const css_data = css_values[chunk.entry_point.entry_point_id]; + try w.writeInt(u32, @intCast(css_data.len), .little); + try w.writeAll(css_data); + } + + if (dev.client_graph.current_chunk_len > 0) + try dev.client_graph.takeBundleToList(.hmr_chunk, &hot_update_payload, ""); + } else { + try w.writeInt(i32, 0, .little); + } + + dev.publish(.hot_update, hot_update_payload.items, .binary); + } + + if (dev.incremental_result.failures_added.items.len > 0) { + dev.bundles_since_last_error = 0; + + for (dev.current_bundle_requests.items) |*req| { + const rb = dev.routeBundlePtr(req.route_bundle_index); + rb.server_state = .possible_bundling_failures; + + const resp: *Response = switch (req.data) { + .server_handler => |*saved| brk: { + const resp = saved.response.TCP; + saved.deinit(); + break :brk resp; + }, + .js_payload => |resp| resp, + }; + + resp.corked(sendSerializedFailures, .{ + dev, + resp, + dev.bundling_failures.keys(), + .bundler, + }); + } + return; + } + + // TODO: improve this visual feedback + if (dev.bundling_failures.count() == 0) { + if (current_bundle.had_reload_event) { + const clear_terminal = !debug.isVisible(); + if (clear_terminal) { + Output.disableBuffering(); + Output.resetTerminalAll(); + Output.enableBuffering(); + } + + dev.bundles_since_last_error += 1; + if (dev.bundles_since_last_error > 1) { + Output.prettyError("[x{d}] ", .{dev.bundles_since_last_error}); + } + } else { + dev.bundles_since_last_error = 0; + } + + Output.prettyError("{s} in {d}ms", .{ + if (current_bundle.had_reload_event) "Reloaded" else "Bundled route", + @divFloor(current_bundle.timer.read(), std.time.ns_per_ms), + }); + + // Compute a file name to display + const file_name: ?[]const u8, const total_count: usize = if (current_bundle.had_reload_event) + .{ null, 0 } + else first_route_file_name: { + const opaque_id = dev.router.routePtr( + dev.routeBundlePtr(dev.current_bundle_requests.items[0].route_bundle_index) + .route, + ).file_page.unwrap() orelse + break :first_route_file_name .{ null, 0 }; + const server_index = fromOpaqueFileId(.server, opaque_id); + + break :first_route_file_name .{ + dev.relativePath(dev.server_graph.bundled_files.keys()[server_index.get()]), + 0, + }; + }; + if (file_name) |name| { + Output.prettyError(": {s}", .{name}); + if (total_count > 1) { + Output.prettyError(" + {d} more", .{total_count - 1}); + } + } + Output.prettyError("\n", .{}); + Output.flush(); + } + + // Release the lock because the underlying handler may acquire one. + dev.graph_safety_lock.unlock(); + defer dev.graph_safety_lock.lock(); + + for (dev.current_bundle_requests.items) |req| { + const rb = dev.routeBundlePtr(req.route_bundle_index); + rb.server_state = .loaded; + + switch (req.data) { + .server_handler => |saved| dev.onRequestWithBundle(req.route_bundle_index, .{ .saved = saved }, saved.response.TCP), + .js_payload => |resp| dev.onJsRequestWithBundle(req.route_bundle_index, resp), + } + } +} + +fn startNextBundleIfPresent(dev: *DevServer) void { + // Clear the current bundle + dev.current_bundle = null; + dev.log.clearAndFree(); + dev.current_bundle_requests.clearRetainingCapacity(); + dev.emitVisualizerMessageIfNeeded() catch {}; + + // If there were pending requests, begin another bundle. + if (dev.next_bundle.reload_event != null or dev.next_bundle.requests.items.len > 0) { + var sfb = std.heap.stackFallback(4096, bun.default_allocator); + const temp_alloc = sfb.get(); + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); + + if (dev.next_bundle.reload_event) |event| { + event.processFileList(dev, &entry_points, temp_alloc); + + if (dev.watcher_atomics.recycleEventFromDevServer(event)) |second| { + second.processFileList(dev, &entry_points, temp_alloc); + dev.watcher_atomics.recycleSecondEventFromDevServer(second); + } + } + + for (dev.next_bundle.route_queue.keys()) |route_bundle_index| { + const rb = dev.routeBundlePtr(route_bundle_index); + rb.server_state = .bundling; + dev.appendRouteEntryPointsIfNotStale(&entry_points, temp_alloc, rb.route) catch bun.outOfMemory(); + } + + dev.startAsyncBundle( + entry_points, + dev.next_bundle.reload_event != null, + std.time.Timer.start() catch @panic("timers unsupported"), + ) catch bun.outOfMemory(); + + dev.next_bundle.route_queue.clearRetainingCapacity(); + dev.next_bundle.reload_event = null; + } } fn insertOrUpdateCssAsset(dev: *DevServer, abs_path: []const u8, code: []const u8) !u31 { @@ -1239,22 +1590,41 @@ fn insertOrUpdateCssAsset(dev: *DevServer, abs_path: []const u8, code: []const u pub fn handleParseTaskFailure( dev: *DevServer, + err: anyerror, graph: bake.Graph, abs_path: []const u8, log: *Log, ) bun.OOM!void { - // Print each error only once - Output.prettyErrorln("Errors while bundling '{s}':", .{ - dev.relativePath(abs_path), - }); - Output.flush(); - log.print(Output.errorWriter()) catch {}; + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); - return switch (graph) { - .server => dev.server_graph.insertFailure(abs_path, log, false), - .ssr => dev.server_graph.insertFailure(abs_path, log, true), - .client => dev.client_graph.insertFailure(abs_path, log, false), - }; + if (err == error.FileNotFound) { + // Special-case files being deleted. Note that if a + // file never existed, resolution would fail first. + // + // TODO: this should walk up the graph one level, and queue all of these + // files for re-bundling if they aren't already in the BundleV2 graph. + switch (graph) { + .server, .ssr => try dev.server_graph.onFileDeleted(abs_path, log), + .client => try dev.client_graph.onFileDeleted(abs_path, log), + } + } else { + Output.prettyErrorln("Error{s} while bundling \"{s}\":", .{ + if (log.errors +| log.warnings != 1) "s" else "", + dev.relativePath(abs_path), + }); + log.print(Output.errorWriterBuffered()) catch {}; + Output.flush(); + + // Do not index css errors + if (!bun.strings.hasSuffixComptime(abs_path, ".css")) { + switch (graph) { + .server => try dev.server_graph.insertFailure(abs_path, log, false), + .ssr => try dev.server_graph.insertFailure(abs_path, log, true), + .client => try dev.client_graph.insertFailure(abs_path, log, false), + } + } + } } const CacheEntry = struct { @@ -1262,6 +1632,9 @@ const CacheEntry = struct { }; pub fn isFileCached(dev: *DevServer, path: []const u8, side: bake.Graph) ?CacheEntry { + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); + switch (side) { inline else => |side_comptime| { const g = switch (side_comptime) { @@ -1282,7 +1655,8 @@ pub fn isFileCached(dev: *DevServer, path: []const u8, side: bake.Graph) ?CacheE fn appendOpaqueEntryPoint( dev: *DevServer, file_names: [][]const u8, - entry_points: *std.ArrayList(BakeEntryPoint), + entry_points: *EntryPointList, + alloc: Allocator, comptime side: bake.Side, optional_id: anytype, ) !void { @@ -1297,13 +1671,7 @@ fn appendOpaqueEntryPoint( .server => dev.server_graph.stale_files.isSet(file_index.get()), .client => dev.client_graph.stale_files.isSet(file_index.get()), }) { - try entry_points.append(.{ - .path = file_names[file_index.get()], - .graph = switch (side) { - .server => .server, - .client => .client, - }, - }); + try entry_points.appendJs(alloc, file_names[file_index.get()], side.graph()); } } @@ -1318,16 +1686,33 @@ fn onRequest(dev: *DevServer, req: *Request, resp: *Response) void { return; } + switch (dev.server.?) { + inline .DebugHTTPServer, .HTTPServer => |s| if (s.config.onRequest != .zero) { + s.onRequest(req, resp); + return; + }, + else => @panic("TODO: HTTPS"), + } + sendBuiltInNotFound(resp); } -fn insertRouteBundle(dev: *DevServer, route: Route.Index) !RouteBundle.Index { +fn getOrPutRouteBundle(dev: *DevServer, route: Route.Index) !RouteBundle.Index { + if (dev.router.routePtr(route).bundle.unwrap()) |bundle_index| + return bundle_index; + const full_pattern = full_pattern: { var buf = bake.PatternBuffer.empty; var current: *Route = dev.router.routePtr(route); - while (true) { - buf.prependPart(current.part); - current = dev.router.routePtr(current.parent.unwrap() orelse break); + // This loop is done to avoid prepending `/` at the root + // if there is more than one component. + buf.prependPart(current.part); + if (current.parent.unwrap()) |first| { + current = dev.router.routePtr(first); + while (current.parent.unwrap()) |next| { + buf.prependPart(current.part); + current = dev.router.routePtr(next); + } } break :full_pattern try dev.allocator.dupe(u8, buf.slice()); }; @@ -1342,6 +1727,7 @@ fn insertRouteBundle(dev: *DevServer, route: Route.Index) !RouteBundle.Index { .cached_module_list = .{}, .cached_client_bundle_url = .{}, .cached_css_file_array = .{}, + .active_viewers = 0, }); const bundle_index = RouteBundle.Index.init(@intCast(dev.route_bundles.items.len - 1)); dev.router.routePtr(route).bundle = bundle_index.toOptional(); @@ -1433,19 +1819,6 @@ fn sendBuiltInNotFound(resp: *Response) void { resp.end(message, true); } -fn sendStubErrorMessage(dev: *DevServer, route: *RouteBundle, resp: *Response, err: JSValue) void { - var sfb = std.heap.stackFallback(65536, dev.allocator); - var a = std.ArrayList(u8).initCapacity(sfb.get(), 65536) catch bun.outOfMemory(); - - a.writer().print("Server route handler for '{s}' threw while loading\n\n", .{ - route.pattern, - }) catch bun.outOfMemory(); - route.dev.vm.printErrorLikeObjectSimple(err, a.writer(), false); - - resp.writeStatus("500 Internal Server Error"); - resp.end(a.items, true); // TODO: "You should never call res.end(huge buffer)" -} - const FileKind = enum(u2) { /// Files that failed to bundle or do not exist on disk will appear in the /// graph as "unknown". @@ -1504,15 +1877,6 @@ pub fn IncrementalGraph(side: bake.Side) type { /// so garbage collection can run less often. edges_free_list: ArrayListUnmanaged(EdgeIndex), - // TODO: delete - /// Used during an incremental update to determine what "HMR roots" - /// are affected. Set for all `bundled_files` that have been visited - /// by the dependency tracing logic. - /// - /// Outside of an incremental bundle, this is empty. - /// Backed by the bundler thread's arena allocator. - affected_by_trace: DynamicBitSetUnmanaged, - /// Byte length of every file queued for concatenation current_chunk_len: usize = 0, /// All part contents @@ -1537,8 +1901,6 @@ pub fn IncrementalGraph(side: bake.Side) type { .edges = .{}, .edges_free_list = .{}, - .affected_by_trace = .{}, - .current_chunk_len = 0, .current_chunk_parts = .{}, @@ -1641,7 +2003,7 @@ pub fn IncrementalGraph(side: bake.Side) type { prev_dependency: EdgeIndex.Optional, }; - /// An index into `bundled_files`, `stale_files`, `first_dep`, `first_import`, or `affected_by_trace` + /// An index into `bundled_files`, `stale_files`, `first_dep`, `first_import` /// Top bits cannot be relied on due to `SerializedFailure.Owner.Packed` pub const FileIndex = bun.GenericIndex(u30, File); pub const react_refresh_index = if (side == .client) FileIndex.init(0); @@ -1798,7 +2160,7 @@ pub fn IncrementalGraph(side: bake.Side) type { const client_graph = &g.owner().client_graph; const client_index = client_graph.getFileIndex(gop.key_ptr.*) orelse Output.panic("Client graph's SCB was already deleted", .{}); - try dev.incremental_result.delete_client_files_later.append(g.owner().allocator, client_index); + client_graph.disconnectAndDeleteFile(client_index); gop.value_ptr.is_client_component_boundary = false; try dev.incremental_result.client_components_removed.append(dev.allocator, file_index); @@ -1886,6 +2248,8 @@ pub fn IncrementalGraph(side: bake.Side) type { // '.seen = false' means an import was removed and should be freed for (quick_lookup.values()) |val| { if (!val.seen) { + g.owner().incremental_result.had_adjusted_edges = true; + // Unlink from dependency list. At this point the edge is // already detached from the import list. g.disconnectEdgeFromDependencyList(val.edge_index); @@ -1897,7 +2261,7 @@ pub fn IncrementalGraph(side: bake.Side) type { if (side == .server) { // Follow this file to the route to mark it as stale. - try g.traceDependencies(file_index, .stop_at_boundary); + try g.traceDependencies(file_index, ctx.gts, .stop_at_boundary); } else { // TODO: Follow this file to the HMR root (info to determine is currently not stored) // without this, changing a client-only file will not mark the route's client bundle as stale @@ -1972,6 +2336,8 @@ pub fn IncrementalGraph(side: bake.Side) type { new_imports.* = edge.toOptional(); first_dep.* = edge.toOptional(); + g.owner().incremental_result.had_adjusted_edges = true; + log("attach edge={d} | id={d} {} -> id={d} {}", .{ edge.get(), file_index.get(), @@ -1987,22 +2353,23 @@ pub fn IncrementalGraph(side: bake.Side) type { const TraceDependencyKind = enum { stop_at_boundary, no_stop, + css_to_route, }; - fn traceDependencies(g: *@This(), file_index: FileIndex, trace_kind: TraceDependencyKind) !void { + fn traceDependencies(g: *@This(), file_index: FileIndex, gts: *GraphTraceState, trace_kind: TraceDependencyKind) !void { g.owner().graph_safety_lock.assertLocked(); if (Environment.enable_logs) { igLog("traceDependencies(.{s}, {}{s})", .{ @tagName(side), bun.fmt.quote(g.bundled_files.keys()[file_index.get()]), - if (g.affected_by_trace.isSet(file_index.get())) " [already visited]" else "", + if (gts.bits(side).isSet(file_index.get())) " [already visited]" else "", }); } - if (g.affected_by_trace.isSet(file_index.get())) + if (gts.bits(side).isSet(file_index.get())) return; - g.affected_by_trace.set(file_index.get()); + gts.bits(side).set(file_index.get()); const file = g.bundled_files.values()[file_index.get()]; @@ -2021,12 +2388,12 @@ pub fn IncrementalGraph(side: bake.Side) type { } }, .client => { - if (file.flags.is_hmr_root) { + if (file.flags.is_hmr_root or (file.flags.kind == .css and trace_kind == .css_to_route)) { const dev = g.owner(); const key = g.bundled_files.keys()[file_index.get()]; const index = dev.server_graph.getFileIndex(key) orelse Output.panic("Server Incremental Graph is missing component for {}", .{bun.fmt.quote(key)}); - try dev.server_graph.traceDependencies(index, trace_kind); + try dev.server_graph.traceDependencies(index, gts, trace_kind); } }, } @@ -2046,24 +2413,24 @@ pub fn IncrementalGraph(side: bake.Side) type { while (it) |dep_index| { const edge = g.edges.items[dep_index.get()]; it = edge.next_dependency.unwrap(); - try g.traceDependencies(edge.dependency, trace_kind); + try g.traceDependencies(edge.dependency, gts, trace_kind); } } - fn traceImports(g: *@This(), file_index: FileIndex, goal: TraceImportGoal) !void { + fn traceImports(g: *@This(), file_index: FileIndex, gts: *GraphTraceState, goal: TraceImportGoal) !void { g.owner().graph_safety_lock.assertLocked(); if (Environment.enable_logs) { igLog("traceImports(.{s}, {}{s})", .{ @tagName(side), bun.fmt.quote(g.bundled_files.keys()[file_index.get()]), - if (g.affected_by_trace.isSet(file_index.get())) " [already visited]" else "", + if (gts.bits(side).isSet(file_index.get())) " [already visited]" else "", }); } - if (g.affected_by_trace.isSet(file_index.get())) + if (gts.bits(side).isSet(file_index.get())) return; - g.affected_by_trace.set(file_index.get()); + gts.bits(side).set(file_index.get()); const file = g.bundled_files.values()[file_index.get()]; @@ -2074,7 +2441,7 @@ pub fn IncrementalGraph(side: bake.Side) type { const key = g.bundled_files.keys()[file_index.get()]; const index = dev.client_graph.getFileIndex(key) orelse Output.panic("Client Incremental Graph is missing component for {}", .{bun.fmt.quote(key)}); - try dev.client_graph.traceImports(index, goal); + try dev.client_graph.traceImports(index, gts, goal); } }, .client => { @@ -2105,7 +2472,7 @@ pub fn IncrementalGraph(side: bake.Side) type { while (it) |dep_index| { const edge = g.edges.items[dep_index.get()]; it = edge.next_import.unwrap(); - try g.traceImports(edge.imported, goal); + try g.traceImports(edge.imported, gts, goal); } } @@ -2216,9 +2583,8 @@ pub fn IncrementalGraph(side: bake.Side) type { try g.first_import.append(g.owner().allocator, .none); } - if (g.stale_files.bit_length > gop.index) { - g.stale_files.set(gop.index); - } + try g.ensureStaleBitCapacity(true); + g.stale_files.set(gop.index); switch (side) { .client => { @@ -2269,6 +2635,17 @@ pub fn IncrementalGraph(side: bake.Side) type { } } + pub fn onFileDeleted(g: *@This(), abs_path: []const u8, log: *const Log) !void { + const index = g.getFileIndex(abs_path) orelse return; + + if (g.first_dep.items[index.get()] == .none) { + g.disconnectAndDeleteFile(index); + } else { + // Keep the file so others may refer to it, but mark as failed. + try g.insertFailure(abs_path, log, false); + } + } + pub fn ensureStaleBitCapacity(g: *@This(), are_new_files_stale: bool) !void { try g.stale_files.resize( g.owner().allocator, @@ -2282,14 +2659,14 @@ pub fn IncrementalGraph(side: bake.Side) type { ); } - pub fn invalidate(g: *@This(), paths: []const []const u8, out_paths: *std.ArrayList(BakeEntryPoint)) !void { + pub fn invalidate(g: *@This(), paths: []const []const u8, entry_points: *EntryPointList, alloc: Allocator) !void { g.owner().graph_safety_lock.assertLocked(); const values = g.bundled_files.values(); for (paths) |path| { const index = g.bundled_files.getIndex(path) orelse { - // cannot enqueue because we don't know what targets to - // bundle for. instead, a failing bundle must retrieve the - // list of files and add them as stale. + // Cannot enqueue because it's impossible to know what + // targets to bundle for. Instead, a failing bundle must + // retrieve the list of files and add them as stale. continue; }; g.stale_files.set(index); @@ -2300,21 +2677,22 @@ pub fn IncrementalGraph(side: bake.Side) type { // the bundler gets confused and bundles both sides without // knowledge of the boundary between them. if (data.flags.kind == .css) - try out_paths.append(BakeEntryPoint.initCss(path)) + try entry_points.appendCss(alloc, path) else if (!data.flags.is_hmr_root) - try out_paths.append(BakeEntryPoint.init(path, .client)); + try entry_points.appendJs(alloc, path, .client); }, .server => { if (data.is_rsc) - try out_paths.append(BakeEntryPoint.init(path, .server)); + try entry_points.appendJs(alloc, path, .server); if (data.is_ssr and !data.is_client_component_boundary) - try out_paths.append(BakeEntryPoint.init(path, .ssr)); + try entry_points.appendJs(alloc, path, .ssr); }, } } } fn reset(g: *@This()) void { + g.owner().graph_safety_lock.assertLocked(); g.current_chunk_len = 0; g.current_chunk_parts.clearRetainingCapacity(); if (side == .client) g.current_css_files.clearRetainingCapacity(); @@ -2430,8 +2808,6 @@ pub fn IncrementalGraph(side: bake.Side) type { } fn disconnectAndDeleteFile(g: *@This(), file_index: FileIndex) void { - const last = FileIndex.init(@intCast(g.bundled_files.count() - 1)); - bun.assert(g.bundled_files.count() > 1); // never remove all files bun.assert(g.first_dep.items[file_index.get()] == .none); // must have no dependencies @@ -2445,49 +2821,21 @@ pub fn IncrementalGraph(side: bake.Side) type { g.disconnectEdgeFromDependencyList(edge_index); g.freeEdge(edge_index); - } - } - // TODO: it is infeasible to do this since FrameworkRouter contains file indices - // to the server graph - { - return; + // TODO: a flag to this function which is queues all + // direct importers to rebuild themselves, which will + // display the bundling errors. + } } - g.bundled_files.swapRemoveAt(file_index.get()); + const keys = g.bundled_files.keys(); - // Move out-of-line data from `last` to replace `file_index` - _ = g.first_dep.swapRemove(file_index.get()); - _ = g.first_import.swapRemove(file_index.get()); + g.owner().allocator.free(keys[file_index.get()]); + keys[file_index.get()] = ""; // cannot be `undefined` as it may be read by hashmap logic - if (file_index != last) { - g.stale_files.setValue(file_index.get(), g.stale_files.isSet(last.get())); - - // This set is not always initialized, so ignore if it's empty - if (g.affected_by_trace.bit_length > 0) { - g.affected_by_trace.setValue(file_index.get(), g.affected_by_trace.isSet(last.get())); - } - - // Adjust all referenced edges to point to the new file - { - var it: ?EdgeIndex = g.first_import.items[file_index.get()].unwrap(); - while (it) |edge_index| { - const dep = &g.edges.items[edge_index.get()]; - it = dep.next_import.unwrap(); - assert(dep.dependency == last); - dep.dependency = file_index; - } - } - { - var it: ?EdgeIndex = g.first_dep.items[file_index.get()].unwrap(); - while (it) |edge_index| { - const dep = &g.edges.items[edge_index.get()]; - it = dep.next_dependency.unwrap(); - assert(dep.imported == last); - dep.imported = file_index; - } - } - } + // TODO: it is infeasible to swapRemove a file since FrameworkRouter + // contains file indices to the server graph. Instead, `file_index` + // should go in a free-list for use by new files. } fn newEdge(g: *@This(), edge: Edge) !EdgeIndex { @@ -2530,6 +2878,8 @@ const IncrementalResult = struct { /// are affected, the route graph must be traced downwards. /// Tracing is used for multiple purposes. routes_affected: ArrayListUnmanaged(RouteIndexAndRecurseFlag), + /// Set to true if any IncrementalGraph edges were added or removed. + had_adjusted_edges: bool, // Following three fields are populated during `receiveChunk` @@ -2551,7 +2901,7 @@ const IncrementalResult = struct { client_components_affected: ArrayListUnmanaged(IncrementalGraph(.server).FileIndex), /// The list of failures which will have to be traced to their route. Such - /// tracing is deferred until the second pass of finalizeBundler as the + /// tracing is deferred until the second pass of finalizeBundle as the /// dependency graph may not fully exist at the time the failure is indexed. /// /// Populated from within the bundler via `handleParseTaskFailure` @@ -2563,6 +2913,7 @@ const IncrementalResult = struct { const empty: IncrementalResult = .{ .routes_affected = .{}, + .had_adjusted_edges = false, .failures_removed = .{}, .failures_added = .{}, .client_components_added = .{}, @@ -2581,10 +2932,20 @@ const IncrementalResult = struct { } }; +/// Used during an incremental update to determine what "HMR roots" +/// are affected. Set for all `bundled_files` that have been visited +/// by the dependency tracing logic. const GraphTraceState = struct { client_bits: DynamicBitSetUnmanaged, server_bits: DynamicBitSetUnmanaged, + fn bits(gts: *GraphTraceState, side: bake.Side) *DynamicBitSetUnmanaged { + return switch (side) { + .client => >s.client_bits, + .server => >s.server_bits, + }; + } + fn deinit(gts: *GraphTraceState, alloc: Allocator) void { gts.client_bits.deinit(alloc); gts.server_bits.deinit(alloc); @@ -2603,7 +2964,7 @@ const TraceImportGoal = struct { }; fn initGraphTraceState(dev: *const DevServer, sfa: Allocator) !GraphTraceState { - const server_bits = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); + var server_bits = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); errdefer server_bits.deinit(sfa); const client_bits = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.client_graph.bundled_files.count()); return .{ .server_bits = server_bits, .client_bits = client_bits }; @@ -2658,6 +3019,8 @@ const DirectoryWatchStore = struct { // `import_source` is not a stable string. let's share memory with the file graph. // this requires that const dev = store.owner(); + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); const owned_file_path = switch (renderer) { .client => path: { const index = try dev.client_graph.insertStale(import_source, false); @@ -3034,10 +3397,13 @@ pub const SerializedFailure = struct { fn writeLogData(data: bun.logger.Data, w: Writer) !void { try writeString32(data.text, w); if (data.location) |loc| { - assert(loc.line >= 0); // one based and not negative + if (loc.line < 0) { + try w.writeInt(u32, 0, .little); + return; + } assert(loc.column >= 0); // zero based and not negative - try w.writeInt(u32, @intCast(loc.line), .little); + try w.writeInt(i32, @intCast(loc.line), .little); try w.writeInt(u32, @intCast(loc.column), .little); try w.writeInt(u32, @intCast(loc.length), .little); @@ -3122,7 +3488,7 @@ fn emitVisualizerMessageIfNeeded(dev: *DevServer) !void { try dev.writeVisualizerMessage(&payload); - dev.publish(HmrSocket.visualizer_topic, payload.items, .binary); + dev.publish(.visualizer, payload.items, .binary); } fn writeVisualizerMessage(dev: *DevServer, payload: *std.ArrayList(u8)) !void { @@ -3182,7 +3548,15 @@ pub fn onWebSocketUpgrade( const dw = bun.create(dev.allocator, HmrSocket, .{ .dev = dev, - .emit_visualizer_events = false, + .is_from_localhost = if (res.getRemoteSocketInfo()) |addr| + if (addr.is_ipv6) + bun.strings.eqlComptime(addr.ip, "::1") + else + bun.strings.eqlComptime(addr.ip, "127.0.0.1") + else + false, + .subscriptions = .{}, + .active_route = .none, }); res.upgrade( *HmrSocket, @@ -3197,50 +3571,73 @@ pub fn onWebSocketUpgrade( /// Every message is to use `.binary`/`ArrayBuffer` transport mode. The first byte /// indicates a Message ID; see comments on each type for how to interpret the rest. /// -/// This format is only intended for communication for the browser build of -/// `hmr-runtime.ts` <-> `DevServer.zig`. Server-side HMR is implemented using a -/// different interface. This document is aimed for contributors to these two -/// components; Any other use-case is unsupported. +/// This format is only intended for communication via the browser and DevServer. +/// Server-side HMR is implemented using a different interface. This API is not +/// versioned alongside Bun; breaking changes may occur at any point. /// /// All integers are sent in little-endian pub const MessageId = enum(u8) { /// Version payload. Sent on connection startup. The client should issue a /// hard-reload when it mismatches with its `config.version`. version = 'V', - /// Sent on a successful bundle, containing client code and changed CSS files. - /// - /// - u32: Number of CSS updates. For Each: - /// - [16]u8 ASCII: CSS identifier (hash of source path) - /// - u32: Length of CSS code - /// - [n]u8 UTF-8: CSS payload - /// - [n]u8 UTF-8: JS Payload. No length, rest of buffer is text. - /// - /// The JS payload will be code to hand to `eval` - // TODO: the above structure does not consider CSS attachments/detachments - hot_update = 'u', - /// Sent on a successful bundle, containing a list of routes that have - /// server changes. This is not sent when only client code changes. + /// Sent on a successful bundle, containing client code, updates routes, and + /// changed CSS files. Emitted on the `.hot_update` topic. /// - /// - `u32`: Number of updated routes. - /// - For each route: - /// - `u32`: Route ID + /// - For each server-side updated route: + /// - `i32`: Route Bundle ID + /// - `i32`: -1 to indicate end of list + /// - For each route stylesheet lists affected: + /// - `i32`: Route Bundle ID /// - `u32`: Length of route pattern /// - `[n]u8` UTF-8: Route pattern + /// - `u32`: Number of CSS attachments: For Each + /// - `[16]u8` ASCII: CSS identifier + /// - `i32`: -1 to indicate end of list + /// - `u32`: Number of CSS mutations. For Each: + /// - `[16]u8` ASCII: CSS identifier + /// - `u32`: Length of CSS code + /// - `[n]u8` UTF-8: CSS payload + /// - `[n]u8` UTF-8: JS Payload. No length, rest of buffer is text. + /// Can be empty if no client-side code changed. /// - /// HMR Runtime contains code that performs route matching at runtime - /// against `location.pathname`. The server is unaware of its routing - /// state. - route_update = 'R', + /// The first list contains route changes that require a page reload, but + /// frameworks can perform via `onServerSideReload`. Fallback behavior + /// is to call `location.reload();` + /// + /// The second list is sent to inform the current list of CSS files + /// reachable by a route, recalculated whenever an import is added or + /// removed as that can inadvertently affect the CSS list. + /// + /// The third list contains CSS mutations, which are when the underlying + /// CSS file itself changes. + /// + /// The JS payload is the remaining data. If defined, it can be passed to + /// `eval`, resulting in an object of new module callables. + hot_update = 'u', /// Sent when the list of errors changes. /// /// - `u32`: Removed errors. For Each: /// - `u32`: Error owner /// - Remainder are added errors. For Each: /// - `SerializedFailure`: Error Data - errors = 'E', - /// Sent when all errors are cleared. - // TODO: Remove this message ID - errors_cleared = 'c', + errors = 'e', + /// A message from the browser. This is used to communicate. + /// - `u32`: Unique ID for the browser tab. Each tab gets a different ID + /// - `[n]u8`: Opaque bytes, untouched from `IncomingMessageId.browser_error` + browser_message = 'b', + /// Sent to clear the messages from `browser_error` + /// - For each removed ID: + /// - `u32`: Unique ID for the browser tab. + browser_message_clear = 'B', + /// Sent when a request handler error is emitted. Each route will own at + /// most 1 error, where sending a new request clears the original one. + /// + /// - `u32`: Removed errors. For Each: + /// - `u32`: Error owner + /// - `u32`: Length of route pattern + /// - `[n]u8`: UTF-8 Route pattern + /// - `SerializedFailure`: The one error list for the request + request_handler_error = 'h', /// Payload for `incremental_visualizer.html`. This can be accessed via /// `/_bun/incremental_visualizer`. This contains both graphs. /// @@ -3268,22 +3665,74 @@ pub const MessageId = enum(u8) { }; pub const IncomingMessageId = enum(u8) { - /// Subscribe to `.visualizer` events. No payload. + /// Subscribe to an event channel. Payload is a sequence of chars available + /// in HmrTopic. + subscribe = 's', + // /// Subscribe to `.route_manifest` events. No payload. + // subscribe_route_manifest = 'r', + // /// Emit a hot update for a file without actually changing its on-disk + // /// content. This can be used by an editor extension to stream contents in + // /// IDE to reflect in the browser. This is gated to only work on localhost + // /// socket connections. + // virtual_file_change = 'w', + /// Emitted on client-side navigations. + /// Rest of payload is a UTF-8 string. + set_url = 'n', + /// Emit a message from the browser. Payload is opaque bytes that DevServer + /// does not care about. In practice, the payload is a JSON object. + browser_message = 'm', + + /// Invalid data + _, +}; + +const HmrTopic = enum(u8) { + hot_update = 'h', + errors = 'e', + browser_error = 'E', visualizer = 'v', + // route_manifest = 'r', + /// Invalid data _, + + pub const max_count = @typeInfo(HmrTopic).Enum.fields.len; + pub const Bits = @Type(.{ .Struct = .{ + .backing_integer = @Type(.{ .Int = .{ + .bits = max_count, + .signedness = .unsigned, + } }), + .fields = &brk: { + const enum_fields = @typeInfo(HmrTopic).Enum.fields; + var fields: [enum_fields.len]std.builtin.Type.StructField = undefined; + for (enum_fields, &fields) |e, *s| { + s.* = .{ + .name = e.name, + .type = bool, + .default_value = &false, + .is_comptime = false, + .alignment = 0, + }; + } + break :brk fields; + }, + .decls = &.{}, + .is_tuple = false, + .layout = .@"packed", + } }); }; const HmrSocket = struct { dev: *DevServer, - emit_visualizer_events: bool, - - pub const global_topic = "*"; - pub const visualizer_topic = "v"; - + subscriptions: HmrTopic.Bits, + /// Allows actions which inspect or mutate sensitive DevServer state. + is_from_localhost: bool, + /// By telling DevServer the active route, this enables receiving detailed + /// `hot_update` events for when the route is updated. + active_route: RouteBundle.Index.Optional, + /// Files which the client definitely has and should not be re-sent pub fn onOpen(s: *HmrSocket, ws: AnyWebSocket) void { _ = ws.send(&(.{MessageId.version.char()} ++ s.dev.configuration_hash_key), .binary, false, true); - _ = ws.subscribe(global_topic); } pub fn onMessage(s: *HmrSocket, ws: AnyWebSocket, msg: []const u8, opcode: uws.Opcode) void { @@ -3295,17 +3744,56 @@ const HmrSocket = struct { } switch (@as(IncomingMessageId, @enumFromInt(msg[0]))) { - .visualizer => { - if (!s.emit_visualizer_events) { - s.emit_visualizer_events = true; - s.dev.emit_visualizer_events += 1; - _ = ws.subscribe(visualizer_topic); - s.dev.emitVisualizerMessageIfNeeded() catch bun.outOfMemory(); + .subscribe => { + var new_bits: HmrTopic.Bits = .{}; + const topics = msg[1..]; + if (topics.len > HmrTopic.max_count) return; + outer: for (topics) |char| { + inline for (@typeInfo(HmrTopic).Enum.fields) |field| { + if (char == field.value) { + @field(new_bits, field.name) = true; + continue :outer; + } + } + } + inline for (comptime std.enums.values(HmrTopic)) |field| { + if (@field(new_bits, @tagName(field)) and !@field(s.subscriptions, @tagName(field))) { + _ = ws.subscribe(&.{@intFromEnum(field)}); + + // on-subscribe hooks + switch (field) { + .visualizer => { + s.dev.emit_visualizer_events += 1; + s.dev.emitVisualizerMessageIfNeeded() catch bun.outOfMemory(); + }, + else => {}, + } + } else if (@field(new_bits, @tagName(field)) and !@field(s.subscriptions, @tagName(field))) { + _ = ws.unsubscribe(&.{@intFromEnum(field)}); + + // on-unsubscribe hooks + switch (field) { + .visualizer => { + s.dev.emit_visualizer_events -= 1; + }, + else => {}, + } + } } }, - else => { - ws.close(); + .set_url => { + const pattern = msg[1..]; + var params: FrameworkRouter.MatchedParams = undefined; + if (s.dev.router.matchSlow(pattern, ¶ms)) |route| { + const rbi = s.dev.getOrPutRouteBundle(route) catch bun.outOfMemory(); + if (s.active_route.unwrap()) |old| { + if (old == rbi) return; + s.dev.routeBundlePtr(old).active_viewers -= 1; + } + s.dev.routeBundlePtr(rbi).active_viewers += 1; + } }, + else => ws.close(), } } @@ -3314,10 +3802,14 @@ const HmrSocket = struct { _ = exit_code; _ = message; - if (s.emit_visualizer_events) { + if (s.subscriptions.visualizer) { s.dev.emit_visualizer_events -= 1; } + if (s.active_route.unwrap()) |old| { + s.dev.routeBundlePtr(old).active_viewers -= 1; + } + defer s.dev.allocator.destroy(s); } }; @@ -3343,199 +3835,90 @@ const c = struct { }; /// Called on DevServer thread via HotReloadTask -pub fn reload(dev: *DevServer, reload_task: *HotReloadTask) bun.OOM!void { - defer reload_task.files.clearRetainingCapacity(); - - const changed_file_paths = reload_task.files.keys(); - // TODO: check for .delete and remove items from graph. this has to be done - // with care because some editors save by deleting and recreating the file. - // delete events are not to be trusted at face value. also, merging of - // events can cause .write and .delete to be true at the same time. - const changed_file_attributes = reload_task.files.values(); - _ = changed_file_attributes; - - var timer = std.time.Timer.start() catch - @panic("timers unsupported"); +pub fn startReloadBundle(dev: *DevServer, event: *HotReloadEvent) bun.OOM!void { + defer event.files.clearRetainingCapacity(); var sfb = std.heap.stackFallback(4096, bun.default_allocator); - var temp_alloc = sfb.get(); - - // pre-allocate a few files worth of strings. it is unlikely but supported - // to change more than 8 files in the same bundling round. - var files = std.ArrayList(BakeEntryPoint).initCapacity(temp_alloc, 8) catch unreachable; - defer files.deinit(); - - { - dev.graph_safety_lock.lock(); - defer dev.graph_safety_lock.unlock(); - - inline for (.{ &dev.server_graph, &dev.client_graph }) |g| { - g.invalidate(changed_file_paths, &files) catch bun.outOfMemory(); - } - } + const temp_alloc = sfb.get(); + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); - if (files.items.len == 0) { - Output.debugWarn("nothing to bundle?? this is a bug?", .{}); + event.processFileList(dev, &entry_points, temp_alloc); + if (entry_points.set.count() == 0) { + Output.debugWarn("nothing to bundle. watcher may potentially be watching too many files.", .{}); return; } - dev.incremental_result.reset(); - defer { - // Remove files last to start, to avoid issues where removing a file - // invalidates the last file index. - std.sort.pdq( - IncrementalGraph(.client).FileIndex, - dev.incremental_result.delete_client_files_later.items, - {}, - IncrementalGraph(.client).FileIndex.sortFnDesc, - ); - for (dev.incremental_result.delete_client_files_later.items) |client_index| { - dev.client_graph.disconnectAndDeleteFile(client_index); - } - dev.incremental_result.delete_client_files_later.clearRetainingCapacity(); - } - - dev.bundle(files.items) catch |err| { + dev.startAsyncBundle( + entry_points, + true, + event.timer, + ) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); return; }; - - dev.graph_safety_lock.lock(); - defer dev.graph_safety_lock.unlock(); - - // This list of routes affected excludes client code. This means changing - // a client component wont count as a route to trigger a reload on. - // - // A second trace is required to determine what routes had changed bundles, - // since changing a layout affects all child routes. Additionally, routes - // that do not have a bundle will not be cleared (as there is nothing to - // clear for those) - if (dev.incremental_result.routes_affected.items.len > 0) { - // re-use some earlier stack memory - files.clearAndFree(); - sfb = std.heap.stackFallback(4096, bun.default_allocator); - temp_alloc = sfb.get(); - - // A bit-set is used to avoid duplicate entries. This is not a problem - // with `dev.incremental_result.routes_affected` - var second_trace_result = try DynamicBitSetUnmanaged.initEmpty(temp_alloc, dev.route_bundles.items.len); - for (dev.incremental_result.routes_affected.items) |request| { - const route = dev.router.routePtr(request.route_index); - if (route.bundle.unwrap()) |id| second_trace_result.set(id.get()); - if (request.should_recurse_when_visiting) { - markAllRouteChildren(&dev.router, &second_trace_result, request.route_index); - } - } - - var sfb2 = std.heap.stackFallback(65536, bun.default_allocator); - var payload = std.ArrayList(u8).initCapacity(sfb2.get(), 65536) catch - unreachable; // enough space - defer payload.deinit(); - payload.appendAssumeCapacity(MessageId.route_update.char()); - const w = payload.writer(); - const count = second_trace_result.count(); - assert(count > 0); - try w.writeInt(u32, @intCast(count), .little); - - var it = second_trace_result.iterator(.{ .kind = .set }); - while (it.next()) |bundled_route_index| { - try w.writeInt(u32, @intCast(bundled_route_index), .little); - const pattern = dev.route_bundles.items[bundled_route_index].full_pattern; - try w.writeInt(u32, @intCast(pattern.len), .little); - try w.writeAll(pattern); - } - - // Notify - dev.publish(HmrSocket.global_topic, payload.items, .binary); - } - - // When client component roots get updated, the `client_components_affected` - // list contains the server side versions of these roots. These roots are - // traced to the routes so that the client-side bundles can be properly - // invalidated. - if (dev.incremental_result.client_components_affected.items.len > 0) { - dev.incremental_result.routes_affected.clearRetainingCapacity(); - dev.server_graph.affected_by_trace.setAll(false); - - var sfa_state = std.heap.stackFallback(65536, dev.allocator); - const sfa = sfa_state.get(); - dev.server_graph.affected_by_trace = try DynamicBitSetUnmanaged.initEmpty(sfa, dev.server_graph.bundled_files.count()); - defer dev.server_graph.affected_by_trace.deinit(sfa); - - for (dev.incremental_result.client_components_affected.items) |index| { - try dev.server_graph.traceDependencies(index, .no_stop); - } - - // TODO: - // for (dev.incremental_result.routes_affected.items) |route| { - // // Free old bundles - // if (dev.routes[route.get()].client_bundle) |old| { - // dev.allocator.free(old); - // } - // dev.routes[route.get()].client_bundle = null; - // } - } - - // TODO: improve this visual feedback - if (dev.bundling_failures.count() == 0) { - const clear_terminal = !debug.isVisible(); - if (clear_terminal) { - Output.flush(); - Output.disableBuffering(); - Output.resetTerminalAll(); - } - - dev.bundles_since_last_error += 1; - if (dev.bundles_since_last_error > 1) { - Output.prettyError("[x{d}] ", .{dev.bundles_since_last_error}); - } - - Output.prettyError("Reloaded in {d}ms: {s}", .{ @divFloor(timer.read(), std.time.ns_per_ms), dev.relativePath(changed_file_paths[0]) }); - if (changed_file_paths.len > 1) { - Output.prettyError(" + {d} more", .{files.items.len - 1}); - } - Output.prettyError("\n", .{}); - Output.flush(); - } else {} } -fn markAllRouteChildren(router: *FrameworkRouter, bits: *DynamicBitSetUnmanaged, route_index: Route.Index) void { +fn markAllRouteChildren(router: *FrameworkRouter, comptime n: comptime_int, bits: [n]*DynamicBitSetUnmanaged, route_index: Route.Index) void { var next = router.routePtr(route_index).first_child.unwrap(); while (next) |child_index| { const route = router.routePtr(child_index); - if (route.bundle.unwrap()) |index| bits.set(index.get()); - markAllRouteChildren(router, bits, child_index); + if (route.bundle.unwrap()) |index| { + inline for (bits) |b| + b.set(index.get()); + } + markAllRouteChildren(router, n, bits, child_index); next = route.next_sibling.unwrap(); } } -pub const HotReloadTask = struct { - /// Align to cache lines to reduce contention. - const Aligned = struct { aligned: HotReloadTask align(std.atomic.cache_line) }; - - dev: *DevServer, - concurrent_task: JSC.ConcurrentTask = undefined, +fn markAllRouteChildrenFailed(dev: *DevServer, route_index: Route.Index) void { + var next = dev.router.routePtr(route_index).first_child.unwrap(); + while (next) |child_index| { + const route = dev.router.routePtr(child_index); + if (route.bundle.unwrap()) |index| { + dev.routeBundlePtr(index).server_state = .possible_bundling_failures; + } + markAllRouteChildrenFailed(dev, child_index); + next = route.next_sibling.unwrap(); + } +} +/// This task informs the DevServer's thread about new files to be bundled. +pub const HotReloadEvent = struct { + /// Align to cache lines to eliminate contention. + const Aligned = struct { aligned: HotReloadEvent align(std.atomic.cache_line) }; + + owner: *DevServer, + /// Initialized in WatcherAtomics.watcherReleaseAndSubmitEvent + concurrent_task: JSC.ConcurrentTask, + /// The watcher is not able to peek into the incremental graph to know what + /// files to invalidate, so the watch events are de-duplicated and passed + /// along. files: bun.StringArrayHashMapUnmanaged(Watcher.Event.Op), + /// Initialized by the WatcherAtomics.watcherAcquireEvent + timer: std.time.Timer, + /// This event may be referenced by either DevServer or Watcher thread. + /// 1 if referenced, 0 if unreferenced; see WatcherAtomics + contention_indicator: std.atomic.Value(u32), - /// I am sorry. - state: std.atomic.Value(u32), - - pub fn initEmpty(dev: *DevServer) HotReloadTask { + pub fn initEmpty(owner: *DevServer) HotReloadEvent { return .{ - .dev = dev, + .owner = owner, + .concurrent_task = undefined, .files = .{}, - .state = .{ .raw = 0 }, + .timer = undefined, + .contention_indicator = std.atomic.Value(u32).init(0), }; } pub fn append( - task: *HotReloadTask, + event: *HotReloadEvent, allocator: Allocator, file_path: []const u8, op: Watcher.Event.Op, ) void { - const gop = task.files.getOrPut(allocator, file_path) catch bun.outOfMemory(); + const gop = event.files.getOrPut(allocator, file_path) catch bun.outOfMemory(); if (gop.found_existing) { gop.value_ptr.* = gop.value_ptr.merge(op); } else { @@ -3543,79 +3926,249 @@ pub const HotReloadTask = struct { } } - pub fn run(initial: *HotReloadTask) void { + /// Invalidates items in IncrementalGraph, appending all new items to `entry_points` + pub fn processFileList( + event: *HotReloadEvent, + dev: *DevServer, + entry_points: *EntryPointList, + alloc: Allocator, + ) void { + const changed_file_paths = event.files.keys(); + // TODO: check for .delete and remove items from graph. this has to be done + // with care because some editors save by deleting and recreating the file. + // delete events are not to be trusted at face value. also, merging of + // events can cause .write and .delete to be true at the same time. + const changed_file_attributes = event.files.values(); + _ = changed_file_attributes; + + { + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); + + inline for (.{ &dev.server_graph, &dev.client_graph }) |g| { + g.invalidate(changed_file_paths, entry_points, alloc) catch bun.outOfMemory(); + } + } + } + + pub fn run(first: *HotReloadEvent) void { debug.log("HMR Task start", .{}); defer debug.log("HMR Task end", .{}); - // TODO: audit the atomics with this reloading strategy - // It was not written by an expert. - - const dev = initial.dev; + const dev = first.owner; if (Environment.allow_assert) { - assert(initial.state.load(.seq_cst) == 0); + assert(first.contention_indicator.load(.seq_cst) == 0); } - // const start_timestamp = std.time.nanoTimestamp(); - dev.reload(initial) catch bun.outOfMemory(); + if (dev.current_bundle != null) { + dev.next_bundle.reload_event = first; + return; + } - // if there was a pending run, do it now - if (dev.watch_state.swap(0, .seq_cst) > 1) { - // debug.log("dual event fire", .{}); - const current = if (initial == &dev.watch_events[0].aligned) - &dev.watch_events[1].aligned - else - &dev.watch_events[0].aligned; - if (current.state.swap(1, .seq_cst) == 0) { - // debug.log("case 1 (run now)", .{}); - dev.reload(current) catch bun.outOfMemory(); - current.state.store(0, .seq_cst); + // defer event.files.clearRetainingCapacity(); + + var sfb = std.heap.stackFallback(4096, bun.default_allocator); + const temp_alloc = sfb.get(); + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); + + first.processFileList(dev, &entry_points, temp_alloc); + const timer = first.timer; + + if (dev.watcher_atomics.recycleEventFromDevServer(first)) |second| { + second.processFileList(dev, &entry_points, temp_alloc); + dev.watcher_atomics.recycleSecondEventFromDevServer(second); + } + + if (entry_points.set.count() == 0) { + Output.debugWarn("nothing to bundle. watcher may potentially be watching too many files.", .{}); + return; + } + + dev.startAsyncBundle( + entry_points, + true, + timer, + ) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + return; + }; + } +}; + +/// All code working with atomics to communicate watcher is in this struct. It +/// attempts to recycle as much memory as possible since files are very +/// frequently updated. +const WatcherAtomics = struct { + const log = Output.scoped(.DevServerWatchAtomics, true); + + /// Only two hot-reload tasks exist ever, since only one bundle may be active at + /// once. Memory is reused by swapping between these two. These items are + /// aligned to cache lines to reduce contention, since these structures are + /// carefully passed between two threads. + events: [2]HotReloadEvent.Aligned align(std.atomic.cache_line), + /// 0 - no watch + /// 1 - has fired additional watch + /// 2+ - new events available, watcher is waiting on bundler to finish + watcher_events_emitted: std.atomic.Value(u32), + /// Which event is the watcher holding on to. + /// This is not atomic because only the watcher thread uses this value. + current: u1 align(std.atomic.cache_line), + + watcher_has_event: std.debug.SafetyLock, + dev_server_has_event: std.debug.SafetyLock, + + pub fn init(dev: *DevServer) WatcherAtomics { + return .{ + .events = .{ + .{ .aligned = HotReloadEvent.initEmpty(dev) }, + .{ .aligned = HotReloadEvent.initEmpty(dev) }, + }, + .current = 0, + .watcher_events_emitted = std.atomic.Value(u32).init(0), + .watcher_has_event = .{}, + .dev_server_has_event = .{}, + }; + } + + /// Atomically get a *HotReloadEvent that is not used by the DevServer thread + /// Call `watcherRelease` when it is filled with files. + fn watcherAcquireEvent(state: *WatcherAtomics) *HotReloadEvent { + state.watcher_has_event.lock(); + + var ev: *HotReloadEvent = &state.events[state.current].aligned; + switch (ev.contention_indicator.swap(1, .seq_cst)) { + 0 => { + // New event, initialize the timer if it is empty. + if (ev.files.count() == 0) + ev.timer = std.time.Timer.start() catch unreachable; + }, + 1 => { + // @branchHint(.unlikely); + // DevServer stole this event. Unlikely but possible when + // the user is saving very heavily (10-30 times per second) + state.current +%= 1; + ev = &state.events[state.current].aligned; + if (Environment.allow_assert) { + bun.assert(ev.contention_indicator.swap(1, .seq_cst) == 0); + } + }, + else => unreachable, + } + + ev.owner.bun_watcher.thread_lock.assertLocked(); + + return ev; + } + + /// Release the pointer from `watcherAcquireHotReloadEvent`, submitting + /// the event if it contains new files. + fn watcherReleaseAndSubmitEvent(state: *WatcherAtomics, ev: *HotReloadEvent) void { + state.watcher_has_event.unlock(); + ev.owner.bun_watcher.thread_lock.assertLocked(); + + if (ev.files.count() > 0) { + // @branchHint(.likely); + // There are files to be processed, increment this count first. + const prev_count = state.watcher_events_emitted.fetchAdd(1, .seq_cst); + + if (prev_count == 0) { + // @branchHint(.likely); + // Submit a task to the DevServer, notifying it that there is + // work to do. The watcher will move to the other event. + ev.concurrent_task = .{ + .auto_delete = false, + .next = null, + .task = JSC.Task.init(ev), + }; + ev.contention_indicator.store(0, .seq_cst); + ev.owner.vm.event_loop.enqueueTaskConcurrent(&ev.concurrent_task); + state.current +%= 1; } else { - // Watcher will emit an event since it reads watch_state 0 - // debug.log("case 2 (run later)", .{}); + // DevServer thread has already notified once. Sending + // a second task would give ownership of both events to + // them. Instead, DevServer will steal this item since + // it can observe `watcher_events_emitted >= 2`. + ev.contention_indicator.store(0, .seq_cst); + } + } else { + ev.contention_indicator.store(0, .seq_cst); + } + + if (Environment.allow_assert) { + bun.assert(ev.contention_indicator.load(.monotonic) == 0); // always must be reset + } + } + + /// Called by DevServer after it receives a task callback. If this returns + /// another event, that event must be recycled with `recycleSecondEventFromDevServer` + fn recycleEventFromDevServer(state: *WatcherAtomics, first_event: *HotReloadEvent) ?*HotReloadEvent { + first_event.files.clearRetainingCapacity(); + first_event.timer = undefined; + + // Reset the watch count to zero, while detecting if + // the other watch event was submitted. + if (state.watcher_events_emitted.swap(0, .seq_cst) >= 2) { + // Cannot use `state.current` because it will contend with the watcher. + // Since there are are two events, one pointer comparison suffices + const other_event = if (first_event == &state.events[0].aligned) + &state.events[1].aligned + else + &state.events[0].aligned; + + switch (other_event.contention_indicator.swap(1, .seq_cst)) { + 0 => { + // DevServer holds the event now. + state.dev_server_has_event.lock(); + return other_event; + }, + 1 => { + // The watcher is currently using this event. + // `watcher_events_emitted` is already zero, so it will + // always submit. + + // Not 100% confident in this logic, but the only way + // to hit this is by saving extremely frequently, and + // a followup save will just trigger the reload. + return null; + }, + else => unreachable, } } + + // If a watch callback had already acquired the event, that is fine as + // it will now read 0 when deciding if to submit the task. + return null; + } + + fn recycleSecondEventFromDevServer(state: *WatcherAtomics, second_event: *HotReloadEvent) void { + second_event.files.clearRetainingCapacity(); + second_event.timer = undefined; + + state.dev_server_has_event.unlock(); + if (Environment.allow_assert) { + const result = second_event.contention_indicator.swap(0, .seq_cst); + bun.assert(result == 1); + } else { + second_event.contention_indicator.store(0, .seq_cst); + } } }; /// Called on watcher's thread; Access to dev-server state restricted. pub fn onFileUpdate(dev: *DevServer, events: []Watcher.Event, changed_files: []?[:0]u8, watchlist: Watcher.ItemList) void { + _ = changed_files; + debug.log("onFileUpdate start", .{}); defer debug.log("onFileUpdate end", .{}); - _ = changed_files; const slice = watchlist.slice(); const file_paths = slice.items(.file_path); const counts = slice.items(.count); const kinds = slice.items(.kind); - // TODO: audit the atomics with this reloading strategy - // It was not written by an expert. - - // Get a Hot reload task pointer - var ev: *HotReloadTask = &dev.watch_events[dev.watch_current].aligned; - if (ev.state.swap(1, .seq_cst) == 1) { - debug.log("work got stolen, must guarantee the other is free", .{}); - dev.watch_current +%= 1; - ev = &dev.watch_events[dev.watch_current].aligned; - bun.assert(ev.state.swap(1, .seq_cst) == 0); - } - defer { - // Submit the Hot reload task for bundling - if (ev.files.entries.len > 0) { - const prev_state = dev.watch_state.fetchAdd(1, .seq_cst); - ev.state.store(0, .seq_cst); - debug.log("prev_state={d}", .{prev_state}); - if (prev_state == 0) { - ev.concurrent_task = .{ .auto_delete = false, .next = null, .task = JSC.Task.init(ev) }; - dev.vm.event_loop.enqueueTaskConcurrent(&ev.concurrent_task); - dev.watch_current +%= 1; - } else { - // DevServer thread is notified. - } - } else { - ev.state.store(0, .seq_cst); - } - } + const ev = dev.watcher_atomics.watcherAcquireEvent(); + defer dev.watcher_atomics.watcherReleaseAndSubmitEvent(ev); defer dev.bun_watcher.flushEvictions(); @@ -3639,8 +4192,7 @@ pub fn onFileUpdate(dev: *DevServer, events: []Watcher.Event, changed_files: []? }, .directory => { // bust the directory cache since this directory has changed - // TODO: correctly solve https://github.com/oven-sh/bun/issues/14913 - _ = dev.server_bundler.resolver.bustDirCache(bun.strings.withoutTrailingSlash(file_path)); + _ = dev.server_bundler.resolver.bustDirCache(bun.strings.withoutTrailingSlashWindowsPath(file_path)); // if a directory watch exists for resolution // failures, check those now. @@ -3691,12 +4243,12 @@ pub fn onWatchError(_: *DevServer, err: bun.sys.Error) void { } } -pub fn publish(dev: *DevServer, topic: []const u8, message: []const u8, opcode: uws.Opcode) void { - if (dev.server) |s| _ = s.publish(topic, message, opcode, false); +pub fn publish(dev: *DevServer, topic: HmrTopic, message: []const u8, opcode: uws.Opcode) void { + if (dev.server) |s| _ = s.publish(&.{@intFromEnum(topic)}, message, opcode, false); } -pub fn numSubscribers(dev: *DevServer, topic: []const u8) u32 { - return if (dev.server) |s| s.numSubscribers(topic) else 0; +pub fn numSubscribers(dev: *DevServer, topic: HmrTopic) u32 { + return if (dev.server) |s| s.numSubscribers(&.{@intFromEnum(topic)}) else 0; } const SafeFileId = packed struct(u32) { @@ -3715,6 +4267,27 @@ pub fn getFileIdForRouter(dev: *DevServer, abs_path: []const u8, associated_rout return toOpaqueFileId(.server, index); } +pub fn onRouterSyntaxError(dev: *DevServer, rel_path: []const u8, log: FrameworkRouter.TinyLog) bun.OOM!void { + _ = dev; // TODO: maybe this should track the error, send over HmrSocket? + log.print(rel_path); +} + +pub fn onRouterCollisionError(dev: *DevServer, rel_path: []const u8, other_id: OpaqueFileId, ty: Route.FileKind) bun.OOM!void { + // TODO: maybe this should track the error, send over HmrSocket? + + Output.errGeneric("Multiple {s} matching the same route pattern is ambiguous", .{ + switch (ty) { + .page => "pages", + .layout => "layout", + }, + }); + Output.prettyErrorln(" - {s}", .{rel_path}); + Output.prettyErrorln(" - {s}", .{ + dev.relativePath(dev.server_graph.bundled_files.keys()[fromOpaqueFileId(.server, other_id).get()]), + }); + Output.flush(); +} + fn toOpaqueFileId(comptime side: bake.Side, index: IncrementalGraph(side).FileIndex) OpaqueFileId { if (Environment.allow_assert) { return OpaqueFileId.init(@bitCast(SafeFileId{ @@ -3744,7 +4317,10 @@ fn relativePath(dev: *const DevServer, path: []const u8) []const u8 { { return path[dev.root.len + 1 ..]; } - return bun.path.relative(dev.root, path); + const rel = bun.path.relative(dev.root, path); + // `rel` is owned by a mutable threadlocal buffer in the path code. + bun.path.platformToPosixInPlace(u8, @constCast(rel)); + return rel; } fn dumpStateDueToCrash(dev: *DevServer) !void { @@ -3785,13 +4361,68 @@ fn dumpStateDueToCrash(dev: *DevServer) !void { Output.note("Dumped incremental bundler graph to {}", .{bun.fmt.quote(filepath)}); } -// const RouteIndexAndRecurseFlag = packed struct(u32) { -const RouteIndexAndRecurseFlag = struct { +const RouteIndexAndRecurseFlag = packed struct(u32) { route_index: Route.Index, /// Set true for layout should_recurse_when_visiting: bool, }; +/// Bake needs to specify which graph (client/server/ssr) each entry point is. +/// File paths are always absolute paths. Files may be bundled for multiple +/// targets. +pub const EntryPointList = struct { + set: bun.StringArrayHashMapUnmanaged(Flags), + + pub const empty: EntryPointList = .{ .set = .{} }; + + const Flags = packed struct(u8) { + client: bool = false, + server: bool = false, + ssr: bool = false, + /// When this is set, also set .client = true + css: bool = false, + // /// Indicates the file might have been deleted. + // potentially_deleted: bool = false, + + unused: enum(u4) { unused = 0 } = .unused, + }; + + pub fn deinit(entry_points: *EntryPointList, allocator: std.mem.Allocator) void { + entry_points.set.deinit(allocator); + } + + pub fn appendJs( + entry_points: *EntryPointList, + allocator: std.mem.Allocator, + abs_path: []const u8, + side: bake.Graph, + ) !void { + return entry_points.append(allocator, abs_path, switch (side) { + .server => .{ .server = true }, + .client => .{ .client = true }, + .ssr => .{ .ssr = true }, + }); + } + + pub fn appendCss(entry_points: *EntryPointList, allocator: std.mem.Allocator, abs_path: []const u8) !void { + return entry_points.append(allocator, abs_path, .{ + .client = true, + .css = true, + }); + } + + /// Deduplictes requests to bundle the same file twice. + pub fn append(entry_points: *EntryPointList, allocator: std.mem.Allocator, abs_path: []const u8, flags: Flags) !void { + const gop = try entry_points.set.getOrPut(allocator, abs_path); + if (gop.found_existing) { + const T = @typeInfo(Flags).Struct.backing_integer.?; + gop.value_ptr.* = @bitCast(@as(T, @bitCast(gop.value_ptr.*)) | @as(T, @bitCast(flags))); + } else { + gop.value_ptr.* = flags; + } + } +}; + const std = @import("std"); const Allocator = std.mem.Allocator; const Mutex = std.Thread.Mutex; @@ -3813,7 +4444,6 @@ const Output = bun.Output; const Bundler = bun.bundler.Bundler; const BundleV2 = bun.bundle_v2.BundleV2; -const BakeEntryPoint = bun.bundle_v2.BakeEntryPoint; const Define = bun.options.Define; const OutputFile = bun.options.OutputFile; diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index b689202c2f06dc..b26bf3eda71ce5 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -8,6 +8,8 @@ const FrameworkRouter = @This(); /// where it is an entrypoint index. pub const OpaqueFileId = bun.GenericIndex(u32, opaque {}); +/// Absolute path to root directory of the router. +root: []const u8, types: []Type, routes: std.ArrayListUnmanaged(Route), /// Keys are full URL, with leading /, no trailing / @@ -83,6 +85,7 @@ pub const Type = struct { ignore_dirs: []const []const u8 = &.{ ".git", "node_modules" }, extensions: []const []const u8, style: Style, + allow_layouts: bool, /// `FrameworkRouter` itself does not use this value. client_file: OpaqueFileId.Optional, /// `FrameworkRouter` itself does not use this value. @@ -97,11 +100,16 @@ pub const Type = struct { pub const Index = bun.GenericIndex(u8, Type); }; -pub fn initEmpty(types: []Type, allocator: Allocator) !FrameworkRouter { +pub fn initEmpty(root: []const u8, types: []Type, allocator: Allocator) !FrameworkRouter { + bun.assert(std.fs.path.isAbsolute(root)); + var routes = try std.ArrayListUnmanaged(Route).initCapacity(allocator, types.len); errdefer routes.deinit(allocator); - for (0..types.len) |type_index| { + for (types, 0..) |*ty, type_index| { + ty.abs_root = bun.strings.withoutTrailingSlashWindowsPath(ty.abs_root); + bun.assert(bun.strings.hasPrefix(ty.abs_root, root)); + routes.appendAssumeCapacity(.{ .part = .{ .text = "" }, .type = Type.Index.init(@intCast(type_index)), @@ -115,6 +123,7 @@ pub fn initEmpty(types: []Type, allocator: Allocator) !FrameworkRouter { }); } return .{ + .root = bun.strings.withoutTrailingSlashWindowsPath(root), .types = types, .routes = routes, .dynamic_routes = .{}, @@ -389,36 +398,68 @@ pub const ParsedPattern = struct { }; }; -pub const Style = enum { - @"nextjs-pages-ui", - @"nextjs-pages-routes", - @"nextjs-app-ui", - @"nextjs-app-routes", - javascript_defined, +pub const Style = union(enum) { + nextjs_pages, + nextjs_app_ui, + nextjs_app_routes, + javascript_defined: JSC.Strong, + + pub const map = bun.ComptimeStringMap(Style, .{ + .{ "nextjs-pages", .nextjs_pages }, + .{ "nextjs-app-ui", .nextjs_app_ui }, + .{ "nextjs-app-routes", .nextjs_app_routes }, + }); + pub const error_message = "'style' must be either \"nextjs-pages\", \"nextjs-app-ui\", \"nextjs-app-routes\", or a function."; + + pub fn fromJS(value: JSValue, global: *JSC.JSGlobalObject) !Style { + if (value.isString()) { + const bun_string = try value.toBunString2(global); + var sfa = std.heap.stackFallback(4096, bun.default_allocator); + const utf8 = bun_string.toUTF8(sfa.get()); + defer utf8.deinit(); + if (map.get(utf8.slice())) |style| { + return style; + } + } else if (value.isCallable(global.vm())) { + return .{ .javascript_defined = JSC.Strong.create(value, global) }; + } + + return global.throwInvalidArguments(error_message, .{}); + } + + pub fn deinit(style: *Style) void { + switch (style.*) { + .javascript_defined => |*strong| strong.deinit(), + else => {}, + } + } pub const UiOrRoutes = enum { ui, routes }; const NextRoutingConvention = enum { app, pages }; - pub fn parse(style: Style, file_path: []const u8, ext: []const u8, log: *TinyLog, arena: Allocator) !?ParsedPattern { + pub fn parse(style: Style, file_path: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern { bun.assert(file_path[0] == '/'); return switch (style) { - .@"nextjs-pages-ui" => parseNextJsPages(file_path, ext, log, arena, .ui), - .@"nextjs-pages-routes" => parseNextJsPages(file_path, ext, log, arena, .routes), - .@"nextjs-app-ui" => parseNextJsApp(file_path, ext, log, arena, .ui), - .@"nextjs-app-routes" => parseNextJsApp(file_path, ext, log, arena, .routes), + .nextjs_pages => parseNextJsPages(file_path, ext, log, allow_layouts, arena), + .nextjs_app_ui => parseNextJsApp(file_path, ext, log, allow_layouts, arena, .ui), + .nextjs_app_routes => parseNextJsApp(file_path, ext, log, allow_layouts, arena, .routes), + + // The strategy for this should be to collect a list of candidates, + // then batch-call the javascript handler and collect all results. + // This will avoid most of the back-and-forth native<->js overhead. .javascript_defined => @panic("TODO: customizable Style"), }; } /// Implements the pages router parser from Next.js: /// https://nextjs.org/docs/getting-started/project-structure#pages-routing-conventions - pub fn parseNextJsPages(file_path_raw: []const u8, ext: []const u8, log: *TinyLog, arena: Allocator, extract: UiOrRoutes) !?ParsedPattern { + pub fn parseNextJsPages(file_path_raw: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern { var file_path = file_path_raw[0 .. file_path_raw.len - ext.len]; var kind: ParsedPattern.Kind = .page; if (strings.hasSuffixComptime(file_path, "/index")) { file_path.len -= "/index".len; - } else if (extract == .ui and strings.hasSuffixComptime(file_path, "/_layout")) { + } else if (allow_layouts and strings.hasSuffixComptime(file_path, "/_layout")) { file_path.len -= "/_layout".len; kind = .layout; } @@ -439,6 +480,7 @@ pub const Style = enum { file_path_raw: []const u8, ext: []const u8, log: *TinyLog, + allow_layouts: bool, arena: Allocator, comptime extract: UiOrRoutes, ) !?ParsedPattern { @@ -468,6 +510,8 @@ pub const Style = enum { }).get(basename) orelse return null; + if (kind == .layout and !allow_layouts) return null; + const dirname = bun.path.dirname(without_ext, .posix); if (dirname.len <= 1) return .{ .kind = kind, @@ -769,6 +813,7 @@ fn newEdge(fr: *FrameworkRouter, alloc: Allocator, edge_data: Route.Edge) !Route const PatternParseError = error{InvalidRoutePattern}; /// Non-allocating single message log, specialized for the messages from the route pattern parsers. +/// DevServer uses this to special-case the printing of these messages to highlight the offending part of the filename pub const TinyLog = struct { msg: std.BoundedArray(u8, 512 + std.fs.max_path_bytes), cursor_at: u32, @@ -777,14 +822,47 @@ pub const TinyLog = struct { pub const empty: TinyLog = .{ .cursor_at = std.math.maxInt(u32), .cursor_len = 0, .msg = .{} }; pub fn fail(log: *TinyLog, comptime fmt: []const u8, args: anytype, cursor_at: usize, cursor_len: usize) PatternParseError { + log.write(fmt, args); + log.cursor_at = @intCast(cursor_at); + log.cursor_len = @intCast(cursor_len); + return PatternParseError.InvalidRoutePattern; + } + + pub fn write(log: *TinyLog, comptime fmt: []const u8, args: anytype) void { log.msg.len = @intCast(if (std.fmt.bufPrint(&log.msg.buffer, fmt, args)) |slice| slice.len else |_| brk: { // truncation should never happen because the buffer is HUGE. handle it anyways @memcpy(log.msg.buffer[log.msg.buffer.len - 3 ..], "..."); break :brk log.msg.buffer.len; }); - log.cursor_at = @intCast(cursor_at); - log.cursor_len = @intCast(cursor_len); - return PatternParseError.InvalidRoutePattern; + } + + pub fn print(log: *const TinyLog, rel_path: []const u8) void { + const after = rel_path[@max(0, log.cursor_at)..]; + bun.Output.errGeneric("\"{s}{s}{s}\" is not a valid route", .{ + rel_path[0..@max(0, log.cursor_at)], + after[0..@min(log.cursor_len, after.len)], + after[@min(log.cursor_len, after.len)..], + }); + const w = bun.Output.errorWriterBuffered(); + w.writeByteNTimes(' ', "error: \"".len + log.cursor_at) catch return; + if (bun.Output.enable_ansi_colors_stderr) { + const symbols = bun.fmt.TableSymbols.unicode; + bun.Output.prettyError("" ++ symbols.topColumnSep(), .{}); + if (log.cursor_len > 1) { + w.writeBytesNTimes(symbols.horizontalEdge(), log.cursor_len - 1) catch return; + } + } else { + if (log.cursor_len <= 1) { + w.writeAll("|") catch return; + } else { + w.writeByteNTimes('-', log.cursor_len - 1) catch return; + } + } + w.writeByte('\n') catch return; + w.writeByteNTimes(' ', "error: \"".len + log.cursor_at) catch return; + w.writeAll(log.msg.slice()) catch return; + bun.Output.prettyError("\n", .{}); + bun.Output.flush(); } }; @@ -794,6 +872,8 @@ pub const InsertionContext = struct { vtable: *const VTable, const VTable = struct { getFileIdForRouter: *const fn (*anyopaque, abs_path: []const u8, associated_route: Route.Index, kind: Route.FileKind) bun.OOM!OpaqueFileId, + onRouterSyntaxError: *const fn (*anyopaque, rel_path: []const u8, fail: TinyLog) bun.OOM!void, + onRouterCollisionError: *const fn (*anyopaque, rel_path: []const u8, other_id: OpaqueFileId, file_kind: Route.FileKind) bun.OOM!void, }; pub fn wrap(comptime T: type, ctx: *T) InsertionContext { const wrapper = struct { @@ -801,11 +881,23 @@ pub const InsertionContext = struct { const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx)); return try cast_ctx.getFileIdForRouter(abs_path, associated_route, kind); } + fn onRouterSyntaxError(opaque_ctx: *anyopaque, rel_path: []const u8, log: TinyLog) bun.OOM!void { + const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx)); + if (!@hasDecl(T, "onRouterSyntaxError")) @panic("TODO: onRouterSyntaxError for " ++ @typeName(T)); + return try cast_ctx.onRouterSyntaxError(rel_path, log); + } + fn onRouterCollisionError(opaque_ctx: *anyopaque, rel_path: []const u8, other_id: OpaqueFileId, file_kind: Route.FileKind) bun.OOM!void { + const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx)); + if (!@hasDecl(T, "onRouterCollisionError")) @panic("TODO: onRouterCollisionError for " ++ @typeName(T)); + return try cast_ctx.onRouterCollisionError(rel_path, other_id, file_kind); + } }; return .{ .opaque_ctx = ctx, .vtable = comptime &.{ .getFileIdForRouter = &wrapper.getFileIdForRouter, + .onRouterSyntaxError = &wrapper.onRouterSyntaxError, + .onRouterCollisionError = &wrapper.onRouterCollisionError, }, }; } @@ -817,12 +909,12 @@ pub fn scan( ty: Type.Index, r: *Resolver, ctx: InsertionContext, -) !void { +) bun.OOM!void { const t = &fw.types[ty.get()]; bun.assert(!strings.hasSuffixComptime(t.abs_root, "/")); bun.assert(std.fs.path.isAbsolute(t.abs_root)); - const root_info = try r.readDirInfo(t.abs_root) orelse - return error.RootDirMissing; + const root_info = r.readDirInfoIgnoreError(t.abs_root) orelse + return; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); try fw.scanInner(alloc, t, ty, r, root_info, &arena_state, ctx); @@ -837,7 +929,7 @@ fn scanInner( dir_info: *const DirInfo, arena_state: *std.heap.ArenaAllocator, ctx: InsertionContext, -) !void { +) bun.OOM!void { const fs = r.fs; const fs_impl = &fs.fs; @@ -871,19 +963,29 @@ fn scanInner( } var rel_path_buf: bun.PathBuffer = undefined; - var rel_path = bun.path.relativeNormalizedBuf( + var full_rel_path = bun.path.relativeNormalizedBuf( rel_path_buf[1..], - t.abs_root, + fr.root, fs.abs(&.{ file.dir, file.base() }), - .posix, + .auto, true, ); rel_path_buf[0] = '/'; - rel_path = rel_path_buf[0 .. rel_path.len + 1]; + bun.path.platformToPosixInPlace(u8, rel_path_buf[0..full_rel_path.len]); + const rel_path = if (t.abs_root.len == fr.root.len) + rel_path_buf[0 .. full_rel_path.len + 1] + else + full_rel_path[t.abs_root.len - fr.root.len - 1 ..]; var log = TinyLog.empty; defer _ = arena_state.reset(.retain_capacity); - const parsed = (t.style.parse(rel_path, ext, &log, arena_state.allocator()) catch - @panic("TODO: propagate error message")) orelse continue :outer; + const parsed = (t.style.parse(rel_path, ext, &log, t.allow_layouts, arena_state.allocator()) catch { + log.cursor_at += @intCast(t.abs_root.len - fr.root.len); + try ctx.vtable.onRouterSyntaxError(ctx.opaque_ctx, full_rel_path, log); + continue :outer; + }) orelse continue :outer; + + if (parsed.kind == .page and t.ignore_underscores and bun.strings.hasPrefixComptime(base, "_")) + continue :outer; var static_total_len: usize = 0; var param_count: usize = 0; @@ -901,11 +1003,18 @@ fn scanInner( } if (param_count > 64) { - @panic("TODO: propagate error for more than 64 params"); + log.write("Pattern cannot have more than 64 param", .{}); + try ctx.vtable.onRouterSyntaxError(ctx.opaque_ctx, full_rel_path, log); + continue :outer; } - if (parsed.kind == .page and t.ignore_underscores and bun.strings.hasPrefixComptime(base, "_")) - continue :outer; + var out_colliding_file_id: OpaqueFileId = undefined; + + const file_kind: Route.FileKind = switch (parsed.kind) { + .page => .page, + .layout => .layout, + .extra => @panic("TODO: associate extra files with route"), + }; const result = switch (param_count > 0) { inline else => |has_dynamic_comptime| result: { @@ -926,18 +1035,13 @@ fn scanInner( bun.assert(s.getWritten().len == allocation.len); break :static_route StaticPattern{ .route_path = allocation }; }; - var out_colliding_file_id: OpaqueFileId = undefined; break :result fr.insert( alloc, t_index, if (has_dynamic_comptime) .dynamic else .static, pattern, - switch (parsed.kind) { - .page => .page, - .layout => .layout, - .extra => @panic("TODO: extra files"), - }, + file_kind, fs.abs(&.{ file.dir, file.base() }), ctx, &out_colliding_file_id, @@ -945,12 +1049,20 @@ fn scanInner( }, }; - result catch @panic("TODO: propagate error message"); + result catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.RouteCollision => { + try ctx.vtable.onRouterCollisionError( + ctx.opaque_ctx, + full_rel_path, + out_colliding_file_id, + file_kind, + ); + }, + }; }, } } - - // } } @@ -963,6 +1075,11 @@ pub const JSFrameworkRouter = struct { files: std.ArrayListUnmanaged(bun.String), router: FrameworkRouter, + stored_parse_errors: std.ArrayListUnmanaged(struct { + // Owned by bun.default_allocator + rel_path: []const u8, + log: TinyLog, + }), const validators = bun.JSC.Node.validators; @@ -982,13 +1099,8 @@ pub const JSFrameworkRouter = struct { return global.throwInvalidArguments("Missing options.root", .{}); defer root.deinit(); - const style = try validators.validateStringEnum( - Style, - global, - try opts.getOptional(global, "style", JSValue) orelse .undefined, - "style", - .{}, - ); + var style = try Style.fromJS(try opts.getOptional(global, "style", JSValue) orelse .undefined, global); + errdefer style.deinit(); const abs_root = try bun.default_allocator.dupe(u8, bun.strings.withoutTrailingSlash( bun.path.joinAbs(bun.fs.FileSystem.instance.top_level_dir, .auto, root.slice()), @@ -1000,6 +1112,7 @@ pub const JSFrameworkRouter = struct { .ignore_underscores = false, .extensions = &.{ ".tsx", ".ts", ".jsx", ".js" }, .style = style, + .allow_layouts = true, // Unused by JSFrameworkRouter .client_file = undefined, .server_file = undefined, @@ -1008,18 +1121,34 @@ pub const JSFrameworkRouter = struct { errdefer bun.default_allocator.free(types); const jsfr = bun.new(JSFrameworkRouter, .{ - .router = try FrameworkRouter.initEmpty(types, bun.default_allocator), + .router = try FrameworkRouter.initEmpty(abs_root, types, bun.default_allocator), .files = .{}, + .stored_parse_errors = .{}, }); - jsfr.router.scan( + try jsfr.router.scan( bun.default_allocator, Type.Index.init(0), &global.bunVM().bundler.resolver, InsertionContext.wrap(JSFrameworkRouter, jsfr), - ) catch |err| { - return global.throwError(err, "while scanning route list"); - }; + ); + if (jsfr.stored_parse_errors.items.len > 0) { + const arr = JSValue.createEmptyArray(global, jsfr.stored_parse_errors.items.len); + for (jsfr.stored_parse_errors.items, 0..) |*item, i| { + arr.putIndex( + global, + @intCast(i), + global.createErrorInstance("Invalid route {}: {s}", .{ + bun.fmt.quote(item.rel_path), + item.log.msg.slice(), + }), + ); + } + return global.throwValue2(global.createAggregateErrorWithArray( + bun.String.static("Errors scanning routes"), + arr, + )); + } return jsfr; } @@ -1104,6 +1233,8 @@ pub const JSFrameworkRouter = struct { this.files.deinit(bun.default_allocator); this.router.deinit(bun.default_allocator); bun.default_allocator.free(this.router.types); + for (this.stored_parse_errors.items) |i| bun.default_allocator.free(i.rel_path); + this.stored_parse_errors.deinit(bun.default_allocator); bun.destroy(this); } @@ -1118,14 +1249,11 @@ pub const JSFrameworkRouter = struct { const style_js, const filepath_js = frame.argumentsAsArray(2); const filepath = try filepath_js.toSlice2(global, alloc); defer filepath.deinit(); - const style_string = try style_js.toSlice2(global, alloc); - defer style_string.deinit(); - - const style = std.meta.stringToEnum(Style, style_string.slice()) orelse - return global.throwInvalidArguments("unknown router style {}", .{bun.fmt.quote(style_string.slice())}); + var style = try Style.fromJS(style_js, global); + errdefer style.deinit(); var log = TinyLog.empty; - const parsed = style.parse(filepath.slice(), std.fs.path.extension(filepath.slice()), &log, alloc) catch |err| switch (err) { + const parsed = style.parse(filepath.slice(), std.fs.path.extension(filepath.slice()), &log, true, alloc) catch |err| switch (err) { error.InvalidRoutePattern => { global.throw("{s} ({d}:{d})", .{ log.msg.slice(), log.cursor_at, log.cursor_len }); return error.JSError; @@ -1166,6 +1294,15 @@ pub const JSFrameworkRouter = struct { return OpaqueFileId.init(@intCast(jsfr.files.items.len - 1)); } + pub fn onRouterSyntaxError(jsfr: *JSFrameworkRouter, rel_path: []const u8, log: TinyLog) !void { + const rel_path_dupe = try bun.default_allocator.dupe(u8, rel_path); + errdefer bun.default_allocator.free(rel_path_dupe); + try jsfr.stored_parse_errors.append(bun.default_allocator, .{ + .rel_path = rel_path_dupe, + .log = log, + }); + } + pub fn fileIdToJS(jsfr: *JSFrameworkRouter, global: *JSGlobalObject, id: OpaqueFileId.Optional) JSValue { return jsfr.files.items[(id.unwrap() orelse return .null).get()].toJS(global); } diff --git a/src/bake/bake.d.ts b/src/bake/bake.d.ts index 17212629ba1151..3262229705a0c0 100644 --- a/src/bake/bake.d.ts +++ b/src/bake/bake.d.ts @@ -255,6 +255,7 @@ declare module "bun" { * Do not traverse into directories and files that start with an `_`. Do * not index pages that start with an `_`. Does not prevent stuff like * `_layout.tsx` from being recognized. + * @default false */ ignoreUnderscores?: boolean; /** @@ -264,8 +265,9 @@ declare module "bun" { /** * Extensions to match on. * '*' - any extension + * @default (set of all valid JavaScript/TypeScript extensions) */ - extensions: string[] | "*"; + extensions?: string[] | "*"; /** * 'nextjs-app' builds routes out of directories with `page.tsx` and `layout.tsx` * 'nextjs-pages' builds routes out of any `.tsx` file and layouts with `_layout.tsx`. @@ -421,15 +423,7 @@ declare module "bun" { } interface ClientEntryPoint { - /** - * Called when server-side code is changed. This can be used to fetch a - * non-html version of the updated page to perform a faster reload. If - * this function does not exist or throws, the client will perform a - * hard reload. - * - * Tree-shaken away in production builds. - */ - onServerSideReload?: () => Promise | void; + // No exports } /** @@ -459,7 +453,7 @@ declare module "bun" { /** * A list of js files that the route will need to be interactive. */ - readonly scripts: ReadonlyArray; + readonly modules: ReadonlyArray; /** * A list of js files that should be preloaded. * @@ -547,9 +541,11 @@ declare module "bun:bake/server" { declare module "bun:bake/client" { /** - * Due to the current implementation of the Dev Server, it must be informed of - * client-side routing so it can load client components. This is not necessary - * in production, and calling this in that situation will fail to compile. + * Callback is invoked when server-side code is changed. This can be used to + * fetch a non-html version of the updated page to perform a faster reload. If + * not provided, the client will perform a hard reload. + * + * Only one callback can be set. This function overwrites the previous one. */ - declare function bundleRouteForDevelopment(href: string, options?: { signal?: AbortSignal }): Promise; + export function onServerSideReload(cb: () => void | Promise): Promise; } diff --git a/src/bake/bake.private.d.ts b/src/bake/bake.private.d.ts index 9497ab63ecc4ac..204a68f151349d 100644 --- a/src/bake/bake.private.d.ts +++ b/src/bake/bake.private.d.ts @@ -47,11 +47,11 @@ declare var __bun_f: any; // The following interfaces have been transcribed manually. -declare module "react-server-dom-webpack/client.browser" { +declare module "react-server-dom-bun/client.browser" { export function createFromReadableStream(readable: ReadableStream): Promise; } -declare module "react-server-dom-webpack/client.node.unbundled.js" { +declare module "react-server-dom-bun/client.node.unbundled.js" { import type { ReactClientManifest } from "bun:bake/server"; import type { Readable } from "node:stream"; export interface Manifest { @@ -70,7 +70,7 @@ declare module "react-server-dom-webpack/client.node.unbundled.js" { export function createFromNodeStream(readable: Readable, manifest?: Manifest): Promise; } -declare module "react-server-dom-webpack/server.node.unbundled.js" { +declare module "react-server-dom-bun/server.node.unbundled.js" { import type { ReactServerManifest } from "bun:bake/server"; import type { ReactElement, ReactElement } from "react"; import type { Writable } from "node:stream"; @@ -98,7 +98,7 @@ declare module "react-server-dom-webpack/server.node.unbundled.js" { } declare module "react-dom/server.node" { - import type { PipeableStream } from "react-server-dom-webpack/server.node.unbundled.js"; + import type { PipeableStream } from "react-server-dom-bun/server.node.unbundled.js"; import type { ReactElement } from "react"; export type RenderToPipeableStreamOptions = any; diff --git a/src/bake/bake.zig b/src/bake/bake.zig index d21fe1f6340ddd..f0408c5fcfb4d7 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -12,15 +12,16 @@ pub const api_name = "app"; /// Zig version of the TS definition 'Bake.Options' in 'bake.d.ts' pub const UserOptions = struct { - arena: std.heap.ArenaAllocator.State, + arena: std.heap.ArenaAllocator, allocations: StringRefList, root: []const u8, framework: Framework, bundler_options: SplitBundlerOptions, + // bundler_plugin: ?*Plugin, pub fn deinit(options: *UserOptions) void { - options.arena.promote(bun.default_allocator).deinit(); + options.arena.deinit(); options.allocations.free(); } @@ -60,7 +61,7 @@ pub const UserOptions = struct { }; return .{ - .arena = arena.state, + .arena = arena, .allocations = allocations, .root = root, .framework = framework, @@ -99,31 +100,13 @@ const BuildConfigSubset = struct { conditions: bun.StringArrayHashMapUnmanaged(void) = .{}, drop: bun.StringArrayHashMapUnmanaged(void) = .{}, // TODO: plugins -}; -/// Temporary function to invoke dev server via JavaScript. Will be -/// replaced with a user-facing API. Refs the event loop forever. -pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - _ = global; - _ = callframe; - - if (!bun.FeatureFlags.bake) return .undefined; - - bun.Output.errGeneric( - \\This api has moved to the `app` property of the default export. - \\ - \\ export default {{ - \\ port: 3000, - \\ app: {{ - \\ framework: 'react' - \\ }}, - \\ }}; - \\ - , - .{}, - ); - return .undefined; -} + pub fn loadFromJs(config: *BuildConfigSubset, value: JSValue, arena: Allocator) !void { + _ = config; // autofix + _ = value; // autofix + _ = arena; // autofix + } +}; /// A "Framework" in our eyes is simply set of bundler options that a framework /// author would set in order to integrate the framework with the application. @@ -132,6 +115,7 @@ pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bu /// /// Full documentation on these fields is located in the TypeScript definitions. pub const Framework = struct { + is_built_in_react: bool, file_system_router_types: []FileSystemRouterType, // static_routers: [][]const u8, server_components: ?ServerComponents = null, @@ -141,9 +125,10 @@ pub const Framework = struct { /// Bun provides built-in support for using React as a framework. /// Depends on externally provided React /// - /// $ bun i react@experimental react-dom@experimental react-server-dom-webpack@experimental react-refresh@experimental + /// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun pub fn react(arena: std.mem.Allocator) !Framework { return .{ + .is_built_in_react = true, .server_components = .{ .separate_ssr_graph = true, .server_runtime_import = "react-server-dom-bun/server", @@ -158,7 +143,8 @@ pub const Framework = struct { .ignore_underscores = true, .ignore_dirs = &.{ "node_modules", ".git" }, .extensions = &.{ ".tsx", ".jsx" }, - .style = .@"nextjs-pages-ui", + .style = .nextjs_pages, + .allow_layouts = true, }, }), // .static_routers = try arena.dupe([]const u8, &.{"public"}), @@ -188,6 +174,7 @@ pub const Framework = struct { ignore_dirs: []const []const u8, extensions: []const []const u8, style: FrameworkRouter.Style, + allow_layouts: bool, }; const BuiltInModule = union(enum) { @@ -208,11 +195,22 @@ pub const Framework = struct { import_source: []const u8 = "react-refresh/runtime", }; - /// Given a Framework configuration, this returns another one with all modules resolved. + pub const react_install_command = "bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun"; + + pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void { + try log.addMsg(.{ + .kind = .note, + .data = try bun.logger.rangeData(null, bun.logger.Range.none, "Install the built in react integration with \"" ++ react_install_command ++ "\"") + .cloneLineText(log.clone_line_text, log.msgs.allocator), + }); + } + + /// Given a Framework configuration, this returns another one with all paths resolved. + /// New memory allocated into provided arena. /// /// All resolution errors will happen before returning error.ModuleNotFound - /// Details written into `r.log` - pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver) !Framework { + /// Errors written into `r.log` + pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver, arena: Allocator) !Framework { var clone = f; var had_errors: bool = false; @@ -226,8 +224,7 @@ pub const Framework = struct { } for (clone.file_system_router_types) |*fsr| { - // TODO: unonwned memory - fsr.root = bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root); + fsr.root = try arena.dupe(u8, bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root)); if (fsr.entry_client) |*entry_client| f.resolveHelper(client, entry_client, &had_errors, "client side entrypoint"); f.resolveHelper(client, &fsr.entry_server, &had_errors, "server side entrypoint"); } @@ -384,6 +381,7 @@ pub const Framework = struct { var it = array.arrayIterator(global); var i: usize = 0; + errdefer for (file_system_router_types[0..i]) |*fsr| fsr.style.deinit(); while (it.next()) |fsr_opts| : (i += 1) { const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse { return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i}); @@ -394,14 +392,12 @@ pub const Framework = struct { const client_entry_point = try getOptionalString(fsr_opts, global, "clientEntryPoint", refs, arena); const prefix = try getOptionalString(fsr_opts, global, "prefix", refs, arena) orelse "/"; const ignore_underscores = try fsr_opts.getBooleanStrict(global, "ignoreUnderscores") orelse false; + const layouts = try fsr_opts.getBooleanStrict(global, "layouts") orelse false; - const style = try validators.validateStringEnum( - FrameworkRouter.Style, - global, - try opts.getOptional(global, "style", JSValue) orelse .undefined, - "style", - .{}, - ); + var style = try FrameworkRouter.Style.fromJS(try fsr_opts.get(global, "style") orelse { + return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'style'", .{i}); + }, global); + errdefer style.deinit(); const extensions: []const []const u8 = if (try fsr_opts.get(global, "extensions")) |exts_js| exts: { if (exts_js.isString()) { @@ -415,8 +411,18 @@ pub const Framework = struct { var i_2: usize = 0; const extensions = try arena.alloc([]const u8, len); while (it_2.next()) |array_item| : (i_2 += 1) { - // TODO: remove/add the prefix `.`, throw error if specifying '*' as an array item instead of as root - extensions[i_2] = refs.track(try array_item.toSlice2(global, arena)); + const slice = refs.track(try array_item.toSlice2(global, arena)); + if (bun.strings.eqlComptime(slice, "*")) + return global.throwInvalidArguments("'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array.", .{}); + + if (slice.len == 0) { + return global.throwInvalidArguments("'extensions' cannot include \"\" as an extension.", .{}); + } + + extensions[i_2] = if (slice[0] == '.') + slice + else + try std.mem.concat(arena, u8, &.{ ".", slice }); } break :exts extensions; } @@ -447,13 +453,16 @@ pub const Framework = struct { .ignore_underscores = ignore_underscores, .extensions = extensions, .ignore_dirs = ignore_dirs, + .allow_layouts = layouts, }; } break :brk file_system_router_types; }; + errdefer for (file_system_router_types) |*fsr| fsr.style.deinit(); const framework: Framework = .{ + .is_built_in_react = false, .file_system_router_types = file_system_router_types, .react_fast_refresh = react_fast_refresh, .server_components = server_components, @@ -517,9 +526,9 @@ pub const Framework = struct { out.options.production = mode != .development; out.options.tree_shaking = mode != .development; - out.options.minify_syntax = true; // required for DCE - // out.options.minify_identifiers = mode != .development; - // out.options.minify_whitespace = mode != .development; + out.options.minify_syntax = mode != .development; + out.options.minify_identifiers = mode != .development; + out.options.minify_whitespace = mode != .development; out.options.experimental_css = true; out.options.css_chunking = true; @@ -560,11 +569,6 @@ fn getOptionalString( return allocations.track(str.toUTF8(arena)); } -export fn Bun__getTemporaryDevServer(global: *JSC.JSGlobalObject) JSValue { - if (!bun.FeatureFlags.bake) return .undefined; - return JSC.JSFunction.create(global, "wipDevServer", jsWipDevServer, 0, .{}); -} - pub inline fn getHmrRuntime(side: Side) [:0]const u8 { return if (Environment.codegen_embed) switch (side) { @@ -586,6 +590,13 @@ pub const Mode = enum { pub const Side = enum(u1) { client, server, + + pub fn graph(s: Side) Graph { + return switch (s) { + .client => .client, + .server => .server, + }; + } }; pub const Graph = enum(u2) { client, @@ -670,6 +681,7 @@ pub const PatternBuffer = struct { pub fn prependPart(pb: *PatternBuffer, part: FrameworkRouter.Part) void { switch (part) { .text => |text| { + bun.assert(text.len == 0 or text[0] != '/'); pb.prepend(text); pb.prepend("/"); }, @@ -686,13 +698,28 @@ pub const PatternBuffer = struct { } }; +pub fn printWarning() void { + // Silence this for the test suite + if (bun.getenvZ("BUN_DEV_SERVER_TEST_RUNNER") == null) { + bun.Output.warn( + \\Be advised that Bun Bake is highly experimental, and its API + \\will have breaking changes. Join the #bake Discord + \\channel to help us find bugs: https://bun.sh/discord + \\ + \\ + , .{}); + bun.Output.flush(); + } +} + const std = @import("std"); const Allocator = std.mem.Allocator; const bun = @import("root").bun; const Environment = bun.Environment; -const ZigString = bun.JSC.ZigString; const JSC = bun.JSC; const JSValue = JSC.JSValue; const validators = bun.JSC.Node.validators; +const ZigString = JSC.ZigString; +const Plugin = JSC.API.JSBundler.Plugin; diff --git a/src/bake/bun-framework-react/client.tsx b/src/bake/bun-framework-react/client.tsx index c0dc76ddcc8e8c..9353da0f8e7662 100644 --- a/src/bake/bun-framework-react/client.tsx +++ b/src/bake/bun-framework-react/client.tsx @@ -5,15 +5,30 @@ import * as React from "react"; import { hydrateRoot } from "react-dom/client"; import { createFromReadableStream } from "react-server-dom-bun/client.browser"; -import { bundleRouteForDevelopment } from "bun:bake/client"; +import { onServerSideReload } from 'bun:bake/client'; +import { flushSync } from 'react-dom'; -let encoder = new TextEncoder(); -let promise = createFromReadableStream( +const te = new TextEncoder(); +const td = new TextDecoder(); + +// It is the framework's responsibility to ensure that client-side navigation +// loads CSS files. The implementation here loads all CSS files as tags, +// and uses the ".disabled" property to enable/disable them. +const cssFiles = new Map | null; link: HTMLLinkElement }>(); +let currentCssList: string[] | undefined = undefined; + +// The initial RSC payload is put into inline