From 2d2e329ee3d6a05740aa8bb8adfab72f7a4cecec Mon Sep 17 00:00:00 2001 From: imide Date: Sun, 24 Nov 2024 16:53:39 -0700 Subject: [PATCH 01/92] Update installation.md (#15392) --- docs/installation.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index f52d4d5f5a..29ed0bab56 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -73,8 +73,7 @@ There are also image variants for different operating systems. $ docker pull oven/bun:debian $ docker pull oven/bun:slim $ docker pull oven/bun:distroless -# alpine not recommended until #918 is fixed -# $ docker pull oven/bun:alpine +$ docker pull oven/bun:alpine ``` ## Checking installation From f4a0fe40aacb1b457c293923105e8ecb616fb4ad Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 24 Nov 2024 22:03:54 -0800 Subject: [PATCH 02/92] Fixes #8683 (#15389) --- src/node-fallbacks/buffer.js | 32 ++++++++++++++++++++++++++-- test/bundler/bundler_browser.test.ts | 29 +++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/node-fallbacks/buffer.js b/src/node-fallbacks/buffer.js index 40febf6828..3de63bc9e9 100644 --- a/src/node-fallbacks/buffer.js +++ b/src/node-fallbacks/buffer.js @@ -3,5 +3,33 @@ * * Imported on usage in `bun build --target=browser` */ -export * from "buffer"; -export { Buffer as default } from "buffer"; +export * from "./node_modules/buffer"; +export { Buffer as default } from "./node_modules/buffer"; +export { Buffer } from "./node_modules/buffer"; +export var kStringMaxLength = 2 ** 32 - 1; +export var { Blob, File, atob, btoa } = globalThis; +export var { createObjectURL } = URL; +export var isAscii = buf => { + if (ArrayBuffer.isView(buf)) { + return buf.every(byte => byte < 128); + } else { + return buf.split("").every(char => char.charCodeAt(0) < 128); + } +}; +export var isUtf8 = buf => { + throw new Error("Not implemented"); +}; +export var constants = { + __proto__: null, + MAX_LENGTH: kStringMaxLength, + MAX_STRING_LENGTH: kStringMaxLength, + BYTES_PER_ELEMENT: 1, +}; + +export function resolveObjectURL(url) { + throw new Error("Not implemented"); +} + +export function transcode(buf, from, to) { + throw new Error("Not implemented"); +} diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts index 49bb5f6853..d437447d7f 100644 --- a/test/bundler/bundler_browser.test.ts +++ b/test/bundler/bundler_browser.test.ts @@ -40,6 +40,31 @@ describe("bundler", () => { "vm": "no-op", "zlib": "polyfill", }; + + itBundled("browser/NodeBuffer#12272", { + files: { + "/entry.js": /* js */ ` + import * as buffer from "node:buffer"; + import { Buffer } from "buffer"; + import Buffer2 from "buffer"; + import { Blob, File } from "buffer"; + if (Buffer !== Buffer2) throw new Error("Buffer is not the same"); + if (Blob !== globalThis.Blob) throw new Error("Blob is not the same"); + if (File !== globalThis.File) throw new Error("File is not the same"); + if (Buffer.from("foo").toString("hex") !== "666f6f") throw new Error("Buffer.from is broken"); + if (buffer.isAscii("foo") !== true) throw new Error("Buffer.isAscii is broken"); + if (Buffer2.alloc(10, 'b').toString("hex") !== "62626262626262626262") throw new Error("Buffer.alloc is broken"); + console.log("Success!"); + `, + }, + target: "browser", + run: { + stdout: "Success!", + }, + onAfterBundle(api) { + api.expectFile("out.js").not.toInclude("import "); + }, + }); itBundled("browser/NodeFS", { files: { "/entry.js": /* js */ ` @@ -56,7 +81,7 @@ describe("bundler", () => { stdout: "function\nfunction\nundefined", }, onAfterBundle(api) { - api.expectFile('out.js').not.toInclude('import '); + api.expectFile("out.js").not.toInclude("import "); }, }); itBundled("browser/NodeTTY", { @@ -73,7 +98,7 @@ describe("bundler", () => { stdout: "function\nfunction\nfalse", }, onAfterBundle(api) { - api.expectFile('out.js').not.toInclude('import '); + api.expectFile("out.js").not.toInclude("import "); }, }); // TODO: use nodePolyfillList to generate the code in here. From c5cd0e4575aa5b55adfb4966bd52f1b0461e1a82 Mon Sep 17 00:00:00 2001 From: Lua MacDougall Date: Sun, 24 Nov 2024 22:04:54 -0800 Subject: [PATCH 03/92] Bun.serve incorrect file for error page template (#15397) --- src/runtime.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime.zig b/src/runtime.zig index 6d6ae1d39e..c7309a280e 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -71,9 +71,9 @@ pub const Fallback = struct { pub inline fn errorJS() string { return if (Environment.codegen_embed) - @embedFile("bun-error/bun-error.css") + @embedFile("bun-error/index.js") else - bun.runtimeEmbedFile(.codegen, "bun-error/bun-error.css"); + bun.runtimeEmbedFile(.codegen, "bun-error/index.js"); } pub inline fn errorCSS() string { From 898feb886f9090e08fe31983baf1169f98d4cbb5 Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Sun, 24 Nov 2024 23:37:18 -0700 Subject: [PATCH 04/92] ci: Temporarily run zig build on ephemeral agents --- .buildkite/ci.mjs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 31144f19ed..8d910265e0 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -304,15 +304,21 @@ function getPipeline(options) { * @param {Target} target * @returns {Agent} */ - const getZigAgent = target => { - const { abi, arch } = target; - // if (abi === "musl") { - // const instanceType = arch === "aarch64" ? "c8g.large" : "c7i.large"; - // return getEmphemeralAgent("v2", target, instanceType); - // } - return { - queue: "build-zig", + const getZigAgent = platform => { + const { arch } = platform; + const instanceType = arch === "aarch64" ? "c8g.large" : "c7i.large"; + const zigPlatform = { + os: "linux", + arch, + abi: "musl", + distro: "alpine", + distroVersion: "3.20" }; + return getEmphemeralAgent("v2", zigPlatform, instanceType); + // TODO: Temporarily disable due to configuration + // return { + // queue: "build-zig", + // }; }; /** @@ -443,7 +449,7 @@ function getPipeline(options) { label: `${getTargetLabel(platform)} - build-zig`, depends_on: getDependsOn(platform), agents: getZigAgent(platform), - retry: getRetry(1), // FIXME: Sometimes zig build hangs, so we need to retry once + retry: getRetry(), cancel_on_build_failing: isMergeQueue(), env: getBuildEnv(platform), command: `bun run build:ci --target bun-zig --toolchain ${toolchain}`, From a468d090645e800200f2c1c4ca907dec578c9b13 Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Sun, 24 Nov 2024 23:38:59 -0700 Subject: [PATCH 05/92] ci: Fix typo --- .buildkite/ci.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 8d910265e0..e38f544e67 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -312,7 +312,7 @@ function getPipeline(options) { arch, abi: "musl", distro: "alpine", - distroVersion: "3.20" + release: "3.20" }; return getEmphemeralAgent("v2", zigPlatform, instanceType); // TODO: Temporarily disable due to configuration From f61f03fae3189b618d18f8a4082570e4930842eb Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Sun, 24 Nov 2024 23:07:08 -0800 Subject: [PATCH 06/92] cmake: Fix cross-compiling zig on alpine (#15400) Co-authored-by: Electroid --- .buildkite/ci.mjs | 12 +++++++----- cmake/Globals.cmake | 10 ---------- cmake/Options.cmake | 10 ++++++++++ cmake/targets/BuildBun.cmake | 2 +- cmake/tools/SetupLLVM.cmake | 2 +- cmake/tools/SetupWebKit.cmake | 2 +- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index e38f544e67..733e95d843 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -307,14 +307,16 @@ function getPipeline(options) { const getZigAgent = platform => { const { arch } = platform; const instanceType = arch === "aarch64" ? "c8g.large" : "c7i.large"; - const zigPlatform = { + return { + robobun: true, + robobun2: true, os: "linux", arch, - abi: "musl", - distro: "alpine", - release: "3.20" + distro: "debian", + release: "11", + "image-name": `linux-${arch}-debian-11-v5`, // v5 is not on main yet + "instance-type": instanceType, }; - return getEmphemeralAgent("v2", zigPlatform, instanceType); // TODO: Temporarily disable due to configuration // return { // queue: "build-zig", diff --git a/cmake/Globals.cmake b/cmake/Globals.cmake index 106e1285ea..3066bb2033 100644 --- a/cmake/Globals.cmake +++ b/cmake/Globals.cmake @@ -136,16 +136,6 @@ else() set(WARNING WARNING) endif() -if(LINUX) - if(EXISTS "/etc/alpine-release") - set(DEFAULT_ABI "musl") - else() - set(DEFAULT_ABI "gnu") - endif() - - optionx(ABI "musl|gnu" "The ABI to use (e.g. musl, gnu)" DEFAULT ${DEFAULT_ABI}) -endif() - # TODO: This causes flaky zig builds in CI, so temporarily disable it. # if(CI) # set(DEFAULT_VENDOR_PATH ${CACHE_PATH}/vendor) diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 89cdaef0e8..d6cc8582ea 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -48,6 +48,16 @@ else() message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}") endif() +if(LINUX) + if(EXISTS "/etc/alpine-release") + set(DEFAULT_ABI "musl") + else() + set(DEFAULT_ABI "gnu") + endif() + + optionx(ABI "musl|gnu" "The ABI to use (e.g. musl, gnu)" DEFAULT ${DEFAULT_ABI}) +endif() + if(ARCH STREQUAL "x64") optionx(ENABLE_BASELINE BOOL "If baseline features should be used for older CPUs (e.g. disables AVX, AVX2)" DEFAULT OFF) endif() diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index c27d820afe..20cbb8293e 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -1163,7 +1163,7 @@ if(NOT BUN_CPP_ONLY) if(CI) set(bunTriplet bun-${OS}-${ARCH}) - if(ABI STREQUAL "musl") + if(LINUX AND ABI STREQUAL "musl") set(bunTriplet ${bunTriplet}-musl) endif() if(ENABLE_BASELINE) diff --git a/cmake/tools/SetupLLVM.cmake b/cmake/tools/SetupLLVM.cmake index 5e5fd3a953..9db637b60d 100644 --- a/cmake/tools/SetupLLVM.cmake +++ b/cmake/tools/SetupLLVM.cmake @@ -4,7 +4,7 @@ if(NOT ENABLE_LLVM) return() endif() -if(CMAKE_HOST_WIN32 OR CMAKE_HOST_APPLE OR ABI STREQUAL "musl") +if(CMAKE_HOST_WIN32 OR CMAKE_HOST_APPLE OR EXISTS "/etc/alpine-release") set(DEFAULT_LLVM_VERSION "18.1.8") else() set(DEFAULT_LLVM_VERSION "16.0.6") diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index dd263335c4..e7cb26be5e 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -63,7 +63,7 @@ else() message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}") endif() -if(ABI STREQUAL "musl") +if(LINUX AND ABI STREQUAL "musl") set(WEBKIT_SUFFIX "-musl") endif() From 468a392fd5d38fd74cef501f259f2ea4baf4a1e3 Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Mon, 25 Nov 2024 00:09:57 -0700 Subject: [PATCH 07/92] ci: Larger zig agents --- .buildkite/ci.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 733e95d843..f7079da089 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -306,7 +306,7 @@ function getPipeline(options) { */ const getZigAgent = platform => { const { arch } = platform; - const instanceType = arch === "aarch64" ? "c8g.large" : "c7i.large"; + const instanceType = arch === "aarch64" ? "c8g.2xlarge" : "c7i.2xlarge"; return { robobun: true, robobun2: true, From 4f8c1c9124bd74ef5cc6dc6632c33b18a685d2f6 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 00:11:10 -0800 Subject: [PATCH 08/92] Does this make the tests less flaky (#15399) --- scripts/runner.node.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 792c825ac1..ae70949a86 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -495,11 +495,11 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { stderr, }); } finally { - try { - rmSync(tmpdirPath, { recursive: true, force: true }); - } catch (error) { - console.warn(error); - } + // try { + // rmSync(tmpdirPath, { recursive: true, force: true }); + // } catch (error) { + // console.warn(error); + // } } } From 9cbe1ec3002aaa10be68963ccf70b4552592c592 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 00:12:28 -0800 Subject: [PATCH 09/92] Include `docs/` folder in bun-types (#15398) --- docs/{ => contributing}/upgrading-webkit.md | 0 docs/troubleshooting.md | 67 --------------------- packages/bun-types/.gitignore | 4 +- packages/bun-types/package.json | 9 ++- 4 files changed, 9 insertions(+), 71 deletions(-) rename docs/{ => contributing}/upgrading-webkit.md (100%) delete mode 100644 docs/troubleshooting.md diff --git a/docs/upgrading-webkit.md b/docs/contributing/upgrading-webkit.md similarity index 100% rename from docs/upgrading-webkit.md rename to docs/contributing/upgrading-webkit.md diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 07303bff33..0000000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,67 +0,0 @@ -## Troubleshooting - -### Bun not running on an M1 (or Apple Silicon) - -If you see a message like this - -> [1] 28447 killed bun create next ./test - -It most likely means you’re running Bun’s x64 version on Apple Silicon. This happens if Bun is running via Rosetta. Rosetta is unable to emulate AVX2 instructions, which Bun indirectly uses. - -The fix is to ensure you installed a version of Bun built for Apple Silicon. - -### error: Unexpected - -If you see an error like this: - -![image](https://user-images.githubusercontent.com/709451/141210854-89434678-d21b-42f4-b65a-7df3b785f7b9.png) - -It usually means the max number of open file descriptors is being explicitly set to a low number. By default, Bun requests the max number of file descriptors available (which on macOS, is something like 32,000). But, if you previously ran into ulimit issues with, e.g., Chokidar, someone on The Internet may have advised you to run `ulimit -n 8192`. - -That advice unfortunately **lowers** the hard limit to `8192`. This can be a problem in large repositories or projects with lots of dependencies. Chokidar (and other watchers) don’t seem to call `setrlimit`, which means they’re reliant on the (much lower) soft limit. - -To fix this issue: - -1. Remove any scripts that call `ulimit -n` and restart your shell. -2. Try again, and if the error still occurs, try setting `ulimit -n` to an absurdly high number, such as `ulimit -n 2147483646` -3. Try again, and if that still doesn’t fix it, open an issue - -### Unzip is required - -Unzip is required to install Bun on Linux. You can use one of the following commands to install `unzip`: - -#### Debian / Ubuntu / Mint - -```sh -$ sudo apt install unzip -``` - -#### RedHat / CentOS / Fedora - -```sh -$ sudo dnf install unzip -``` - -#### Arch / Manjaro - -```sh -$ sudo pacman -S unzip -``` - -#### OpenSUSE - -```sh -$ sudo zypper install unzip -``` - -### bun install is stuck - -Please run `bun install --verbose 2> logs.txt` and send them to me in Bun's discord. If you're on Linux, it would also be helpful if you run `sudo perf trace bun install --silent` and attach the logs. - -### Uninstalling - -Bun's binary and install cache is located in `~/.bun` by default. To uninstall bun, delete this directory and edit your shell config (`.bashrc`, `.zshrc`, or similar) to remove `~/.bun/bin` from the `$PATH` variable. - -```sh -$ rm -rf ~/.bun # make sure to remove ~/.bun/bin from $PATH -``` diff --git a/packages/bun-types/.gitignore b/packages/bun-types/.gitignore index 04c01ba7ba..324bb91768 100644 --- a/packages/bun-types/.gitignore +++ b/packages/bun-types/.gitignore @@ -1,2 +1,4 @@ node_modules/ -dist/ \ No newline at end of file +dist/ +docs/ +*.tgz \ No newline at end of file diff --git a/packages/bun-types/package.json b/packages/bun-types/package.json index 3d406b2c92..5cf34ddc8e 100644 --- a/packages/bun-types/package.json +++ b/packages/bun-types/package.json @@ -3,14 +3,16 @@ "license": "MIT", "main": "", "types": "index.d.ts", - "description": "Type definitions for Bun, an incredibly fast JavaScript runtime", + "description": "Type definitions and documentation for Bun, an incredibly fast JavaScript runtime", "repository": { "type": "git", "url": "https://github.com/oven-sh/bun", "directory": "packages/bun-types" }, "files": [ - "*.d.ts" + "*.d.ts", + "docs/**/*.md", + "docs/*.md" ], "homepage": "https://bun.sh", "dependencies": { @@ -25,7 +27,8 @@ }, "scripts": { "prebuild": "echo $(pwd)", - "build": "bun scripts/build.ts && bun run fmt", + "copy-docs": "rm -rf docs && cp -r ../../docs/ ./docs", + "build": "bun run copy-docs && bun scripts/build.ts && bun run fmt", "test": "tsc", "fmt": "echo $(which biome) && biome format --write ." }, From 812288eb7226941dca9be848f7ab1c54a83ef3cd Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 25 Nov 2024 04:43:58 -0800 Subject: [PATCH 10/92] [internal] Add problem matcher for Zig --- .vscode/tasks.json | 91 +++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index faf1dc0d22..5ead186425 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", + "type": "shell", + "command": "bun run build", + "group": { + "kind": "build", + "isDefault": true, }, - "options": { - "cwd": "${workspaceFolder}", - }, - "isBuildCommand": true, - "runOptions": { - "instanceLimit": 1, - "reevaluateOnRerun": 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 11/92] 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 c7977af8eb..66e4d05ea2 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 12/92] 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 191c0a815e..00f72d4ddf 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 50af2bfa40..87bdedea0c 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 4cd108db82..06bac6ac3c 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 fbe26418f1..08be605378 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 32c54f6a39..ee7c2ca91f 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 03cc2b5247..931cf8d72b 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 f0b7756ba1..87bda9659b 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 9b78740a1f..bd2197511c 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 13/92] 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 d8723e2ba9..a99b662488 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 2080aa324c..671158126e 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 14/92] 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 ba1b1636aa..f89c6f08b2 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 45bd817209..9a7a276d76 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 3c5fd21b8e..5f99735d25 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 2865d92074..5aeb892712 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 2baa031f5c..5f45a118da 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 3c5115ac97..b40b005f01 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 12ba5cd8e9..87ab2838a3 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 af62ff0b89..63b462787b 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 5ae74ade31..7d8ffc223f 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 92eab545e0..34fc02a4b5 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 e899de489d..ba5e3cfad8 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 a475d6b968..2b6e85b508 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 4947c7e97e..2ab7e4ddd6 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 12c78ff158..36f85fdcde 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 e3b3c06c33..f1443a2df4 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 fbe8aab8d5..9445c8ab7c 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 8c6e176cee..cf7662994d 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 316d0cf451..0768bfb0c1 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 21a7464a00..2ba0819628 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 35e48af852..a54941f028 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 33a34d6a27..748fdbe32c 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 15/92] 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 e872c00b10..dcf4307f6b 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 16/92] 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 | 113 +-- 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, 978 insertions(+), 164 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 5f99735d25..ab7c421e48 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 5aeb892712..861d5d002b 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 87ab2838a3..4648fafbc9 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 a99b662488..0497c1c6a6 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 f80dd36148..b813c614cf 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 63b462787b..cfeaee4b89 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); } + 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; + }, + }; + } + // 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; - return prop; - } - - return null; + 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 d9cb08f989..c0ea3aafaf 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -1449,63 +1449,70 @@ fn crash() noreturn { pub var verbose_error_trace = false; -fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { +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; + }; + + 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)}, + ); + } + 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| { - // 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; - }; - - 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)}, - ); - } - 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 }, - ); - } - } + coldHandleErrorReturnTrace(@intFromError(err), trace, is_root); } } diff --git a/src/string.zig b/src/string.zig index d47cc49cf0..3b2dab9fcb 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 82d3231b6f..15a3e8da03 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 0000000000..21e62f2d2c --- /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 0000000000..8414c32d60 --- /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 17/92] 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 9a7a276d76..fa535763df 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 4648fafbc9..0ee0befe78 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 cfeaee4b89..f488508018 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 34fc02a4b5..6437dcfb4d 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 3031cf2192..26af8a9848 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 a54941f028..1d40b949c1 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 748fdbe32c..577936c05d 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 18/92] feat(DevServer): batch bundles & run them asynchronously (#15181) Co-authored-by: Ashcon Partovi --- src/analytics/analytics_thread.zig | 4 +- src/bake/DevServer.zig | 2108 +++++++++++++++-------- 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 | 400 ++++- 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 | 69 +- 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, 4472 insertions(+), 2208 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 18094bcba1..3c2c9bbe4d 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 086ab1e41a..64821c5dbb 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, - framework: bake.Framework, - dump_sources: ?[]const u8 = if (Environment.isDebug) ".bake-debug" else null, - dump_state_on_crash: bool = bun.FeatureFlags.bake_debugging_features, - verbose_watcher: bool = false, vm: *VirtualMachine, + framework: bake.Framework, + + // Debugging features + dump_sources: ?[]const u8 = if (Environment.isDebug) ".bake-debug" else null, + dump_state_on_crash: bool = false, + verbose_watcher: bool = false, }; // 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); + const route_bundle_index = try dev.getOrPutRouteBundle(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(); + // 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); + } - var sfa = std.heap.stackFallback(4096, dev.allocator); - const temp_alloc = sfa.get(); + 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"); - var entry_points = std.ArrayList(BakeEntryPoint).init(temp_alloc); - defer entry_points.deinit(); + 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(); - // 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); - } + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); - if (entry_points.items.len == 0) { - @panic("TODO: trace graph for possible errors, so DevServer knows what state this should go to"); - } + dev.appendRouteEntryPointsIfNotStale(&entry_points, temp_alloc, route_index) catch bun.outOfMemory(); - 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 + 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, +const DeferredRequest = struct { + route_bundle_index: RouteBundle.Index, + data: Data, - ServerLoadFailed, + const Data = union(enum) { + server_handler: bun.JSC.API.SavedRequest, + js_payload: *Response, + + const Tag = @typeInfo(Data).Union.tag_type.?; + }; }; -fn bundle(dev: *DevServer, files: []const BakeEntryPoint) BundleError!void { - defer dev.emitVisualizerMessageIfNeeded() catch bun.outOfMemory(); +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(); - assert(files.len > 0); - - const bundle_file_list = bun.Output.Scoped(.bundle_file_list, false); - - 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; + bv2.asynchronous = true; - 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(); - - 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(); + dev.graph_safety_lock.lock(); + defer dev.graph_safety_lock.unlock(); - 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.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(); + 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); + try w.writeInt(u32, @intCast(dev.incremental_result.failures_removed.items.len), .little); - for (dev.incremental_result.failures_removed.items) |removed| { - try w.writeInt(u32, @bitCast(removed.getOwner().encode()), .little); - removed.deinit(); - } - - 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: a flag to this function which is queues all + // direct importers to rebuild themselves, which will + // display the bundling errors. } } - // TODO: it is infeasible to do this since FrameworkRouter contains file indices - // to the server graph - { - return; - } + const keys = g.bundled_files.keys(); - g.bundled_files.swapRemoveAt(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 - // 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()); - - 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. + /// Sent on a successful bundle, containing client code, updates routes, and + /// changed CSS files. Emitted on the `.hot_update` topic. /// - /// - 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. - /// - /// - `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. - visualizer = 'v', + /// 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(); + const temp_alloc = sfb.get(); + var entry_points: EntryPointList = EntryPointList.empty; + defer entry_points.deinit(temp_alloc); - // 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(); - } - } - - 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) }; +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(); + } +} - dev: *DevServer, - concurrent_task: JSC.ConcurrentTask = undefined, +/// 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 b689202c2f..b26bf3eda7 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 17212629ba..3262229705 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 9497ab63ec..204a68f151 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 d21fe1f634..f0408c5fcf 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,32 +100,14 @@ const BuildConfigSubset = struct { conditions: bun.StringArrayHashMapUnmanaged(void) = .{}, drop: bun.StringArrayHashMapUnmanaged(void) = .{}, // TODO: plugins + + pub fn loadFromJs(config: *BuildConfigSubset, value: JSValue, arena: Allocator) !void { + _ = config; // autofix + _ = value; // autofix + _ = arena; // autofix + } }; -/// 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; -} - /// 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. /// Since many fields have default values which may point to static memory, this @@ -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 c0dc76ddcc..9353da0f8e 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