diff --git a/.gitignore b/.gitignore index 3822491fcbf949..af3120ab439e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -116,8 +116,10 @@ scripts/env.local sign.*.json sign.json src/bake/generated.ts +src/generated_enum_extractor.zig src/bun.js/bindings-obj src/bun.js/bindings/GeneratedJS2Native.zig +src/bun.js/bindings/GeneratedBindings.zig src/bun.js/debug-bindings-obj src/deps/zig-clap/.gitattributes src/deps/zig-clap/.github diff --git a/.vscode/settings.json b/.vscode/settings.json index e1cc89f0a93d8f..deead7e531af4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,7 +63,7 @@ "editor.tabSize": 4, "editor.defaultFormatter": "xaver.clang-format", }, - "clangd.arguments": ["-header-insertion=never"], + "clangd.arguments": ["-header-insertion=never", "-no-unused-includes"], // JavaScript "prettier.enable": true, diff --git a/build.zig b/build.zig index cfc512ad8d9d41..9a1e3b25a7fd82 100644 --- a/build.zig +++ b/build.zig @@ -327,6 +327,19 @@ pub fn build(b: *Build) !void { .{ .os = .windows, .arch = .x86_64 }, }); } + + // zig build enum-extractor + { + // const step = b.step("enum-extractor", "Extract enum definitions (invoked by a code generator)"); + // const exe = b.addExecutable(.{ + // .name = "enum_extractor", + // .root_source_file = b.path("./src/generated_enum_extractor.zig"), + // .target = b.graph.host, + // .optimize = .Debug, + // }); + // const run = b.addRunArtifact(exe); + // step.dependOn(&run.step); + } } pub fn addMultiCheck( diff --git a/bun.lockb b/bun.lockb index 4ccfae2715a8e4..2413198394fa84 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 20cbb8293e91de..3b6635febdfdd0 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -318,13 +318,13 @@ register_command( TARGET bun-bake-codegen COMMENT - "Bundling Kit Runtime" + "Bundling Bake Runtime" COMMAND ${BUN_EXECUTABLE} run ${BUN_BAKE_RUNTIME_CODEGEN_SCRIPT} --debug=${DEBUG} - --codegen_root=${CODEGEN_PATH} + --codegen-root=${CODEGEN_PATH} SOURCES ${BUN_BAKE_RUNTIME_SOURCES} ${BUN_BAKE_RUNTIME_CODEGEN_SOURCES} @@ -334,6 +334,39 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) + +file(GLOB_RECURSE BUN_BINDGEN_SOURCES ${CONFIGURE_DEPENDS} + ${CWD}/src/**/*.bind.ts +) + +set(BUN_BINDGEN_CPP_OUTPUTS + ${CODEGEN_PATH}/GeneratedBindings.cpp +) + +set(BUN_BINDGEN_ZIG_OUTPUTS + ${CWD}/src/bun.js/bindings/GeneratedBindings.zig +) + +register_command( + TARGET + bun-binding-generator + COMMENT + "Processing \".bind.ts\" files" + COMMAND + ${BUN_EXECUTABLE} + run + ${BUN_BINDGEN_SCRIPT} + --debug=${DEBUG} + --codegen-root=${CODEGEN_PATH} + SOURCES + ${BUN_BINDGEN_SOURCES} + ${BUN_BINDGEN_SCRIPT} + OUTPUTS + ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGEN_ZIG_OUTPUTS} +) + set(BUN_JS_SINK_SCRIPT ${CWD}/src/codegen/generate-jssink.ts) set(BUN_JS_SINK_SOURCES @@ -385,7 +418,6 @@ set(BUN_OBJECT_LUT_OUTPUTS ${CODEGEN_PATH}/NodeModuleModule.lut.h ) - macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps) set(_tmp) get_source_file_property(_tmp ${_source} OBJECT_DEPENDS) @@ -461,6 +493,7 @@ list(APPEND BUN_ZIG_SOURCES ${CWD}/build.zig ${CWD}/root.zig ${CWD}/root_wasm.zig + ${BUN_BINDGEN_ZIG_OUTPUTS} ) set(BUN_ZIG_GENERATED_SOURCES @@ -482,7 +515,6 @@ endif() set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-zig.o) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64") if(APPLE) set(ZIG_CPU "apple_m1") @@ -606,6 +638,7 @@ list(APPEND BUN_CPP_SOURCES ${BUN_JS_SINK_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} + ${BUN_BINDGEN_CPP_OUTPUTS} ) if(WIN32) diff --git a/docs/nav.ts b/docs/nav.ts index c4f04ca7c28cbe..6dd5a06dca6c02 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -402,6 +402,9 @@ export default { page("project/building-windows", "Building Windows", { description: "Learn how to setup a development environment for contributing to the Windows build of Bun.", }), + page("project/bindgen", "Bindgen", { + description: "About the bindgen code generator", + }), page("project/licensing", "License", { description: `Bun is a MIT-licensed project with a large number of statically-linked dependencies with various licenses.`, }), diff --git a/docs/project/bindgen.md b/docs/project/bindgen.md new file mode 100644 index 00000000000000..3144d7f57f58c5 --- /dev/null +++ b/docs/project/bindgen.md @@ -0,0 +1,199 @@ +{% callout %} + +This document is for maintainers and contributors to Bun, and describes internal implementation details. + +{% /callout %} + +The new bindings generator, introduced to the codebase in Dec 2024, scans for +`*.bind.ts` to find function and class definition, and generates glue code to +interop between JavaScript and native code. + +There are currently other code generators and systems that achieve similar +purposes. The following will all eventually be completely phased out in favor of +this one: + +- "Classes generator", converting `*.classes.ts` for custom classes. +- "JS2Native", allowing ad-hoc calls from `src/js` to native code. + +## Creating JS Functions in Zig + +Given a file implementing a simple function, such as `add` + +```zig#src/bun.js/math.zig +pub fn add(global: *JSC.JSGlobalObject, a: i32, b: i32) !i32 { + return std.math.add(i32, a, b) catch { + // Binding functions can return `error.OutOfMemory` and `error.JSError`. + // Others like `error.Overflow` from `std.math.add` must be converted. + // Remember to be descriptive. + return global.throwPretty("Integer overflow while adding", .{}); + }; +} + +const gen = bun.gen.math; // "math" being this file's basename + +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +``` + +Then describe the API schema using a `.bind.ts` function. The binding file goes next to the Zig file. + +```ts#src/bun.js/math.bind.ts +import { t, fn } from 'bindgen'; + +export const add = fn({ + args: { + global: t.globalObject, + a: t.i32, + b: t.i32.default(1), + }, + ret: t.i32, +}); +``` + +This function declaration is equivalent to: + +```ts +/** + * Throws if zero arguments are provided. + * Wraps out of range numbers using modulo. + */ +declare function add(a: number, b: number = 1): number; +``` + +The code generator will provide `bun.gen.math.jsAdd`, which is the native function implementation. To pass to JavaScript, use `bun.gen.math.createAddCallback(global)` + +## Strings + +The type for receiving strings is one of [`t.DOMString`](https://webidl.spec.whatwg.org/#idl-DOMString), [`t.ByteString`](https://webidl.spec.whatwg.org/#idl-ByteString), and [`t.USVString`](https://webidl.spec.whatwg.org/#idl-USVString). These map directly to their WebIDL counterparts, and have slightly different conversion logic. Bindgen will pass BunString to native code in all cases. + +When in doubt, use DOMString. + +`t.UTF8String` can be used in place of `t.DOMString`, but will call `bun.String.toUTF8`. The native callback gets `[]const u8` (WTF-8 data) passed to native code, freeing it after the function returns. + +TLDRs from WebIDL spec: + +- ByteString can only contain valid latin1 characters. It is not safe to assume bun.String is already in 8-bit format, but it is extremely likely. +- USVString will not contain invalid surrogate pairs, aka text that can be represented correctly in UTF-8. +- DOMString is the loosest but also most recommended strategy. + +## Function Variants + +A `variants` can specify multiple variants (also known as overloads). + +```ts#src/bun.js/math.bind.ts +import { t, fn } from 'bindgen'; + +export const action = fn({ + variants: [ + { + args: { + a: t.i32, + }, + ret: t.i32, + }, + { + args: { + a: t.DOMString, + }, + ret: t.DOMString, + }, + ] +}); +``` + +In Zig, each variant gets a number, based on the order the schema defines. + +``` +fn action1(a: i32) i32 { + return a; +} + +fn action2(a: bun.String) bun.String { + return a; +} +``` + +## `t.dictionary` + +A `dictionary` is a definition for a JavaScript object, typically as a function inputs. For function outputs, it is usually a smarter idea to declare a class type to add functions and destructuring. + +## Enumerations + +To use [WebIDL's enumeration](https://webidl.spec.whatwg.org/#idl-enums) type, use either: + +- `t.stringEnum`: Create and codegen a new enum type. +- `t.zigEnum`: Derive a bindgen type off of an existing enum in the codebase. + +An example of `stringEnum` as used in `fmt.zig` / `bun:internal-for-testing` + +```ts +export const Formatter = t.stringEnum( + "highlight-javascript", + "escape-powershell", +); + +export const fmtString = fn({ + args: { + global: t.globalObject, + code: t.UTF8String, + formatter: Formatter, + }, + ret: t.DOMString, +}); +``` + +WebIDL strongly encourages using kebab case for enumeration values, to be consistent with existing Web APIs. + +### Deriving enums from Zig code + +TODO: zigEnum + +## `t.oneOf` + +A `oneOf` is a union between two or more types. It is represented by `union(enum)` in Zig. + +TODO: + +## Attributes + +There are set of attributes that can be chained onto `t.*` types. On all types there are: + +- `.required`, in dictionary parameters only +- `.optional`, in function arguments only +- `.default(T)` + +When a value is optional, it is lowered to a Zig optional. + +Depending on the type, there are more attributes available. See the type definitions in auto-complete for more details. Note that one of the above three can only be applied, and they must be applied at the end. + +### Integer Attributes + +Integer types allow customizing the overflow behavior with `clamp` or `enforceRange` + +```ts +import { t, fn } from "bindgen"; + +export const add = fn({ + args: { + global: t.globalObject, + // enforce in i32 range + a: t.i32.enforceRange(), + // clamp to u16 range + c: t.u16, + // enforce in arbitrary range, with a default if not provided + b: t.i32.enforceRange(0, 1000).default(5), + // clamp to arbitrary range, or null + d: t.u16.clamp(0, 10).optional, + }, + ret: t.i32, +}); +``` + +## Callbacks + +TODO + +## Classes + +TODO diff --git a/package.json b/package.json index accf7ccaac5471..f32b639df7f8d7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "source-map-js": "^1.2.0", - "typescript": "^5.4.5", + "typescript": "^5.7.2", "caniuse-lite": "^1.0.30001620", "autoprefixer": "^10.4.19", "@mdn/browser-compat-data": "~5.5.28" diff --git a/packages/bun-types/ambient.d.ts b/packages/bun-types/ambient.d.ts new file mode 100644 index 00000000000000..b4ac81a0a3e277 --- /dev/null +++ b/packages/bun-types/ambient.d.ts @@ -0,0 +1,9 @@ +declare module "*.txt" { + var text: string; + export = text; +} + +declare module "*.toml" { + var contents: any; + export = contents; +} diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index a29bcc91cb9dd3..7b845798178431 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -1,5 +1,3 @@ -export {}; - type _ReadableStream = typeof globalThis extends { onerror: any; ReadableStream: infer T; @@ -141,16 +139,6 @@ import type { TextDecoder as NodeTextDecoder, TextEncoder as NodeTextEncoder } f import type { MessagePort } from "worker_threads"; import type { WebSocket as _WebSocket } from "ws"; -declare module "*.txt" { - var text: string; - export = text; -} - -declare module "*.toml" { - var contents: any; - export = contents; -} - declare global { var Bun: typeof import("bun"); @@ -1835,10 +1823,10 @@ declare global { readonly main: boolean; /** Alias of `import.meta.dir`. Exists for Node.js compatibility */ - readonly dirname: string; + dirname: string; /** Alias of `import.meta.path`. Exists for Node.js compatibility */ - readonly filename: string; + filename: string; } /** diff --git a/packages/bun-types/index.d.ts b/packages/bun-types/index.d.ts index c0ceea7286b8c7..68202904d49853 100644 --- a/packages/bun-types/index.d.ts +++ b/packages/bun-types/index.d.ts @@ -20,3 +20,4 @@ /// /// /// +/// diff --git a/packages/bun-types/sqlite.d.ts b/packages/bun-types/sqlite.d.ts index 97b2e833203b1b..dd370d3f46bcc2 100644 --- a/packages/bun-types/sqlite.d.ts +++ b/packages/bun-types/sqlite.d.ts @@ -1127,7 +1127,7 @@ declare module "bun:sqlite" { * * @since Bun v1.1.14 */ - interface Changes { + export interface Changes { /** * The number of rows changed by the last `run` or `exec` call. */ diff --git a/packages/bun-types/test/ffi.test.ts b/packages/bun-types/test/ffi.test.ts index dd7ca6a3f45725..277c45b5ec57a6 100644 --- a/packages/bun-types/test/ffi.test.ts +++ b/packages/bun-types/test/ffi.test.ts @@ -1,4 +1,4 @@ -import { CString, dlopen, FFIType, Pointer, read, suffix } from "bun:ffi"; +import { CString, dlopen, FFIType, JSCallback, Pointer, read, suffix } from "bun:ffi"; import * as tsd from "./utilities.test"; // `suffix` is either "dylib", "so", or "dll" depending on the platform @@ -62,12 +62,14 @@ const lib = dlopen( }, ); +declare const ptr: Pointer; + tsd.expectType(lib.symbols.sqlite3_libversion()); tsd.expectType(lib.symbols.add(1, 2)); -tsd.expectType(lib.symbols.ptr_type(0)); +tsd.expectType(lib.symbols.ptr_type(ptr)); -tsd.expectType(lib.symbols.fn_type(0)); +tsd.expectType(lib.symbols.fn_type(new JSCallback(() => {}, {}))); function _arg( ...params: [ @@ -166,16 +168,16 @@ tsd.expectType(lib2.symbols.multi_args(1, 2)); tsd.expectTypeEquals, undefined>(true); tsd.expectTypeEquals, []>(true); -tsd.expectType(read.u8(0)); -tsd.expectType(read.u8(0, 0)); -tsd.expectType(read.i8(0, 0)); -tsd.expectType(read.u16(0, 0)); -tsd.expectType(read.i16(0, 0)); -tsd.expectType(read.u32(0, 0)); -tsd.expectType(read.i32(0, 0)); -tsd.expectType(read.u64(0, 0)); -tsd.expectType(read.i64(0, 0)); -tsd.expectType(read.f32(0, 0)); -tsd.expectType(read.f64(0, 0)); -tsd.expectType(read.ptr(0, 0)); -tsd.expectType(read.intptr(0, 0)); +tsd.expectType(read.u8(ptr)); +tsd.expectType(read.u8(ptr, 0)); +tsd.expectType(read.i8(ptr, 0)); +tsd.expectType(read.u16(ptr, 0)); +tsd.expectType(read.i16(ptr, 0)); +tsd.expectType(read.u32(ptr, 0)); +tsd.expectType(read.i32(ptr, 0)); +tsd.expectType(read.u64(ptr, 0)); +tsd.expectType(read.i64(ptr, 0)); +tsd.expectType(read.f32(ptr, 0)); +tsd.expectType(read.f64(ptr, 0)); +tsd.expectType(read.ptr(ptr, 0)); +tsd.expectType(read.intptr(ptr, 0)); diff --git a/packages/bun-types/test/sqlite.test.ts b/packages/bun-types/test/sqlite.test.ts index c094eab8c9da17..32cd98057e3d7a 100644 --- a/packages/bun-types/test/sqlite.test.ts +++ b/packages/bun-types/test/sqlite.test.ts @@ -1,4 +1,4 @@ -import { Database } from "bun:sqlite"; +import { Changes, Database } from "bun:sqlite"; import { expectType } from "./utilities.test"; const db = new Database(":memory:"); @@ -22,7 +22,7 @@ expectType>(allResults); expectType<{ name: string; dob: number } | null>(getResults); // tslint:disable-next-line:invalid-void // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -expectType(runResults); +expectType(runResults); const query3 = db.prepare< { name: string; dob: number }, // return type first diff --git a/packages/bun-types/tsconfig.json b/packages/bun-types/tsconfig.json index 42a706acb02ee9..d7bf9b4856b1a2 100644 --- a/packages/bun-types/tsconfig.json +++ b/packages/bun-types/tsconfig.json @@ -1,16 +1,14 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["ESNext"], "skipLibCheck": false, - "strict": true, - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "disableSolutionSearching": true, - "noUnusedLocals": true, - "noEmit": true, - "resolveJsonModule": true + + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "declarationDir": "out" }, + "files": ["ambient.d.ts"], // ambient defines .txt and .toml loaders + "include": ["**/*.ts"], "exclude": ["dist", "node_modules"] } diff --git a/src/Global.zig b/src/Global.zig index 2945062f4373e3..f935d3b958c3a9 100644 --- a/src/Global.zig +++ b/src/Global.zig @@ -43,8 +43,6 @@ else if (Environment.isDebug) std.fmt.comptimePrint(version_string ++ "-debug+{s}", .{Environment.git_sha_short}) else if (Environment.is_canary) std.fmt.comptimePrint(version_string ++ "-canary.{d}+{s}", .{ Environment.canary_revision, Environment.git_sha_short }) -else if (Environment.isTest) - std.fmt.comptimePrint(version_string ++ "-test+{s}", .{Environment.git_sha_short}) else std.fmt.comptimePrint(version_string ++ "+{s}", .{Environment.git_sha_short}); @@ -68,7 +66,6 @@ else "unknown"; pub inline fn getStartTime() i128 { - if (Environment.isTest) return 0; return bun.start_time; } diff --git a/src/api/tsconfig.json b/src/api/tsconfig.json deleted file mode 100644 index a49f0ba6f9535e..00000000000000 --- a/src/api/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "moduleResolution": "node" - }, - "include": ["./node_modules/peechy", "./schema.d.ts"] -} diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index cae853a63da7f9..fc00d222c54079 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1870,12 +1870,12 @@ pub fn IncrementalGraph(side: bake.Side) type { /// exact size, instead of the log approach that dynamic arrays use. stale_files: DynamicBitSetUnmanaged, - /// Start of the 'dependencies' linked list. These are the other files - /// that import used by this file. Walk this list to discover what - /// files are to be reloaded when something changes. + /// Start of a file's 'dependencies' linked list. These are the other + /// files that have imports to this file. Walk this list to discover + /// what files are to be reloaded when something changes. first_dep: ArrayListUnmanaged(EdgeIndex.Optional), - /// Start of the 'imports' linked list. These are the files that this - /// file imports. + /// Start of a file's 'imports' linked lists. These are the files that + /// this file imports. first_import: ArrayListUnmanaged(EdgeIndex.Optional), /// `File` objects act as nodes in a directional many-to-many graph, /// where edges represent the imports between modules. An 'dependency' @@ -3319,7 +3319,7 @@ pub const SerializedFailure = struct { } }; - const ErrorKind = enum(u8) { + pub const ErrorKind = enum(u8) { // A log message. The `logger.Kind` is encoded here. bundler_log_err = 0, bundler_log_warn = 1, diff --git a/src/bake/bake.bind.ts b/src/bake/bake.bind.ts new file mode 100644 index 00000000000000..7a28653d34e6fa --- /dev/null +++ b/src/bake/bake.bind.ts @@ -0,0 +1,9 @@ +// import { t } from "bindgen"; + +// export const ReactFastRefresh = t.dictionary({ +// importSource: t.UTF8String, +// }); + +// export const FrameworkConfig = t.dictionary({ +// reactFastRefresh: t.oneOf(t.boolean, ReactFastRefresh).default(false), +// }); diff --git a/src/bake/bun-framework-react/client.tsx b/src/bake/bun-framework-react/client.tsx index 9353da0f8e7662..a37261ff75ae16 100644 --- a/src/bake/bun-framework-react/client.tsx +++ b/src/bake/bun-framework-react/client.tsx @@ -5,8 +5,8 @@ import * as React from "react"; import { hydrateRoot } from "react-dom/client"; import { createFromReadableStream } from "react-server-dom-bun/client.browser"; -import { onServerSideReload } from 'bun:bake/client'; -import { flushSync } from 'react-dom'; +import { onServerSideReload } from "bun:bake/client"; +import { flushSync } from "react-dom"; const te = new TextEncoder(); const td = new TextDecoder(); @@ -74,7 +74,7 @@ const Root = () => { const root = hydrateRoot(document, , { onUncaughtError(e) { console.error(e); - } + }, }); // Keep a cache of page objects to avoid re-fetching a page when pressing the @@ -118,7 +118,7 @@ const firstPageId = Date.now(); // This is done client-side because a React error will unmount all elements. const sheet = new CSSStyleSheet(); document.adoptedStyleSheets.push(sheet); - sheet.replaceSync(':where(*)::view-transition-group(root){animation:none}'); + sheet.replaceSync(":where(*)::view-transition-group(root){animation:none}"); } } @@ -142,10 +142,9 @@ async function goto(href: string, cacheId?: number) { if (cached) { currentCssList = cached.css; await ensureCssIsReady(currentCssList); - setPage?.(rscPayload = cached.element); + setPage?.((rscPayload = cached.element)); console.log("cached", cached); - if (olderController?.signal.aborted === false) - abortOnRender = olderController; + if (olderController?.signal.aborted === false) abortOnRender = olderController; return; } @@ -199,7 +198,7 @@ async function goto(href: string, cacheId?: number) { // Save this promise so that pressing the back button in the browser navigates // to the same instance of the old page, instead of re-fetching it. if (cacheId) { - cachedPages.set(cacheId, { css: currentCssList, element: p }); + cachedPages.set(cacheId, { css: currentCssList!, element: p }); } // Defer aborting a previous request until VERY late. If a previous stream is @@ -214,8 +213,7 @@ async function goto(href: string, cacheId?: number) { if (document.startViewTransition as unknown) { document.startViewTransition(() => { flushSync(() => { - if (thisNavigationId === lastNavigationId) - setPage(rscPayload = p); + if (thisNavigationId === lastNavigationId) setPage((rscPayload = p)); }); }); } else { @@ -342,8 +340,8 @@ window.addEventListener("popstate", event => { if (import.meta.env.DEV) { // Frameworks can call `onServerSideReload` to hook into server-side hot - // module reloading. - onServerSideReload(async() => { + // module reloading. + onServerSideReload(async () => { const newId = Date.now(); history.replaceState(newId, "", location.href); await goto(location.href, newId); @@ -355,7 +353,7 @@ if (import.meta.env.DEV) { onServerSideReload, get currentCssList() { return currentCssList; - } + }, }; } @@ -417,7 +415,7 @@ async function readCssMetadataFallback(stream: ReadableStream) { } if (chunks.length === 1) { const first = chunks[0]; - if(first.byteLength >= size) { + if (first.byteLength >= size) { chunks[0] = first.subarray(size); totalBytes -= size; return first.subarray(0, size); @@ -446,14 +444,14 @@ async function readCssMetadataFallback(stream: ReadableStream) { return buffer; } }; - const header = new Uint32Array(await readChunk(4))[0]; - console.log('h', header); + const header = new Uint32Array(await readChunk(4))[0]; + console.log("h", header); if (header === 0) { currentCssList = []; } else { currentCssList = td.decode(await readChunk(header)).split("\n"); } - console.log('cc', currentCssList); + console.log("cc", currentCssList); if (chunks.length === 0) { return stream; } @@ -474,6 +472,6 @@ async function readCssMetadataFallback(stream: ReadableStream) { }, cancel() { reader.cancel(); - } + }, }); } diff --git a/src/bake/bun-framework-react/ssr.tsx b/src/bake/bun-framework-react/ssr.tsx index d42c10a2412422..a58a16239b0e80 100644 --- a/src/bake/bun-framework-react/ssr.tsx +++ b/src/bake/bun-framework-react/ssr.tsx @@ -7,7 +7,7 @@ import type { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { createFromNodeStream, type Manifest } from "react-server-dom-bun/client.node.unbundled.js"; import { renderToPipeableStream } from "react-dom/server.node"; -import { MiniAbortSignal } from "./server"; +import type { MiniAbortSignal } from "./server"; // Verify that React 19 is being used. if (!React.use) { diff --git a/src/bake/hmr-module.ts b/src/bake/hmr-module.ts index 15801f5031e2bb..4cec10a244366a 100644 --- a/src/bake/hmr-module.ts +++ b/src/bake/hmr-module.ts @@ -56,7 +56,7 @@ export class HotModule { mod._deps.set(this, onReload ? { _callback: onReload, _expectedImports: expectedImports } : undefined); const { exports, __esModule } = mod; const object = __esModule ? exports : (mod._ext_exports ??= { ...exports, default: exports }); - + if (expectedImports && mod._state === State.Ready) { for (const key of expectedImports) { if (!(key in object)) { @@ -156,14 +156,16 @@ class Hot { } function isUnsupportedViteEventName(str: string) { - return str === 'vite:beforeUpdate' - || str === 'vite:afterUpdate' - || str === 'vite:beforeFullReload' - || str === 'vite:beforePrune' - || str === 'vite:invalidate' - || str === 'vite:error' - || str === 'vite:ws:disconnect' - || str === 'vite:ws:connect'; + return ( + str === "vite:beforeUpdate" || + str === "vite:afterUpdate" || + str === "vite:beforeFullReload" || + str === "vite:beforePrune" || + str === "vite:invalidate" || + str === "vite:error" || + str === "vite:ws:disconnect" || + str === "vite:ws:connect" + ); } /** @@ -196,7 +198,7 @@ export function loadModule(key: Id, type: LoadModuleType): HotModule load(mod); mod._state = State.Ready; mod._deps.forEach((entry, dep) => { - entry._callback?.(mod.exports); + entry?._callback(mod.exports); }); } catch (err) { console.error(err); @@ -212,7 +214,7 @@ export const getModule = registry.get.bind(registry); export function replaceModule(key: Id, load: ModuleLoadFunction) { const module = registry.get(key); if (module) { - module._onDispose?.forEach((cb) => cb(null)); + module._onDispose?.forEach(cb => cb(null)); module.exports = {}; load(module); const { exports } = module; @@ -268,7 +270,7 @@ if (side === "client") { const server_module = new HotModule("bun:bake/client"); server_module.__esModule = true; server_module.exports = { - onServerSideReload: async (cb) => { + onServerSideReload: async cb => { onServerSideReload = cb; }, }; diff --git a/src/bake/hmr-runtime-error.ts b/src/bake/hmr-runtime-error.ts index e59e97efe40f03..433f70b8c78ef0 100644 --- a/src/bake/hmr-runtime-error.ts +++ b/src/bake/hmr-runtime-error.ts @@ -54,7 +54,7 @@ initWebSocket({ } }, - [MessageId.errors_cleared]() { - location.reload(); - }, + // [MessageId.errors_cleared]() { + // location.reload(); + // }, }); diff --git a/src/bake/tsconfig.json b/src/bake/tsconfig.json index 81f1f16c3a66df..11e0dce4dd0d48 100644 --- a/src/bake/tsconfig.json +++ b/src/bake/tsconfig.json @@ -1,22 +1,14 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["DOM", "ESNext"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "noEmit": true, - "strict": true, - "noImplicitAny": false, - "allowJs": true, - "downlevelIteration": true, - "esModuleInterop": true, - "skipLibCheck": true, + "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], "paths": { - "bun-framework-react/*": ["./bun-framework-react/*"] + "bun-framework-react/*": ["./bun-framework-react/*"], + "bindgen": ["../codegen/bindgen-lib"] }, "jsx": "react-jsx", "types": ["react/experimental"] }, - "include": ["**/*.ts", "**/*.tsx"] + "include": ["**/*.ts", "**/*.tsx", "../runtime.js", "../runtime.bun.js"], + "references": [{ "path": "../../packages/bun-types" }] } diff --git a/src/bun.js/api/BunObject.bind.ts b/src/bun.js/api/BunObject.bind.ts new file mode 100644 index 00000000000000..9cfbd7ccdafa3b --- /dev/null +++ b/src/bun.js/api/BunObject.bind.ts @@ -0,0 +1,36 @@ +import { t, fn } from "bindgen"; + +export const BracesOptions = t.dictionary({ + tokenize: t.boolean.default(false), + parse: t.boolean.default(false), +}); + +export const braces = fn({ + args: { + global: t.globalObject, + input: t.DOMString, + options: BracesOptions.default({}), + }, + ret: t.any, +}); + +export const gc = fn({ + args: { + vm: t.zigVirtualMachine, + force: t.boolean.default(false), + }, + ret: t.usize, +}); + +export const StringWidthOptions = t.dictionary({ + countAnsiEscapeCodes: t.boolean.default(false), + ambiguousIsNarrow: t.boolean.default(true), +}); + +export const stringWidth = fn({ + args: { + str: t.DOMString.default(""), + opts: StringWidthOptions.default({}), + }, + ret: t.usize, +}); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index ec7cd1db4e7c27..9f1345b3d84316 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1,4 +1,5 @@ const conv = std.builtin.CallingConvention.Unspecified; + /// How to add a new function or property to the Bun global /// /// - Add a callback or property to the below struct @@ -10,7 +11,6 @@ const conv = std.builtin.CallingConvention.Unspecified; pub const BunObject = struct { // --- Callbacks --- pub const allocUnsafe = toJSCallback(Bun.allocUnsafe); - pub const braces = toJSCallback(Bun.braces); pub const build = toJSCallback(Bun.JSBundler.buildFn); pub const color = toJSCallback(bun.css.CssColor.jsFunctionColor); pub const connect = toJSCallback(JSC.wrapStaticMethod(JSC.API.Listener, "connect", false)); @@ -18,7 +18,6 @@ pub const BunObject = struct { pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter); pub const deflateSync = toJSCallback(JSZlib.deflateSync); pub const file = toJSCallback(WebCore.Blob.constructBunFile); - pub const gc = toJSCallback(Bun.runGC); pub const generateHeapSnapshot = toJSCallback(Bun.generateHeapSnapshot); pub const gunzipSync = toJSCallback(JSZlib.gunzipSync); pub const gzipSync = toJSCallback(JSZlib.gzipSync); @@ -39,7 +38,6 @@ pub const BunObject = struct { pub const sleepSync = toJSCallback(Bun.sleepSync); pub const spawn = toJSCallback(JSC.wrapStaticMethod(JSC.Subprocess, "spawn", false)); pub const spawnSync = toJSCallback(JSC.wrapStaticMethod(JSC.Subprocess, "spawnSync", false)); - pub const stringWidth = toJSCallback(Bun.stringWidth); pub const udpSocket = toJSCallback(JSC.wrapStaticMethod(JSC.API.UDPSocket, "udpSocket", false)); pub const which = toJSCallback(Bun.which); pub const write = toJSCallback(JSC.WebCore.Blob.writeFile); @@ -136,7 +134,6 @@ pub const BunObject = struct { // -- Callbacks -- @export(BunObject.allocUnsafe, .{ .name = callbackName("allocUnsafe") }); - @export(BunObject.braces, .{ .name = callbackName("braces") }); @export(BunObject.build, .{ .name = callbackName("build") }); @export(BunObject.color, .{ .name = callbackName("color") }); @export(BunObject.connect, .{ .name = callbackName("connect") }); @@ -144,7 +141,6 @@ pub const BunObject = struct { @export(BunObject.createShellInterpreter, .{ .name = callbackName("createShellInterpreter") }); @export(BunObject.deflateSync, .{ .name = callbackName("deflateSync") }); @export(BunObject.file, .{ .name = callbackName("file") }); - @export(BunObject.gc, .{ .name = callbackName("gc") }); @export(BunObject.generateHeapSnapshot, .{ .name = callbackName("generateHeapSnapshot") }); @export(BunObject.gunzipSync, .{ .name = callbackName("gunzipSync") }); @export(BunObject.gzipSync, .{ .name = callbackName("gzipSync") }); @@ -165,7 +161,6 @@ pub const BunObject = struct { @export(BunObject.sleepSync, .{ .name = callbackName("sleepSync") }); @export(BunObject.spawn, .{ .name = callbackName("spawn") }); @export(BunObject.spawnSync, .{ .name = callbackName("spawnSync") }); - @export(BunObject.stringWidth, .{ .name = callbackName("stringWidth") }); @export(BunObject.udpSocket, .{ .name = callbackName("udpSocket") }); @export(BunObject.which, .{ .name = callbackName("which") }); @export(BunObject.write, .{ .name = callbackName("write") }); @@ -285,68 +280,42 @@ pub fn shellEscape(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b return jsval; } -pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(2); - var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); - defer arguments.deinit(); - - const brace_str_js = arguments.nextEat() orelse { - return globalThis.throw("braces: expected at least 1 argument, got 0", .{}); - }; - const brace_str = brace_str_js.toBunString(globalThis); - defer brace_str.deref(); - if (globalThis.hasException()) return .zero; +const gen = bun.gen.BunObject; +pub fn braces(global: *JSC.JSGlobalObject, brace_str: bun.String, opts: gen.BracesOptions) bun.JSError!JSC.JSValue { const brace_slice = brace_str.toUTF8(bun.default_allocator); defer brace_slice.deinit(); - var tokenize: bool = false; - var parse: bool = false; - if (arguments.nextEat()) |opts_val| { - if (opts_val.isObject()) { - if (comptime bun.Environment.allow_assert) { - if (try opts_val.getTruthy(globalThis, "tokenize")) |tokenize_val| { - tokenize = if (tokenize_val.isBoolean()) tokenize_val.asBoolean() else false; - } - - if (try opts_val.getTruthy(globalThis, "parse")) |tokenize_val| { - parse = if (tokenize_val.isBoolean()) tokenize_val.asBoolean() else false; - } - } - } - } - if (globalThis.hasException()) return .zero; - var arena = std.heap.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); var lexer_output = Braces.Lexer.tokenize(arena.allocator(), brace_slice.slice()) catch |err| { - return globalThis.throwError(err, "failed to tokenize braces"); + return global.throwError(err, "failed to tokenize braces"); }; const expansion_count = Braces.calculateExpandedAmount(lexer_output.tokens.items[0..]) catch |err| { - return globalThis.throwError(err, "failed to calculate brace expansion amount"); + return global.throwError(err, "failed to calculate brace expansion amount"); }; - if (tokenize) { - const str = try std.json.stringifyAlloc(globalThis.bunVM().allocator, lexer_output.tokens.items[0..], .{}); - defer globalThis.bunVM().allocator.free(str); + if (opts.tokenize) { + const str = try std.json.stringifyAlloc(global.bunVM().allocator, lexer_output.tokens.items[0..], .{}); + defer global.bunVM().allocator.free(str); var bun_str = bun.String.fromBytes(str); - return bun_str.toJS(globalThis); + return bun_str.toJS(global); } - if (parse) { + if (opts.parse) { var parser = Braces.Parser.init(lexer_output.tokens.items[0..], arena.allocator()); const ast_node = parser.parse() catch |err| { - return globalThis.throwError(err, "failed to parse braces"); + return global.throwError(err, "failed to parse braces"); }; - const str = try std.json.stringifyAlloc(globalThis.bunVM().allocator, ast_node, .{}); - defer globalThis.bunVM().allocator.free(str); + const str = try std.json.stringifyAlloc(global.bunVM().allocator, ast_node, .{}); + defer global.bunVM().allocator.free(str); var bun_str = bun.String.fromBytes(str); - return bun_str.toJS(globalThis); + return bun_str.toJS(global); } if (expansion_count == 0) { - return bun.String.toJSArray(globalThis, &.{brace_str}); + return bun.String.toJSArray(global, &.{brace_str}); } var expanded_strings = try arena.allocator().alloc(std.ArrayList(u8), expansion_count); @@ -360,8 +329,10 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS lexer_output.tokens.items[0..], expanded_strings, lexer_output.contains_nested, - ) catch { - return globalThis.throwOutOfMemory(); + ) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.UnexpectedToken => return global.throwPretty("Unexpected token while expanding braces", .{}), + error.StackFull => return global.throwPretty("Too much nesting while expanding braces", .{}), }; var out_strings = try arena.allocator().alloc(bun.String, expansion_count); @@ -369,7 +340,7 @@ pub fn braces(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS out_strings[i] = bun.String.fromBytes(expanded_strings[i].items[0..]); } - return bun.String.toJSArray(globalThis, out_strings[0..]); + return bun.String.toJSArray(global, out_strings[0..]); } pub fn which(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { @@ -845,10 +816,8 @@ pub fn generateHeapSnapshot(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame return globalObject.generateHeapSnapshot(); } -pub fn runGC(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(1); - const arguments = arguments_.slice(); - return globalObject.bunVM().garbageCollect(arguments.len > 0 and arguments[0].isBoolean() and arguments[0].toBoolean()); +pub fn gc(vm: *JSC.VirtualMachine, sync: bool) usize { + return vm.garbageCollect(sync); } pub fn shrink(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { @@ -4147,8 +4116,8 @@ pub const FFIObject = struct { finalizationCallback: ?JSValue, ) JSC.JSValue { switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { - .err => |erro| { - return erro; + .err => |err| { + return err; }, .slice => |slice| { var callback: JSC.C.JSTypedArrayBytesDeallocator = null; @@ -4191,8 +4160,8 @@ pub const FFIObject = struct { valueLength: ?JSValue, ) JSC.JSValue { switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { - .err => |erro| { - return erro; + .err => |err| { + return err; }, .slice => |slice| { return JSC.JSValue.createBuffer(globalThis, slice, null); @@ -4208,37 +4177,14 @@ pub const FFIObject = struct { } }; -fn stringWidth(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(2).slice(); - const value = if (arguments.len > 0) arguments[0] else .undefined; - const options_object = if (arguments.len > 1) arguments[1] else .undefined; +pub fn stringWidth(str: bun.String, opts: gen.StringWidthOptions) usize { + if (str.length() == 0) + return 0; - if (!value.isString()) { - return JSC.jsNumber(0); - } - - const str = value.toBunString(globalObject); - defer str.deref(); - - var count_ansi_escapes = false; - var ambiguous_as_wide = false; - - if (options_object.isObject()) { - if (try options_object.getTruthy(globalObject, "countAnsiEscapeCodes")) |count_ansi_escapes_value| { - if (count_ansi_escapes_value.isBoolean()) - count_ansi_escapes = count_ansi_escapes_value.toBoolean(); - } - if (try options_object.getTruthy(globalObject, "ambiguousIsNarrow")) |ambiguous_is_narrow| { - if (ambiguous_is_narrow.isBoolean()) - ambiguous_as_wide = !ambiguous_is_narrow.toBoolean(); - } - } - - if (count_ansi_escapes) { - return JSC.jsNumber(str.visibleWidth(ambiguous_as_wide)); - } + if (opts.count_ansi_escape_codes) + return str.visibleWidth(!opts.ambiguous_is_narrow); - return JSC.jsNumber(str.visibleWidthExcludeANSIColors(ambiguous_as_wide)); + return str.visibleWidthExcludeANSIColors(!opts.ambiguous_is_narrow); } /// EnvironmentVariables is runtime defined. diff --git a/src/bun.js/bindgen_test.bind.ts b/src/bun.js/bindgen_test.bind.ts new file mode 100644 index 00000000000000..9093ffd0b915a9 --- /dev/null +++ b/src/bun.js/bindgen_test.bind.ts @@ -0,0 +1,20 @@ +import { t, fn } from "bindgen"; + +export const add = fn({ + args: { + global: t.globalObject, + a: t.i32, + b: t.i32.default(-1), + }, + ret: t.i32, +}); + +export const requiredAndOptionalArg = fn({ + args: { + a: t.boolean, + b: t.usize.optional, + c: t.i32.enforceRange(0, 100).default(42), + d: t.u8.optional, + }, + ret: t.i32, +}); diff --git a/src/bun.js/bindgen_test.zig b/src/bun.js/bindgen_test.zig new file mode 100644 index 00000000000000..224ca58a0600fb --- /dev/null +++ b/src/bun.js/bindgen_test.zig @@ -0,0 +1,36 @@ +//! This namespace is used to test binding generator +const gen = bun.gen.bindgen_test; + +pub fn getBindgenTestFunctions(global: *JSC.JSGlobalObject) JSC.JSValue { + return global.createObjectFromStruct(.{ + .add = gen.createAddCallback(global), + .requiredAndOptionalArg = gen.createRequiredAndOptionalArgCallback(global), + }).toJS(); +} + +// This example should be kept in sync with bindgen's documentation +pub fn add(global: *JSC.JSGlobalObject, a: i32, b: i32) !i32 { + return std.math.add(i32, a, b) catch { + // Binding functions can return `error.OutOfMemory` and `error.JSError`. + // Others like `error.Overflow` from `std.math.add` must be converted. + // Remember to be descriptive. + return global.throwPretty("Integer overflow while adding", .{}); + }; +} + +pub fn requiredAndOptionalArg(a: bool, b: ?usize, c: i32, d: ?u8) i32 { + const b_nonnull = b orelse { + return (123456 +% c) +% (d orelse 0); + }; + var math_result: i32 = @truncate(@as(isize, @as(u53, @truncate( + (b_nonnull +% @as(usize, @abs(c))) *% (d orelse 1), + )))); + if (a) { + math_result = -math_result; + } + return math_result; +} + +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 1ae5f4e00abf37..7188d1bb4bbbfe 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -32,6 +32,7 @@ #include "wtf/text/ASCIILiteral.h" #include "BunObject+exports.h" #include "ErrorCode.h" +#include "GeneratedBunObject.h" BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__lookup); BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolve); @@ -325,7 +326,7 @@ static JSValue constructBunShell(VM& vm, JSObject* bunObject) } auto* bunShell = shell.getObject(); - bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "braces"_s), 1, BunObject_callback_braces, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); + bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "braces"_s), 1, Generated::BunObject::jsBraces, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "escape"_s), 1, BunObject_callback_shellEscape, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); return bunShell; @@ -597,7 +598,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj fetch constructBunFetchObject ReadOnly|DontDelete|PropertyCallback file BunObject_callback_file DontDelete|Function 1 fileURLToPath functionFileURLToPath DontDelete|Function 1 - gc BunObject_callback_gc DontDelete|Function 1 + gc Generated::BunObject::jsGc DontDelete|Function 1 generateHeapSnapshot BunObject_callback_generateHeapSnapshot DontDelete|Function 1 gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1 gzipSync BunObject_callback_gzipSync DontDelete|Function 1 @@ -643,7 +644,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj stderr BunObject_getter_wrap_stderr DontDelete|PropertyCallback stdin BunObject_getter_wrap_stdin DontDelete|PropertyCallback stdout BunObject_getter_wrap_stdout DontDelete|PropertyCallback - stringWidth BunObject_callback_stringWidth DontDelete|Function 2 + stringWidth Generated::BunObject::jsStringWidth DontDelete|Function 2 unsafe BunObject_getter_wrap_unsafe DontDelete|PropertyCallback version constructBunVersion ReadOnly|DontDelete|PropertyCallback which BunObject_callback_which DontDelete|Function 1 diff --git a/src/bun.js/bindings/ObjectBindings.cpp b/src/bun.js/bindings/ObjectBindings.cpp index b5e633dcfcd3ec..1595199bd0ee02 100644 --- a/src/bun.js/bindings/ObjectBindings.cpp +++ b/src/bun.js/bindings/ObjectBindings.cpp @@ -1,4 +1,4 @@ -#include "root.h" +#include "ObjectBindings.h" #include #include #include @@ -54,7 +54,7 @@ static bool getNonIndexPropertySlotPrototypePollutionMitigation(JSC::VM& vm, JSO // Returns empty for exception, returns deleted if not found. // Be careful when handling the return value. -JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name) +JSC::JSValue getIfPropertyExistsPrototypePollutionMitigationUnsafe(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name) { auto scope = DECLARE_THROW_SCOPE(vm); auto propertySlot = PropertySlot(object, PropertySlot::InternalMethodType::Get); @@ -70,9 +70,20 @@ JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::VM& vm, JSC::J return value; } -JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name) +JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name) { - return getIfPropertyExistsPrototypePollutionMitigation(JSC::getVM(globalObject), globalObject, object, name); + auto scope = DECLARE_THROW_SCOPE(vm); + auto propertySlot = PropertySlot(object, PropertySlot::InternalMethodType::Get); + auto isDefined = getNonIndexPropertySlotPrototypePollutionMitigation(vm, object, globalObject, name, propertySlot); + + if (!isDefined) { + return JSC::jsUndefined(); + } + + scope.assertNoException(); + JSValue value = propertySlot.getValue(globalObject, name); + RETURN_IF_EXCEPTION(scope, {}); + return value; } } diff --git a/src/bun.js/bindings/ObjectBindings.h b/src/bun.js/bindings/ObjectBindings.h index 8c32283cbb4047..e32febca1d87bc 100644 --- a/src/bun.js/bindings/ObjectBindings.h +++ b/src/bun.js/bindings/ObjectBindings.h @@ -1,9 +1,8 @@ #pragma once +#include "root.h" namespace Bun { -JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name); - /** * This is `JSObject::getIfPropertyExists`, except it stops when it reaches globalObject->objectPrototype(). * @@ -12,5 +11,17 @@ JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::JSGlobalObject * This method also does not support index properties. */ JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name); +/** + * Same as `getIfPropertyExistsPrototypePollutionMitigation`, but uses + * JSValue::ValueDeleted instead of `JSC::jsUndefined` to encode the lack of a + * property. This is used by some JS bindings that want to distinguish between + * the property not existing and the property being undefined. + */ +JSC::JSValue getIfPropertyExistsPrototypePollutionMitigationUnsafe(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name); + +ALWAYS_INLINE JSC::JSValue getIfPropertyExistsPrototypePollutionMitigation(JSC::JSGlobalObject* globalObject, JSC::JSObject* object, const JSC::PropertyName& name) +{ + return getIfPropertyExistsPrototypePollutionMitigation(JSC::getVM(globalObject), globalObject, object, name); +} } diff --git a/src/bun.js/bindings/bindings-generator.zig b/src/bun.js/bindings/bindings-generator.zig deleted file mode 100644 index 3b9bb3dc9814e2..00000000000000 --- a/src/bun.js/bindings/bindings-generator.zig +++ /dev/null @@ -1,66 +0,0 @@ -const Bindings = @import("bindings.zig"); -const Exports = @import("exports.zig"); -const HeaderGen = @import("./header-gen.zig").HeaderGen; -const std = @import("std"); -const builtin = @import("builtin"); -const bun = @import("root").bun; -const io = std.io; -const fs = std.fs; -const process = std.process; -const ChildProcess = std.ChildProcess; -const Progress = std.Progress; -const mem = std.mem; -const testing = std.testing; -const Allocator = std.mem.Allocator; - -pub const bindgen = true; - -const JSC = bun.JSC; - -const Classes = JSC.GlobalClasses; - -pub fn main() anyerror!void { - const allocator = std.heap.c_allocator; - const src: std.builtin.SourceLocation = @src(); - const src_path = comptime bun.Environment.base_path ++ std.fs.path.dirname(src.file).?; - { - const paths = [_][]const u8{ src_path, "headers.h" }; - const paths2 = [_][]const u8{ src_path, "headers-cpp.h" }; - const paths4 = [_][]const u8{ src_path, "ZigGeneratedCode.cpp" }; - - const cpp = try std.fs.createFileAbsolute(try std.fs.path.join(allocator, &paths2), .{}); - const file = try std.fs.createFileAbsolute(try std.fs.path.join(allocator, &paths), .{}); - const generated = try std.fs.createFileAbsolute(try std.fs.path.join(allocator, &paths4), .{}); - - const HeaderGenerator = HeaderGen( - Bindings, - Exports, - "src/bun.js/bindings/bindings.zig", - ); - HeaderGenerator.exec(HeaderGenerator{}, file, cpp, generated); - } - // TODO: finish this - const use_cpp_generator = false; - if (use_cpp_generator) { - comptime var i: usize = 0; - inline while (i < Classes.len) : (i += 1) { - const Class = Classes[i]; - const paths = [_][]const u8{ src_path, Class.name ++ ".generated.h" }; - const headerFilePath = try std.fs.path.join( - allocator, - &paths, - ); - const implFilePath = try std.fs.path.join( - allocator, - &[_][]const u8{ std.fs.path.dirname(src.file) orelse return error.BadPath, Class.name ++ ".generated.cpp" }, - ); - var headerFile = try std.fs.createFileAbsolute(headerFilePath, .{}); - const header_writer = headerFile.writer(); - var implFile = try std.fs.createFileAbsolute(implFilePath, .{}); - try Class.@"generateC++Header"(header_writer); - try Class.@"generateC++Class"(implFile.writer()); - headerFile.close(); - implFile.close(); - } - } -} diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 7ff4573359c600..7305ad8930e137 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1426,7 +1426,7 @@ bool Bun__deepMatch(JSValue objValue, JSValue subsetValue, JSGlobalObject* globa } if (subsetProp.isObject() and prop.isObject()) { - // if this is called from inside an objectContaining asymmetric matcher, it should behave slighlty differently: + // if this is called from inside an objectContaining asymmetric matcher, it should behave slightly differently: // in such case, it expects exhaustive matching of any nested object properties, not just a subset, // and the user would need to opt-in to subset matching by using another nested objectContaining matcher if (enableAsymmetricMatchers && isMatchingObjectContaining) { @@ -3202,7 +3202,7 @@ void JSC__JSPromise__resolve(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, arg0->resolve(arg1, JSC::JSValue::decode(JSValue2)); } -// This implementation closely mimicks the one in JSC::JSPromise::resolve +// This implementation closely mimics the one in JSC::JSPromise::resolve void JSC__JSPromise__resolveOnNextTick(JSC__JSPromise* promise, JSC__JSGlobalObject* lexicalGlobalObject, JSC__JSValue encoedValue) { @@ -3223,7 +3223,7 @@ bool JSC__JSValue__isAnyError(JSC__JSValue JSValue0) return type == JSC::ErrorInstanceType; } -// This implementation closely mimicks the one in JSC::JSPromise::reject +// This implementation closely mimics the one in JSC::JSPromise::reject void JSC__JSPromise__rejectOnNextTickWithHandled(JSC__JSPromise* promise, JSC__JSGlobalObject* lexicalGlobalObject, JSC__JSValue encoedValue, bool handled) { @@ -3802,12 +3802,12 @@ JSC__JSValue JSC__JSValue__getIfPropertyExistsImpl(JSC__JSValue JSValue0, 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 + // 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 })); const auto identifier = JSC::Identifier::fromString(vm, propertyString); const auto property = JSC::PropertyName(identifier); - return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, globalObject, object, property)); + return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigationUnsafe(vm, globalObject, object, property)); } extern "C" JSC__JSValue JSC__JSValue__getOwn(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, BunString* propertyName) @@ -4886,7 +4886,7 @@ void JSC__Exception__getStackTrace(JSC__Exception* arg0, ZigStackTrace* trace) #pragma mark - JSC::VM -JSC__JSValue JSC__VM__runGC(JSC__VM* vm, bool sync) +size_t JSC__VM__runGC(JSC__VM* vm, bool sync) { JSC::JSLockHolder lock(vm); @@ -4919,7 +4919,7 @@ JSC__JSValue JSC__VM__runGC(JSC__VM* vm, bool sync) } #endif - return JSC::JSValue::encode(JSC::jsNumber(vm->heap.sizeAfterLastFullCollection())); + return vm->heap.sizeAfterLastFullCollection(); } bool JSC__VM__isJITEnabled() { return JSC::Options::useJIT(); } @@ -5187,7 +5187,7 @@ JSC__JSValue JSC__JSValue__fastGet(JSC__JSValue JSValue0, JSC__JSGlobalObject* g auto& vm = globalObject->vm(); const auto property = JSC::PropertyName(builtinNameMap(vm, arg2)); - return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, globalObject, object, property)); + return JSC::JSValue::encode(Bun::getIfPropertyExistsPrototypePollutionMitigationUnsafe(vm, globalObject, object, property)); } extern "C" JSC__JSValue JSC__JSValue__fastGetOwn(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, unsigned char arg2) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 87170e0040a9a0..173add280230e9 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6327,7 +6327,7 @@ pub const VM = extern struct { }); } - pub fn runGC(vm: *VM, sync: bool) JSValue { + pub fn runGC(vm: *VM, sync: bool) usize { return cppFn("runGC", .{ vm, sync, diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 43a5dd50a5f119..f3896c63efb8da 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -426,7 +426,7 @@ CPP_DECL void JSC__VM__notifyNeedShellTimeoutCheck(JSC__VM* arg0); CPP_DECL void JSC__VM__notifyNeedTermination(JSC__VM* arg0); CPP_DECL void JSC__VM__notifyNeedWatchdogCheck(JSC__VM* arg0); CPP_DECL void JSC__VM__releaseWeakRefs(JSC__VM* arg0); -CPP_DECL JSC__JSValue JSC__VM__runGC(JSC__VM* arg0, bool arg1); +CPP_DECL size_t JSC__VM__runGC(JSC__VM* arg0, bool arg1); CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* arg0, bool arg1); CPP_DECL void JSC__VM__setExecutionForbidden(JSC__VM* arg0, bool arg1); CPP_DECL void JSC__VM__setExecutionTimeLimit(JSC__VM* arg0, double arg1); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 0a3910de08fcfe..5836a77370f160 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -229,7 +229,6 @@ pub extern fn JSC__JSValue__bigIntSum(arg0: *bindings.JSGlobalObject, arg1: JSC_ pub extern fn JSC__JSValue__getClassName(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: [*c]ZigString) void; pub extern fn JSC__JSValue__getErrorsProperty(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__JSValue__getIfPropertyExistsFromPath(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) JSC__JSValue; -pub extern fn JSC__JSValue__getIfPropertyExistsImpl(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: [*c]const u8, arg3: u32) JSC__JSValue; pub extern fn JSC__JSValue__getLengthIfPropertyExistsInternal(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject) f64; pub extern fn JSC__JSValue__getNameProperty(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: [*c]ZigString) void; pub extern fn JSC__JSValue__getPrototype(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject) JSC__JSValue; @@ -321,7 +320,7 @@ pub extern fn JSC__VM__notifyNeedShellTimeoutCheck(arg0: *bindings.VM) void; pub extern fn JSC__VM__notifyNeedTermination(arg0: *bindings.VM) void; pub extern fn JSC__VM__notifyNeedWatchdogCheck(arg0: *bindings.VM) void; pub extern fn JSC__VM__releaseWeakRefs(arg0: *bindings.VM) void; -pub extern fn JSC__VM__runGC(arg0: *bindings.VM, arg1: bool) JSC__JSValue; +pub extern fn JSC__VM__runGC(arg0: *bindings.VM, arg1: bool) usize; pub extern fn JSC__VM__setControlFlowProfiler(arg0: *bindings.VM, arg1: bool) void; pub extern fn JSC__VM__setExecutionForbidden(arg0: *bindings.VM, arg1: bool) void; pub extern fn JSC__VM__setExecutionTimeLimit(arg0: *bindings.VM, arg1: f64) void; diff --git a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h index 224922f1b80eed..ce0c5d6904d51c 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h @@ -32,6 +32,7 @@ namespace WebCore { // Specialized by generated code for IDL enumeration conversion. +template std::optional parseEnumerationFromString(const String&); template std::optional parseEnumeration(JSC::JSGlobalObject&, JSC::JSValue); template ASCIILiteral expectedEnumerationValues(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 7fc7ce4903e2ea..5d36c6c718e547 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1244,14 +1244,14 @@ pub const VirtualMachine = struct { return this.bundler.getPackageManager(); } - pub fn garbageCollect(this: *const VirtualMachine, sync: bool) JSValue { + pub fn garbageCollect(this: *const VirtualMachine, sync: bool) usize { @setCold(true); Global.mimalloc_cleanup(false); if (sync) return this.global.vm().runGC(true); this.global.vm().collectAsync(); - return JSValue.jsNumber(this.global.vm().heapSize()); + return this.global.vm().heapSize(); } pub inline fn autoGarbageCollect(this: *const VirtualMachine) void { diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index df80522e280979..c8c6c19393647c 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2462,6 +2462,15 @@ pub const ModuleLoader = struct { return jsSyntheticModule(.InternalForTesting, specifier); }, + .@"internal/test/binding" => { + if (!Environment.isDebug) { + if (!is_allowed_to_use_internal_testing_apis) + return null; + } + + return jsSyntheticModule(.@"internal:test/binding", specifier); + }, + // These are defined in src/js/* .@"bun:ffi" => return jsSyntheticModule(.@"bun:ffi", specifier), .@"bun:sql" => { @@ -2675,7 +2684,6 @@ pub const HardcodedModule = enum { @"bun:test", // usually replaced by the transpiler but `await import("bun:" + "test")` has to work @"bun:sql", @"bun:sqlite", - @"bun:internal-for-testing", @"detect-libc", @"node:assert", @"node:assert/strict", @@ -2736,6 +2744,9 @@ pub const HardcodedModule = enum { @"node:diagnostics_channel", @"node:dgram", @"node:cluster", + // these are gated behind '--expose-internals' + @"bun:internal-for-testing", + @"internal/test/binding", /// Already resolved modules go in here. /// This does not remap the module name, it is just a hash table. @@ -2815,6 +2826,8 @@ pub const HardcodedModule = enum { .{ "@vercel/fetch", HardcodedModule.@"@vercel/fetch" }, .{ "utf-8-validate", HardcodedModule.@"utf-8-validate" }, .{ "abort-controller", HardcodedModule.@"abort-controller" }, + + .{ "internal/test/binding", HardcodedModule.@"internal/test/binding" }, }, ); @@ -2956,6 +2969,8 @@ pub const HardcodedModule = enum { .{ "next/dist/compiled/ws", .{ .path = "ws" } }, .{ "next/dist/compiled/node-fetch", .{ .path = "node-fetch" } }, .{ "next/dist/compiled/undici", .{ .path = "undici" } }, + + .{ "internal/test/binding", .{ .path = "internal/test/binding" } }, }; const bun_extra_alias_kvs = .{ diff --git a/src/bun.zig b/src/bun.zig index 352c6c9148562c..efac52b91b14ad 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -11,7 +11,7 @@ const bun = @This(); pub const Environment = @import("env.zig"); -pub const use_mimalloc = !Environment.isTest; +pub const use_mimalloc = true; pub const default_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator @@ -115,6 +115,12 @@ pub const fmt = @import("./fmt.zig"); pub const allocators = @import("./allocators.zig"); pub const bun_js = @import("./bun_js.zig"); +/// All functions and interfaces provided from Bun's `bindgen` utility. +pub const gen = @import("bun.js/bindings/GeneratedBindings.zig"); +comptime { + _ = &gen; // reference bindings +} + /// Copied from Zig std.trait pub const trait = @import("./trait.zig"); /// Copied from Zig std.Progress before 0.13 rewrite @@ -1275,7 +1281,7 @@ pub const SignalCode = enum(u8) { return @enumFromInt(std.mem.asBytes(&value)[0]); } - // This wrapper struct is lame, what if bun's color formatter was more versitile + // This wrapper struct is lame, what if bun's color formatter was more versatile const Fmt = struct { signal: SignalCode, enable_ansi_colors: bool, @@ -2758,8 +2764,6 @@ pub const MakePath = struct { @ptrCast(component.path)) else try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - _ = is_last; // autofix var result = makeOpenDirAccessMaskW(self, sub_path_w.span().ptr, access_mask, .{ .no_follow = no_follow, .create_disposition = w.FILE_OPEN_IF, diff --git a/src/cli.zig b/src/cli.zig index 42b931c360e07e..17adb70c0480b2 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -230,6 +230,7 @@ pub const Arguments = struct { clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, + clap.parseParam("--expose-internals Expose internals used for testing Bun itself. Usage of these APIs are completely unsupported.") catch unreachable, }; const auto_or_run_params = [_]ParamType{ @@ -775,6 +776,10 @@ pub const Arguments = struct { bun.JSC.RuntimeTranspilerCache.is_disabled = true; } + + if (args.flag("--expose-internals")) { + bun.JSC.ModuleLoader.is_allowed_to_use_internal_testing_apis = true; + } } if (opts.port != null and opts.origin == null) { diff --git a/src/codegen/bake-codegen.ts b/src/codegen/bake-codegen.ts index 22d389ce77f374..c38993061c9b27 100644 --- a/src/codegen/bake-codegen.ts +++ b/src/codegen/bake-codegen.ts @@ -1,26 +1,15 @@ import assert from "node:assert"; import { existsSync, writeFileSync, rmSync, readFileSync } from "node:fs"; -import { watch } from "node:fs/promises"; import { basename, join } from "node:path"; +import { argParse } from "./helpers"; // arg parsing -const options = {}; -for (const arg of process.argv.slice(2)) { - if (!arg.startsWith("--")) { - console.error("Unknown argument " + arg); - process.exit(1); - } - const split = arg.split("="); - const value = split[1] || "true"; - options[split[0].slice(2)] = value; -} - -let { codegen_root, debug, live } = options as any; -if (!codegen_root) { - console.error("Missing --codegen_root=..."); +let { "codegen-root": codegenRoot, debug, ...rest } = argParse(["codegen-root", "debug"]); +if (debug === "false" || debug === "0" || debug == "OFF") debug = false; +if (!codegenRoot) { + console.error("Missing --codegen-root=..."); process.exit(1); } -if (debug === "false" || debug === "0" || debug == "OFF") debug = false; const base_dir = join(import.meta.dirname, "../bake"); process.chdir(base_dir); // to make bun build predictable in development @@ -53,7 +42,7 @@ async function run() { minify: { syntax: !debug, }, - target: side === 'server' ? 'bun' : 'browser', + target: side === "server" ? "bun" : "browser", }); if (!result.success) throw new AggregateError(result.logs); assert(result.outputs.length === 1, "must bundle to a single file"); @@ -92,8 +81,10 @@ async function run() { rmSync(generated_entrypoint); - if (code.includes('export default ')) { - throw new AggregateError([new Error('export default is not allowed in bake codegen. this became a commonjs module!')]); + if (code.includes("export default ")) { + throw new AggregateError([ + new Error("export default is not allowed in bake codegen. this became a commonjs module!"), + ]); } if (file !== "error") { @@ -119,13 +110,13 @@ async function run() { if (code[code.length - 1] === ";") code = code.slice(0, -1); if (side === "server") { - code = debug - ? `${code} return ${outName('server_exports')};\n` - : `${code};return ${outName('server_exports')};`; + code = debug + ? `${code} return ${outName("server_exports")};\n` + : `${code};return ${outName("server_exports")};`; - const params = `${outName('$separateSSRGraph')},${outName('$importMeta')}`; - code = code.replaceAll('import.meta', outName('$importMeta')); - code = `let ${outName('input_graph')}={},${outName('config')}={separateSSRGraph:${outName('$separateSSRGraph')}},${outName('server_exports')};${code}`; + const params = `${outName("$separateSSRGraph")},${outName("$importMeta")}`; + code = code.replaceAll("import.meta", outName("$importMeta")); + code = `let ${outName("input_graph")}={},${outName("config")}={separateSSRGraph:${outName("$separateSSRGraph")}},${outName("server_exports")};${code}`; code = debug ? `((${params}) => {${code}})\n` : `((${params})=>{${code}})\n`; } else { @@ -133,7 +124,7 @@ async function run() { } } - writeFileSync(join(codegen_root, `bake.${file}.js`), code); + writeFileSync(join(codegenRoot, `bake.${file}.js`), code); }), ); @@ -174,26 +165,12 @@ async function run() { console.error(`Errors while bundling Bake ${kind.map(x => map[x]).join(" and ")}:`); console.error(err); } - if (!live) process.exit(1); } else { console.log("-> bake.client.js, bake.server.js, bake.error.js"); - const empty_file = join(codegen_root, "bake_empty_file"); + const empty_file = join(codegenRoot, "bake_empty_file"); if (!existsSync(empty_file)) writeFileSync(empty_file, "this is used to fulfill a cmake dependency"); } } await run(); - -if (live) { - const watcher = watch(base_dir, { recursive: true }) as any; - for await (const event of watcher) { - if (event.filename.endsWith(".zig")) continue; - if (event.filename.startsWith(".")) continue; - try { - await run(); - } catch (e) { - console.log(e); - } - } -} diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts new file mode 100644 index 00000000000000..3a47ad66634a9f --- /dev/null +++ b/src/codegen/bindgen-lib-internal.ts @@ -0,0 +1,1045 @@ +// While working on this file, it is important to have very rigorous errors +// and checking on input data. The goal is to allow people not aware of +// various footguns in JavaScript, C++, and the bindings generator to +// always produce correct code, or bail with an error. +import { expect } from "bun:test"; +import type { FuncOptions, Type, t } from "./bindgen-lib"; +import * as path from "node:path"; +import assert from "node:assert"; + +export const src = path.join(import.meta.dirname, "../"); + +export type TypeKind = keyof typeof t; + +export let allFunctions: Func[] = []; +export let files = new Map(); +/** A reachable type is one that is required for code generation */ +export let typeHashToReachableType = new Map(); +export let typeHashToStruct = new Map(); +export let typeHashToNamespace = new Map(); +export let structHashToSelf = new Map(); + +/** String literal */ +export const str = (v: any) => JSON.stringify(v); +/** Capitalize */ +export const cap = (s: string) => s[0].toUpperCase() + s.slice(1); +/** Escape a Zig Identifier */ +export const zid = (s: string) => (s.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) ? s : "@" + str(s)); +/** Snake Case */ +export const snake = (s: string) => + s[0].toLowerCase() + + s + .slice(1) + .replace(/([A-Z])/g, "_$1") + .replace(/-/g, "_") + .toLowerCase(); +/** Camel Case */ +export const camel = (s: string) => + s[0].toLowerCase() + s.slice(1).replace(/[_-](\w)?/g, (_, letter) => letter?.toUpperCase() ?? ""); +/** Pascal Case */ +export const pascal = (s: string) => cap(camel(s)); + +// Return symbol names of extern values (must be equivalent between C++ and Zig) + +/** The JS Host function, aka fn (*JSC.JSGlobalObject, *JSC.CallFrame) JSValue.MaybeException */ +export const extJsFunction = (namespaceVar: string, fnLabel: string) => + `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}`; +/** Each variant gets a dispatcher function. */ +export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) => + `bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`; + +interface TypeDataDefs { + /** The name */ + ref: string; + + sequence: { + element: TypeImpl; + repr: "slice"; + }; + record: { + value: TypeImpl; + repr: "kv-slices"; + }; + zigEnum: { + file: string; + impl: string; + }; + stringEnum: string[]; + oneOf: TypeImpl[]; + dictionary: DictionaryField[]; +} +type TypeData = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any; + +interface Flags { + optional?: boolean; + required?: boolean; + nullable?: boolean; + default?: any; + range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"]; +} + +export interface DictionaryField { + key: string; + type: TypeImpl; +} + +export declare const isType: unique symbol; + +/** + * Implementation of the Type interface. All types are immutable and hashable. + * Hashes de-duplicate structure and union definitions. Flags do not account for + * the hash, so `oneOf(A, B)` and `oneOf(A, B).optional` will point to the same + * generated struct type, the purpose of the flags are to inform receivers like + * `t.dictionary` and `fn` to mark uses as optional or provide default values. + */ +export class TypeImpl { + kind: K; + data: TypeData; + flags: Flags; + /** Access via .name(). */ + nameDeduplicated: string | null | undefined = undefined; + /** Access via .hash() */ + #hash: string | undefined = undefined; + ownerFile: string; + + declare [isType]: true; + + constructor(kind: K, data: TypeData, flags: Flags = {}) { + this.kind = kind; + this.data = data; + this.flags = flags; + this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts"); + } + + isVirtualArgument() { + return this.kind === "globalObject" || this.kind === "zigVirtualMachine"; + } + + hash() { + if (this.#hash) { + return this.#hash; + } + let h = `${this.kind}:`; + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + h += this.data.element.hash(); + break; + case "record": + h += this.data.value.hash(); + break; + case "zigEnum": + h += `${this.data.file}:${this.data.impl}`; + break; + case "stringEnum": + h += this.data.join(","); + break; + case "oneOf": + h += this.data.map(t => t.hash()).join(","); + break; + case "dictionary": + h += this.data.map(({ key, required, type }) => `${key}:${required}:${type.hash()}`).join(","); + break; + } + let hash = String(Bun.hash(h)); + this.#hash = hash; + return hash; + } + + /** + * If this type lowers to a named type (struct, union, enum) + */ + lowersToNamedType() { + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + case "record": + case "oneOf": + case "dictionary": + case "stringEnum": + case "zigEnum": + return true; + default: + return false; + } + } + + canDirectlyMapToCAbi(): CAbiType | null { + let kind = this.kind; + switch (kind) { + case "ref": + throw new Error("TODO"); + case "any": + return "JSValue"; + case "ByteString": + case "DOMString": + case "USVString": + case "UTF8String": + return "bun.String"; + case "boolean": + return "bool"; + case "strictBoolean": + return "bool"; + case "f64": + case "i8": + case "i16": + case "i32": + case "i64": + case "u8": + case "u16": + case "u32": + case "u64": + case "usize": + return kind; + case "globalObject": + case "zigVirtualMachine": + return "*JSGlobalObject"; + case "stringEnum": + return cAbiTypeForEnum(this.data.length); + case "zigEnum": + throw new Error("TODO"); + case "undefined": + return "u0"; + case "oneOf": // `union(enum)` + case "UTF8String": // []const u8 + case "record": // undecided how to lower records + case "sequence": // []const T + return null; + case "externalClass": + throw new Error("TODO"); + return "*anyopaque"; + case "dictionary": { + let existing = typeHashToStruct.get(this.hash()); + if (existing) return existing; + existing = new Struct(); + for (const { key, type } of this.data as DictionaryField[]) { + if (type.flags.optional && !("default" in type.flags)) { + return null; // ?T + } + const repr = type.canDirectlyMapToCAbi(); + if (!repr) return null; + + existing.add(key, repr); + } + existing.reorderForSmallestSize(); + if (!structHashToSelf.has(existing.hash())) { + structHashToSelf.set(existing.hash(), existing); + } + existing.assignName(this.name()); + typeHashToStruct.set(this.hash(), existing); + return existing; + } + case "sequence": { + return null; + } + default: { + throw new Error("unexpected: " + (kind satisfies never)); + } + } + } + + name() { + if (this.nameDeduplicated) { + return this.nameDeduplicated; + } + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + if (existing) return (this.nameDeduplicated = existing.nameDeduplicated ??= this.#generateName()); + return (this.nameDeduplicated = `anon_${this.kind}_${hash}`); + } + + cppInternalName() { + const name = this.name(); + const cAbiType = this.canDirectlyMapToCAbi(); + const namespace = typeHashToNamespace.get(this.hash()); + if (cAbiType) { + if (typeof cAbiType === "string") { + return cAbiType; + } + } + return namespace ? `${namespace}${name}` : name; + } + + cppClassName() { + assert(this.lowersToNamedType()); + const name = this.name(); + const namespace = typeHashToNamespace.get(this.hash()); + return namespace ? `${namespace}::${cap(name)}` : name; + } + + cppName() { + const name = this.name(); + const cAbiType = this.canDirectlyMapToCAbi(); + const namespace = typeHashToNamespace.get(this.hash()); + if (cAbiType && typeof cAbiType === "string" && this.kind !== "zigEnum" && this.kind !== "stringEnum") { + return cAbiTypeName(cAbiType); + } + return namespace ? `${namespace}::${cap(name)}` : name; + } + + #generateName() { + return `bindgen_${this.ownerFile}_${this.hash()}`; + } + + /** + * Name assignment is done to give readable names. + * The first name to a unique hash wins. + */ + assignName(name: string) { + if (this.nameDeduplicated) return; + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + if (existing) { + this.nameDeduplicated = existing.nameDeduplicated ??= name; + return; + } + this.nameDeduplicated = name; + } + + markReachable() { + if (!this.lowersToNamedType()) return; + const hash = this.hash(); + const existing = typeHashToReachableType.get(hash); + this.nameDeduplicated ??= existing?.name() ?? `anon_${this.kind}_${hash}`; + if (!existing) typeHashToReachableType.set(hash, this); + + switch (this.kind) { + case "ref": + throw new Error("TODO"); + case "sequence": + this.data.element.markReachable(); + break; + case "record": + this.data.value.markReachable(); + break; + case "oneOf": + for (const type of this.data as TypeImpl[]) { + type.markReachable(); + } + break; + case "dictionary": + for (const { type } of this.data as DictionaryField[]) { + type.markReachable(); + } + break; + } + } + + // Interface definition API + get optional() { + if (this.flags.required) { + throw new Error("Cannot derive optional on a required type"); + } + if (this.flags.default) { + throw new Error("Cannot derive optional on a something with a default value (default implies optional)"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + optional: true, + }); + } + + get nullable() { + return new TypeImpl(this.kind, this.data, { + ...this.flags, + nullable: true, + }); + } + + get required() { + if (this.flags.required) { + throw new Error("This type already has required set"); + } + if (this.flags.required) { + throw new Error("Cannot derive required on an optional type"); + } + return new TypeImpl(this.kind, this.data, { + ...this.flags, + required: true, + }); + } + + clamp(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "clamp"); + } + + enforceRange(min?: number | bigint, max?: number | bigint) { + return this.#rangeModifier(min, max, "enforce"); + } + + #rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") { + if (this.flags.range) { + throw new Error("This type already has a range modifier set"); + } + + // cAbiIntegerLimits throws on non-integer types + const range = cAbiIntegerLimits(this.kind as CAbiType); + const abiMin = BigInt(range[0]); + const abiMax = BigInt(range[1]); + if (min === undefined) { + min = abiMin; + max = abiMax; + } else { + if (max === undefined) { + throw new Error("Expected min and max to be both set or both unset"); + } + min = BigInt(min); + max = BigInt(max); + + if (min < abiMin || min > abiMax) { + throw new Error(`Expected integer in range ${range}, got ${inspect(min)}`); + } + if (max < abiMin || max > abiMax) { + throw new Error(`Expected integer in range ${range}, got ${inspect(max)}`); + } + if (min > max) { + throw new Error(`Expected min <= max, got ${inspect(min)} > ${inspect(max)}`); + } + } + + return new TypeImpl(this.kind, this.data, { + ...this.flags, + range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max], + }); + } + + assertDefaultIsValid(value: unknown) { + switch (this.kind) { + case "DOMString": + case "ByteString": + case "USVString": + case "UTF8String": + if (typeof value !== "string") { + throw new Error(`Expected string, got ${inspect(value)}`); + } + break; + case "boolean": + if (typeof value !== "boolean") { + throw new Error(`Expected boolean, got ${inspect(value)}`); + } + break; + case "f64": + if (typeof value !== "number") { + throw new Error(`Expected number, got ${inspect(value)}`); + } + break; + case "usize": + case "u8": + case "u16": + case "u32": + case "u64": + case "i8": + case "i16": + case "i32": + case "i64": + const range = this.flags.range?.slice(1) ?? cAbiIntegerLimits(this.kind); + if (typeof value === "number") { + if (value % 1 !== 0) { + throw new Error(`Expected integer, got ${inspect(value)}`); + } + if (value >= Number.MAX_SAFE_INTEGER || value <= Number.MIN_SAFE_INTEGER) { + throw new Error( + `Specify default ${this.kind} outside of max safe integer range as a BigInt to avoid precision loss`, + ); + } + if (value < Number(range[0]) || value > Number(range[1])) { + throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); + } + } else if (typeof value === "bigint") { + if (value < BigInt(range[0]) || value > BigInt(range[1])) { + throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); + } + } else { + throw new Error(`Expected integer, got ${inspect(value)}`); + } + break; + case "dictionary": + if (typeof value !== "object" || value === null) { + throw new Error(`Expected object, got ${inspect(value)}`); + } + for (const { key, type } of this.data as DictionaryField[]) { + if (key in value) { + type.assertDefaultIsValid(value[key]); + } else if (type.flags.required) { + throw new Error(`Missing key ${key} in dictionary`); + } + } + break; + default: + throw new Error(`TODO: set default value on type ${this.kind}`); + } + } + + emitCppDefaultValue(w: CodeWriter) { + const value = this.flags.default; + switch (this.kind) { + case "boolean": + w.add(value ? "true" : "false"); + break; + case "f64": + w.add(String(value)); + break; + case "usize": + case "u8": + case "u16": + case "u32": + case "u64": + case "i8": + case "i16": + case "i32": + case "i64": + w.add(String(value)); + break; + case "dictionary": + const struct = this.structType(); + w.line(`${this.cppName()} {`); + w.indent(); + for (const { name } of struct.fields) { + w.add(`.${name} = `); + const type = this.data.find(f => f.key === name)!.type; + type.emitCppDefaultValue(w); + w.line(","); + } + w.dedent(); + w.add(`}`); + break; + case "DOMString": + case "ByteString": + case "USVString": + case "UTF8String": + if (typeof value === "string") { + w.add("Bun::BunStringEmpty"); + } else { + throw new Error(`TODO: non-empty string default`); + } + break; + default: + throw new Error(`TODO: set default value on type ${this.kind}`); + } + } + + structType() { + const direct = this.canDirectlyMapToCAbi(); + assert(typeof direct !== "string"); + if (direct) return direct; + throw new Error("TODO: generate non-extern struct for representing this data type"); + } + + default(def: any) { + if ("default" in this.flags) { + throw new Error("This type already has a default value"); + } + if (this.flags.required) { + throw new Error("Cannot derive default on a required type"); + } + this.assertDefaultIsValid(def); + return new TypeImpl(this.kind, this.data, { + ...this.flags, + default: def, + }); + } + + [Symbol.toStringTag] = "Type"; + [Bun.inspect.custom](depth, options, inspect) { + return ( + `${options.stylize("Type", "special")} ${ + this.nameDeduplicated ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " " : "" + }${options.stylize( + `[${this.kind}${["required", "optional", "nullable"] + .filter(k => this.flags[k]) + .map(x => ", " + x) + .join("")}]`, + "regexp", + )}` + + (this.data + ? " " + + inspect(this.data, { + ...options, + depth: options.depth === null ? null : options.depth - 1, + }).replace(/\n/g, "\n") + : "") + ); + } +} + +function cAbiIntegerLimits(type: CAbiType) { + switch (type) { + case "u8": + return [0, 255]; + case "u16": + return [0, 65535]; + case "u32": + return [0, 4294967295]; + case "u64": + return [0, 18446744073709551615n]; + case "usize": + return [0, 18446744073709551615n]; + case "i8": + return [-128, 127]; + case "i16": + return [-32768, 32767]; + case "i32": + return [-2147483648, 2147483647]; + case "i64": + return [-9223372036854775808n, 9223372036854775807n]; + default: + throw new Error(`Unexpected type ${type}`); + } +} + +export function cAbiTypeForEnum(length: number): CAbiType { + return ("u" + alignForward(length, 8)) as CAbiType; +} + +export function inspect(value: any) { + return Bun.inspect(value, { colors: Bun.enableANSIColors }); +} + +export function oneOfImpl(types: TypeImpl[]): TypeImpl { + const out: TypeImpl[] = []; + for (const type of types) { + if (type.kind === "oneOf") { + out.push(...type.data); + } else { + if (type.flags.nullable) { + throw new Error("Union type cannot include nullable"); + } + if (type.flags.default) { + throw new Error( + "Union type cannot include a default value. Instead, set a default value on the union type itself", + ); + } + if (type.isVirtualArgument()) { + throw new Error(`t.${type.kind} can only be used as a function argument type`); + } + out.push(type); + } + } + return new TypeImpl("oneOf", out); +} + +export function dictionaryImpl(record: Record): TypeImpl { + const out: DictionaryField[] = []; + for (const key in record) { + const type = record[key]; + if (type.isVirtualArgument()) { + throw new Error(`t.${type.kind} can only be used as a function argument type`); + } + out.push({ + key, + type: type, + }); + } + return new TypeImpl("dictionary", out); +} + +export const isFunc = Symbol("isFunc"); + +export interface Func { + [isFunc]: true; + name: string; + zigPrefix: string; + snapshot: string; + zigFile: string; + variants: Variant[]; +} + +export interface Variant { + suffix: string; + args: Arg[]; + ret: TypeImpl; + returnStrategy?: ReturnStrategy; + argStruct?: Struct; + globalObjectArg?: number | "hidden"; + minRequiredArgs: number; + communicationStruct?: Struct; +} + +export interface Arg { + name: string; + type: TypeImpl; + loweringStrategy?: ArgStrategy; + zigMappedName?: string; +} + +/** + * The strategy for moving arguments over the ABI boundary are computed before + * any code is generated so that the proper definitions can be easily made, + * while allow new special cases to be added. + */ +export type ArgStrategy = + // The argument is communicated as a C parameter + | { type: "c-abi-pointer"; abiType: CAbiType } + // The argument is communicated as a C parameter + | { type: "c-abi-value"; abiType: CAbiType } + // The data is added as a field on `.communicationStruct` + | { + type: "uses-communication-buffer"; + /** + * Unique prefix for fields. For example, moving an optional over the ABI + * boundary uses two fields, `bool {prefix}_set` and `T {prefix}_value`. + */ + prefix: string; + /** + * For compound complex types, such as `?union(enum) { a: u32, b: + * bun.String }`, the child item is assigned the prefix + * `{prefix_of_optional}_value`. The interpretation of this array depends + * on `arg.type.kind`. + */ + children: ArgStrategyChildItem[]; + }; + +export type ArgStrategyChildItem = + | { + type: "c-abi-compatible"; + abiType: CAbiType; + } + | { + type: "uses-communication-buffer"; + prefix: string; + children: ArgStrategyChildItem[]; + }; +/** + * In addition to moving a payload over, an additional bit of information + * crosses the ABI boundary indicating if the function threw an exception. + * + * For simplicity, the possibility of any Zig binding returning an error/calling + * `throw` is assumed and there isnt a way to disable the exception check. + */ +export type ReturnStrategy = + // JSValue is special cased because it encodes exception as 0x0 + | { type: "jsvalue" } + // For primitives and simple structures where direct assignment into a + // pointer is possible. function returns a boolean indicating success/error. + | { type: "basic-out-param"; abiType: CAbiType }; + +export interface File { + functions: Func[]; + typedefs: TypeDef[]; +} + +export interface TypeDef { + name: string; + type: TypeImpl; +} + +export function registerFunction(opts: FuncOptions) { + const snapshot = snapshotCallerLocation(); + const filename = stackTraceFileName(snapshot); + expect(filename).toEndWith(".bind.ts"); + const zigFile = path.relative(src, filename.replace(/\.bind\.ts$/, ".zig")); + let file = files.get(zigFile); + if (!file) { + file = { functions: [], typedefs: [] }; + files.set(zigFile, file); + } + const variants: Variant[] = []; + if ("variants" in opts) { + let i = 1; + for (const variant of opts.variants) { + const { minRequiredArgs } = validateVariant(variant); + variants.push({ + ...variant, + suffix: `${i}`, + minRequiredArgs, + } as unknown as Variant); + i++; + } + } else { + const { minRequiredArgs } = validateVariant(opts); + variants.push({ + suffix: "", + args: Object.entries(opts.args).map(([name, type]) => ({ name, type })) as Arg[], + ret: opts.ret as TypeImpl, + minRequiredArgs, + }); + } + + const func: Func = { + [isFunc]: true, + name: "", + zigPrefix: opts.implNamespace ? `${opts.implNamespace}.` : "", + snapshot, + zigFile, + variants, + }; + allFunctions.push(func); + file.functions.push(func); + return func; +} + +function validateVariant(variant: any) { + let minRequiredArgs = 0; + let seenOptionalArgument = false; + let i = 0; + + for (const [name, type] of Object.entries(variant.args) as [string, TypeImpl][]) { + if (!(type instanceof TypeImpl)) { + throw new Error(`Expected type for argument ${name}, got ${inspect(type)}`); + } + i += 1; + if (type.isVirtualArgument()) { + continue; + } + if (!type.flags.optional && !("default" in type.flags)) { + if (seenOptionalArgument) { + throw new Error(`Required argument ${name} cannot follow an optional argument`); + } + minRequiredArgs++; + } else { + seenOptionalArgument = true; + } + } + + return { minRequiredArgs }; +} + +function snapshotCallerLocation(): string { + const stack = new Error().stack!; + const lines = stack.split("\n"); + let i = 1; + for (; i < lines.length; i++) { + if (!lines[i].includes(import.meta.dir)) { + return lines[i]; + } + } + throw new Error("Couldn't find caller location in stack trace"); +} + +function stackTraceFileName(line: string): string { + return / \(((?:[A-Za-z]:)?.*?)[:)]/.exec(line)![1].replaceAll("\\", "/"); +} + +export type CAbiType = + | "*anyopaque" + | "*JSGlobalObject" + | "JSValue" + | "JSValue.MaybeException" + | "u0" + | "bun.String" + | "bool" + | "u8" + | "u16" + | "u32" + | "u64" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "f64" + | Struct; + +export function cAbiTypeInfo(type: CAbiType): [size: number, align: number] { + if (typeof type !== "string") { + return type.abiInfo(); + } + switch (type) { + case "u0": + return [0, 0]; // no-op + case "bool": + case "u8": + case "i8": + return [1, 1]; + case "u16": + case "i16": + return [2, 2]; + case "u32": + case "i32": + return [4, 4]; + case "usize": + case "u64": + case "i64": + case "f64": + return [8, 8]; + case "*anyopaque": + case "*JSGlobalObject": + case "JSValue": + case "JSValue.MaybeException": + return [8, 8]; // pointer size + case "bun.String": + return [24, 8]; + default: + throw new Error("unexpected: " + (type satisfies never)); + } +} + +export function cAbiTypeName(type: CAbiType) { + if (typeof type !== "string") { + return type.name(); + } + return ( + { + "*anyopaque": "void*", + "*JSGlobalObject": "JSC::JSGlobalObject*", + "JSValue": "JSValue", + "JSValue.MaybeException": "JSValue", + "bool": "bool", + "u8": "uint8_t", + "u16": "uint16_t", + "u32": "uint32_t", + "u64": "uint64_t", + "i8": "int8_t", + "i16": "int16_t", + "i32": "int32_t", + "i64": "int64_t", + "f64": "double", + "usize": "size_t", + "bun.String": "BunString", + u0: "void", + } satisfies Record, string> + )[type]; +} + +export function alignForward(size: number, alignment: number) { + return Math.floor((size + alignment - 1) / alignment) * alignment; +} + +export class Struct { + fields: StructField[] = []; + #hash?: string; + #name?: string; + namespace?: string; + + abiInfo(): [size: number, align: number] { + let size = 0; + let align = 0; + for (const field of this.fields) { + size = alignForward(size, field.naturalAlignment); + size += field.size; + align = Math.max(align, field.naturalAlignment); + } + return [size, align]; + } + + reorderForSmallestSize() { + // for conistency sort by alignment, then size, then name + this.fields.sort((a, b) => { + if (a.naturalAlignment !== b.naturalAlignment) { + return a.naturalAlignment - b.naturalAlignment; + } + if (a.size !== b.size) { + return a.size - b.size; + } + return a.name.localeCompare(b.name); + }); + } + + hash() { + return (this.#hash ??= String( + Bun.hash( + this.fields + .map(f => { + if (f.type instanceof Struct) { + return f.name + `:` + f.type.hash(); + } + return f.name + `:` + f.type; + }) + .join(","), + ), + )); + } + + name() { + if (this.#name) return this.#name; + const hash = this.hash(); + const existing = structHashToSelf.get(hash); + if (existing && existing !== this) return (this.#name = existing.name()); + return (this.#name = `anon_extern_struct_${hash}`); + } + + toString() { + return this.namespace ? `${this.namespace}.${this.name()}` : this.name(); + } + + assignName(name: string) { + if (this.#name) return; + const hash = this.hash(); + const existing = structHashToSelf.get(hash); + if (existing && existing.#name) name = existing.#name; + this.#name = name; + if (existing) existing.#name = name; + } + + assignGeneratedName(name: string) { + if (this.#name) return; + this.assignName(name); + } + + add(name: string, cType: CAbiType) { + const [size, naturalAlignment] = cAbiTypeInfo(cType); + this.fields.push({ name, type: cType, size, naturalAlignment }); + } + + emitZig(zig: CodeWriter, semi: "with-semi" | "no-semi") { + zig.line("extern struct {"); + zig.indent(); + for (const field of this.fields) { + zig.line(`${snake(field.name)}: ${field.type},`); + } + zig.dedent(); + zig.line("}" + (semi === "with-semi" ? ";" : "")); + } + + emitCpp(cpp: CodeWriter, structName: string) { + cpp.line(`struct ${structName} {`); + cpp.indent(); + for (const field of this.fields) { + cpp.line(`${cAbiTypeName(field.type)} ${field.name};`); + } + cpp.dedent(); + cpp.line("};"); + } +} + +export interface StructField { + /** camel case */ + name: string; + type: CAbiType; + size: number; + naturalAlignment: number; +} + +export class CodeWriter { + level = 0; + buffer = ""; + + temporaries = new Set(); + + line(s?: string) { + this.add((s ?? "") + "\n"); + } + + add(s: string) { + this.buffer += (this.buffer.endsWith("\n") ? " ".repeat(this.level) : "") + s; + } + + indent() { + this.level += 1; + } + + dedent() { + this.level -= 1; + } + + trimLastNewline() { + this.buffer = this.buffer.trimEnd(); + } + + resetTemporaries() { + this.temporaries.clear(); + } + + nextTemporaryName(label: string) { + let i = 0; + let name = `${label}_${i}`; + while (this.temporaries.has(name)) { + i++; + name = `${label}_${i}`; + } + this.temporaries.add(name); + return name; + } +} diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts new file mode 100644 index 00000000000000..2ef06e92ede9c4 --- /dev/null +++ b/src/codegen/bindgen-lib.ts @@ -0,0 +1,242 @@ +// This is the public API for `bind.ts` files +// It is aliased as `import {} from 'bindgen'` +import { + isType, + dictionaryImpl, + oneOfImpl, + registerFunction, + TypeImpl, + type TypeKind, + isFunc, +} from "./bindgen-lib-internal"; + +export type Type = { + [isType]: true | [T, K, Flags]; +} & (Flags extends null + ? { + /** + * Optional means the value may be omitted from a parameter definition. + * Parameters are required by default. + */ + optional: Type; + /** + * When this is used as a dictionary value, this makes that parameter + * required. Dictionary entries are optional by default. + */ + required: Type, K, false>; + + /** Implies `optional`, this sets a default value if omitted */ + default(def: T): Type; + } & (K extends IntegerTypeKind + ? { + /** + * Applies [Clamp] semantics + * https://webidl.spec.whatwg.org/#Clamp + * If a custom numeric range is provided, it will be used instead of the built-in clamp rules. + */ + clamp(min?: T, max?: T): Type; + /** + * Applies [EnforceRange] semantics + * https://webidl.spec.whatwg.org/#EnforceRange + * If a custom numeric range is provided, it will be used instead of the built-in enforce rules. + */ + enforceRange(min?: T, max?: T): Type; + } + : {}) + : {}); + +export type AcceptedDictionaryTypeKind = Exclude; +export type IntegerTypeKind = "usize" | "i32" | "i64" | "u32" | "u64" | "i8" | "u8" | "i16" | "u16"; + +function builtinType() { + return (kind: K) => new TypeImpl(kind, undefined as any, {}) as Type as Type; +} + +/** Contains all primitive types provided by the bindings generator */ +export namespace t { + /** + * Can only be used as an argument type. + * Tells the code generator to pass `*JSC.JSGlobalObject` as a parameter + */ + export const globalObject = builtinType()("globalObject"); + /** + * Can only be used as an argument type. + * Tells the code generator to pass `*JSC.VirtualMachine` as a parameter + */ + export const zigVirtualMachine = builtinType()("zigVirtualMachine"); + + /** + * Provides the raw JSValue from the JavaScriptCore API. Avoid using this if + * possible. This indicates the bindings generator is incapable of processing + * your use case. + */ + export const any = builtinType()("any"); + /** Void function type */ + export const undefined = builtinType()("undefined"); + /** Does not throw on parse. Equivalent to `!!value` */ + export const boolean = builtinType()("boolean"); + /** Throws if the value is not a boolean. */ + export const strictBoolean = builtinType()("strictBoolean"); + + export const f64 = builtinType()("f64"); + + export const u8 = builtinType()("u8"); + export const u16 = builtinType()("u16"); + export const u32 = builtinType()("u32"); + export const u64 = builtinType()("u64"); + export const i8 = builtinType()("i8"); + export const i16 = builtinType()("i16"); + export const i32 = builtinType()("i32"); + export const i64 = builtinType()("i64"); + export const usize = builtinType()("usize"); + + /** + * The DOMString type corresponds to strings. + * + * **Note**: A DOMString value might include unmatched surrogate code points. + * Use USVString if this is not desirable. + * + * https://webidl.spec.whatwg.org/#idl-DOMString + */ + export const DOMString = builtinType()("DOMString"); + /* + * The USVString type corresponds to scalar value strings. Depending on the + * context, these can be treated as sequences of code units or scalar values. + * + * Specifications should only use USVString for APIs that perform text + * processing and need a string of scalar values to operate on. Most APIs that + * use strings should instead be using DOMString, which does not make any + * interpretations of the code units in the string. When in doubt, use + * DOMString + * + * https://webidl.spec.whatwg.org/#idl-USVString + */ + export const USVString = builtinType()("USVString"); + /** + * The ByteString type corresponds to byte sequences. + * + * WARNING: Specifications should only use ByteString for interfacing with protocols + * that use bytes and strings interchangeably, such as HTTP. In general, + * strings should be represented with DOMString values, even if it is expected + * that values of the string will always be in ASCII or some 8-bit character + * encoding. Sequences or frozen arrays with octet or byte elements, + * Uint8Array, or Int8Array should be used for holding 8-bit data rather than + * ByteString. + * + * https://webidl.spec.whatwg.org/#idl-ByteString + */ + export const ByteString = builtinType()("ByteString"); + /** + * DOMString but encoded as `[]const u8` + */ + export const UTF8String = builtinType()("UTF8String"); + + /** An array or iterable type of T */ + export function sequence(itemType: Type): Type, "sequence"> { + return new TypeImpl("sequence", { + element: itemType as TypeImpl, + repr: "slice", + }); + } + + /** Object with arbitrary keys but a specific value type */ + export function record(valueType: Type): Type, "record"> { + return new TypeImpl("record", { + value: valueType as TypeImpl, + repr: "kv-slices", + }); + } + + /** + * Reference a type by string name instead of by object reference. This is + * required in some siutations like `Request` which can take an existing + * request object in as itself. + */ + export function ref(name: string): Type { + return new TypeImpl("ref", name); + } + + /** + * Reference an external class type that is not defined with `bindgen`, + * from either WebCore, JavaScriptCore, or Bun. + */ + export function externalClass(name: string): Type { + return new TypeImpl("ref", name); + } + + export function oneOf[]>( + ...types: T + ): Type< + { + [K in keyof T]: T[K] extends Type ? U : never; + }[number], + "oneOf" + > { + return oneOfImpl(types as unknown[] as TypeImpl[]); + } + + export function dictionary>>( + fields: R, + ): Type< + { + [K in keyof R]?: R[K] extends Type ? T : never; + }, + "dictionary" + > { + return dictionaryImpl(fields as Record); + } + + /** Create an enum from a list of strings. */ + export function stringEnum( + ...values: T + ): Type< + { + [K in keyof T]: K; + }[number], + "stringEnum" + > { + return new TypeImpl("stringEnum", values.sort()); + } + + /** + * Equivalent to `stringEnum`, but using an enum sourced from the given Zig + * file. Use this to get an enum type that can have functions added. + */ + export function zigEnum(file: string, impl: string): Type { + return new TypeImpl("zigEnum", { file, impl }); + } +} + +export type FuncOptions = FuncMetadata & + ( + | { + variants: FuncVariant[]; + } + | FuncVariant + ); + +export interface FuncMetadata { + /** + * The namespace where the implementation is, by default it's in the root. + */ + implNamespace?: string; + /** + * TODO: + * Automatically generate code to expose this function on a well-known object + */ + exposedOn?: ExposedOn; +} + +export type FuncReference = { [isFunc]: true }; + +export type ExposedOn = "JSGlobalObject" | "BunObject"; + +export interface FuncVariant { + /** Ordered record. Cannot include ".required" types since required is the default. */ + args: Record>; + ret: Type; +} + +export function fn(opts: FuncOptions) { + return registerFunction(opts) as FuncReference; +} diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts new file mode 100644 index 00000000000000..d548a9a7cde09f --- /dev/null +++ b/src/codegen/bindgen.ts @@ -0,0 +1,1302 @@ +// The binding generator to rule them all. +// Converts binding definition files (.bind.ts) into C++ and Zig code. +// +// Generated bindings are available in `bun.generated..*` in Zig, +// or `Generated::::*` in C++ from including `Generated.h`. +import * as path from "node:path"; +import { + CodeWriter, + TypeImpl, + cAbiTypeInfo, + cAbiTypeName, + cap, + extDispatchVariant, + extJsFunction, + files, + snake, + src, + str, + Struct, + type CAbiType, + type DictionaryField, + type ReturnStrategy, + type TypeKind, + type Variant, + typeHashToNamespace, + typeHashToReachableType, + zid, + ArgStrategyChildItem, + inspect, + pascal, + alignForward, + isFunc, + Func, +} from "./bindgen-lib-internal"; +import assert from "node:assert"; +import { argParse, readdirRecursiveWithExclusionsAndExtensionsSync, writeIfNotChanged } from "./helpers"; +import { type IntegerTypeKind } from "bindgen"; + +// arg parsing +let { "codegen-root": codegenRoot, debug } = argParse(["codegen-root", "debug"]); +if (debug === "false" || debug === "0" || debug == "OFF") debug = false; +if (!codegenRoot) { + console.error("Missing --codegen-root=..."); + process.exit(1); +} + +function resolveVariantStrategies(vari: Variant, name: string) { + let argIndex = 0; + let communicationStruct: Struct | undefined; + for (const arg of vari.args) { + if (arg.type.isVirtualArgument() && vari.globalObjectArg === undefined) { + vari.globalObjectArg = argIndex; + } + argIndex += 1; + + // If `extern struct` can represent this type, that is the simplest way to cross the C-ABI boundary. + const isNullable = (arg.type.flags.optional && !("default" in arg.type.flags)) || arg.type.flags.nullable; + const abiType = !isNullable && arg.type.canDirectlyMapToCAbi(); + if (abiType) { + arg.loweringStrategy = { + // This does not work in release builds, possibly due to a Zig 0.13 bug + // regarding by-value extern structs in C functions. + // type: cAbiTypeInfo(abiType)[0] > 8 ? "c-abi-pointer" : "c-abi-value", + // Always pass an argument by-pointer for now. + type: abiType === "*anyopaque" || abiType === "*JSGlobalObject" ? "c-abi-value" : "c-abi-pointer", + abiType, + }; + continue; + } + + communicationStruct ??= new Struct(); + const prefix = `${arg.name}`; + const children = isNullable + ? resolveNullableArgumentStrategy(arg.type, prefix, communicationStruct) + : resolveComplexArgumentStrategy(arg.type, prefix, communicationStruct); + arg.loweringStrategy = { + type: "uses-communication-buffer", + prefix, + children, + }; + } + + if (vari.globalObjectArg === undefined) { + vari.globalObjectArg = "hidden"; + } + + return_strategy: { + if (vari.ret.kind === "any") { + vari.returnStrategy = { type: "jsvalue" }; + break return_strategy; + } + const abiType = vari.ret.canDirectlyMapToCAbi(); + if (abiType) { + vari.returnStrategy = { + type: "basic-out-param", + abiType, + }; + break return_strategy; + } + } + + communicationStruct?.reorderForSmallestSize(); + communicationStruct?.assignGeneratedName(name); + vari.communicationStruct = communicationStruct; +} + +function resolveNullableArgumentStrategy( + type: TypeImpl, + prefix: string, + communicationStruct: Struct, +): ArgStrategyChildItem[] { + assert((type.flags.optional && !("default" in type.flags)) || type.flags.nullable); + communicationStruct.add(`${prefix}Set`, "bool"); + return resolveComplexArgumentStrategy(type, `${prefix}Value`, communicationStruct); +} + +function resolveComplexArgumentStrategy( + type: TypeImpl, + prefix: string, + communicationStruct: Struct, +): ArgStrategyChildItem[] { + const abiType = type.canDirectlyMapToCAbi(); + if (abiType) { + communicationStruct.add(prefix, abiType); + return [ + { + type: "c-abi-compatible", + abiType, + }, + ]; + } + + switch (type.kind) { + default: + throw new Error(`TODO: resolveComplexArgumentStrategy for ${type.kind}`); + } +} + +function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionName: string) { + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + if (variant.minRequiredArgs > 0) { + cpp.line(`size_t argumentCount = callFrame->argumentCount();`); + cpp.line(`if (argumentCount < ${variant.minRequiredArgs}) {`); + cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); + cpp.line(`}`); + } + const communicationStruct = variant.communicationStruct; + if (communicationStruct) { + cpp.line(`${communicationStruct.name()} buf;`); + communicationStruct.emitCpp(cppInternal, communicationStruct.name()); + } + + let i = 0; + for (const arg of variant.args) { + const type = arg.type; + if (type.isVirtualArgument()) continue; + + const exceptionContext: ExceptionContext = { + type: "argument", + argumentIndex: i, + name: arg.name, + functionName: name, + }; + + const strategy = arg.loweringStrategy!; + assert(strategy); + + const get = variant.minRequiredArgs > i ? "uncheckedArgument" : "argument"; + cpp.line(`JSC::EnsureStillAliveScope arg${i} = callFrame->${get}(${i});`); + + let storageLocation; + let needDeclare = true; + switch (strategy.type) { + case "c-abi-pointer": + case "c-abi-value": + storageLocation = "arg" + cap(arg.name); + break; + case "uses-communication-buffer": + storageLocation = `buf.${strategy.prefix}`; + needDeclare = false; + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); + } + + const jsValueRef = `arg${i}.value()`; + + /** If JavaScript may pass null or undefined */ + const isOptionalToUser = type.flags.nullable || type.flags.optional || "default" in type.flags; + /** If the final representation may include null */ + const isNullable = type.flags.nullable || (type.flags.optional && !("default" in type.flags)); + + if (isOptionalToUser) { + if (needDeclare) { + addHeaderForType(type); + cpp.line(`${type.cppName()} ${storageLocation};`); + } + if (isNullable) { + assert(strategy.type === "uses-communication-buffer"); + cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.isUndefinedOrNull())) {`); + storageLocation = `${storageLocation}Value`; + } else { + cpp.line(`if (!${jsValueRef}.isUndefinedOrNull()) {`); + } + cpp.indent(); + emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, "assign"); + cpp.dedent(); + if ("default" in type.flags) { + cpp.line(`} else {`); + cpp.indent(); + cpp.add(`${storageLocation} = `); + type.emitCppDefaultValue(cpp); + cpp.line(";"); + cpp.dedent(); + } else { + assert(isNullable); + } + cpp.line(`}`); + } else { + emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, needDeclare ? "declare" : "assign"); + } + + i += 1; + } + + const returnStrategy = variant.returnStrategy!; + switch (returnStrategy.type) { + case "jsvalue": + cpp.line(`return ${dispatchFunctionName}(`); + break; + case "basic-out-param": + cpp.line(`${cAbiTypeName(returnStrategy.abiType)} out;`); + cpp.line(`if (!${dispatchFunctionName}(`); + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); + } + + let emittedFirstArgument = false; + function addCommaAfterArgument() { + if (emittedFirstArgument) { + cpp.line(","); + } else { + emittedFirstArgument = true; + } + } + + const totalArgs = variant.args.length; + i = 0; + cpp.indent(); + + if (variant.globalObjectArg === "hidden") { + addCommaAfterArgument(); + cpp.add("global"); + } + + for (const arg of variant.args) { + i += 1; + if (arg.type.isVirtualArgument()) { + switch (arg.type.kind) { + case "zigVirtualMachine": + case "globalObject": + addCommaAfterArgument(); + cpp.add("global"); + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(arg.type)}`); + } + } else { + const storageLocation = `arg${cap(arg.name)}`; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + addCommaAfterArgument(); + cpp.add(`&${storageLocation}`); + break; + case "c-abi-value": + addCommaAfterArgument(); + cpp.add(`${storageLocation}`); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); + } + } + } + + if (communicationStruct) { + addCommaAfterArgument(); + cpp.add("&buf"); + } + + switch (returnStrategy.type) { + case "jsvalue": + cpp.dedent(); + if (totalArgs === 0) { + cpp.trimLastNewline(); + } + cpp.line(");"); + break; + case "basic-out-param": + addCommaAfterArgument(); + cpp.add("&out"); + cpp.line(); + cpp.dedent(); + cpp.line(")) {"); + cpp.line(` return {};`); + cpp.line("}"); + const simpleType = getSimpleIdlType(variant.ret); + if (simpleType) { + cpp.line(`return JSC::JSValue::encode(WebCore::toJS<${simpleType}>(*global, out));`); + break; + } + switch (variant.ret.kind) { + case "UTF8String": + throw new Error("Memory lifetime is ambiguous when returning UTF8String"); + case "DOMString": + case "USVString": + case "ByteString": + cpp.line( + `return JSC::JSValue::encode(WebCore::toJS(*global, out.toWTFString()));`, + ); + break; + } + break; + default: + throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); + } +} + +/** If a simple IDL type mapping exists, it also currently means there is a direct C ABI mapping */ +function getSimpleIdlType(type: TypeImpl): string | undefined { + const map: { [K in TypeKind]?: string } = { + boolean: "WebCore::IDLBoolean", + undefined: "WebCore::IDLUndefined", + f64: "WebCore::IDLDouble", + usize: "WebCore::IDLUnsignedLongLong", + u8: "WebCore::IDLOctet", + u16: "WebCore::IDLUnsignedShort", + u32: "WebCore::IDLUnsignedLong", + u64: "WebCore::IDLUnsignedLongLong", + i8: "WebCore::IDLByte", + i16: "WebCore::IDLShort", + i32: "WebCore::IDLLong", + i64: "WebCore::IDLLongLong", + }; + let entry = map[type.kind]; + if (!entry) { + switch (type.kind) { + case "stringEnum": + type.lowersToNamedType; + // const cType = cAbiTypeForEnum(type.data.length); + // entry = map[cType as IntegerTypeKind]!; + entry = `WebCore::IDLEnumeration<${type.cppClassName()}>`; + break; + default: + return; + } + } + + if (type.flags.range) { + // TODO: when enforceRange is used, a custom adaptor should be used instead + // of chaining both `WebCore::IDLEnforceRangeAdaptor` and custom logic. + const rangeAdaptor = { + "clamp": "WebCore::IDLClampAdaptor", + "enforce": "WebCore::IDLEnforceRangeAdaptor", + }[type.flags.range[0]]; + assert(rangeAdaptor); + entry = `${rangeAdaptor}<${entry}>`; + } + + return entry; +} + +type ExceptionContext = + | { type: "none" } + | { type: "argument"; argumentIndex: number; name: string; functionName: string }; + +function emitConvertValue( + storageLocation: string, + type: TypeImpl, + jsValueRef: string, + exceptionContext: ExceptionContext, + decl: "declare" | "assign", +) { + if (decl === "declare") { + addHeaderForType(type); + } + + const simpleType = getSimpleIdlType(type); + if (simpleType) { + const cAbiType = type.canDirectlyMapToCAbi(); + assert(cAbiType); + let exceptionHandlerBody; + + switch (type.kind) { + case "zigEnum": + case "stringEnum": { + if (exceptionContext.type === "argument") { + const { argumentIndex, name, functionName: quotedFunctionName } = exceptionContext; + exceptionHandlerBody = `WebCore::throwArgumentMustBeEnumError(lexicalGlobalObject, scope, ${argumentIndex}, ${str(name)}_s, ${str(type.name())}_s, ${str(quotedFunctionName)}_s, WebCore::expectedEnumerationValues<${type.cppClassName()}>());`; + } + break; + } + } + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + + let exceptionHandler = exceptionHandlerBody + ? `, [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { ${exceptionHandlerBody} }` + : ""; + cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandler});`); + + if (type.flags.range && type.flags.range[1] !== "abi") { + emitRangeModifierCheck(cAbiType, storageLocation, type.flags.range); + } + + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + } else { + switch (type.kind) { + case "any": { + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = JSC::JSValue::encode(${jsValueRef});`); + break; + } + case "USVString": + case "DOMString": + case "ByteString": { + const temp = cpp.nextTemporaryName("wtfString"); + cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = Bun::toString(${temp});`); + break; + } + case "UTF8String": { + const temp = cpp.nextTemporaryName("wtfString"); + cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); + cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); + + if (decl === "declare") { + cpp.add(`${type.cppName()} `); + } + cpp.line(`${storageLocation} = Bun::toString(${temp});`); + break; + } + case "dictionary": { + if (decl === "declare") { + cpp.line(`${type.cppName()} ${storageLocation};`); + } + cpp.line(`if (!convert${type.cppInternalName()}(&${storageLocation}, global, ${jsValueRef}))`); + cpp.indent(); + cpp.line(`return {};`); + cpp.dedent(); + break; + } + default: + throw new Error(`TODO: emitConvertValue for Type ${type.kind}`); + } + } +} + +/** + * The built in WebCore range adaptors do not support arbitrary ranges, but that + * is something we want to have. They aren't common, so they are just tacked + * onto the webkit one. + */ +function emitRangeModifierCheck( + cAbiType: CAbiType, + storageLocation: string, + range: ["clamp" | "enforce", bigint, bigint], +) { + const [kind, min, max] = range; + if (kind === "clamp") { + cpp.line(`if (${storageLocation} < ${min}) ${storageLocation} = ${min};`); + cpp.line(`else if (${storageLocation} > ${max}) ${storageLocation} = ${max};`); + } else if (kind === "enforce") { + cpp.line(`if (${storageLocation} < ${min} || ${storageLocation} > ${max}) {`); + cpp.indent(); + cpp.line( + `throwTypeError(global, throwScope, rangeErrorString<${cAbiTypeName(cAbiType)}>(${storageLocation}, ${min}, ${max}));`, + ); + cpp.line(`return {};`); + cpp.dedent(); + cpp.line(`}`); + } else { + throw new Error(`TODO: range modifier ${kind}`); + } +} + +function addHeaderForType(type: TypeImpl) { + if (type.lowersToNamedType() && type.ownerFile) { + headers.add(`Generated${cap(type.ownerFile)}.h`); + } +} + +function emitConvertDictionaryFunction(type: TypeImpl) { + assert(type.kind === "dictionary"); + const fields = type.data as DictionaryField[]; + + addHeaderForType(type); + + cpp.line(`// Internal dictionary parse for ${type.name()}`); + cpp.line( + `bool convert${type.cppInternalName()}(${type.cppName()}* result, JSC::JSGlobalObject* global, JSC::JSValue value) {`, + ); + cpp.indent(); + + cpp.line(`auto& vm = JSC::getVM(global);`); + cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); + cpp.line(`bool isNullOrUndefined = value.isUndefinedOrNull();`); + cpp.line(`auto* object = isNullOrUndefined ? nullptr : value.getObject();`); + cpp.line(`if (UNLIKELY(!isNullOrUndefined && !object)) {`); + cpp.line(` throwTypeError(global, throwScope);`); + cpp.line(` return false;`); + cpp.line(`}`); + cpp.line(`JSC::JSValue propValue;`); + + for (const field of fields) { + const { key, type: fieldType } = field; + cpp.line("// " + key); + cpp.line(`if (isNullOrUndefined) {`); + cpp.line(` propValue = JSC::jsUndefined();`); + cpp.line(`} else {`); + headers.add("ObjectBindings.h"); + cpp.line( + ` propValue = Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, global, object, JSC::Identifier::fromString(vm, ${str(key)}_s));`, + ); + cpp.line(` RETURN_IF_EXCEPTION(throwScope, false);`); + cpp.line(`}`); + cpp.line(`if (!propValue.isUndefined()) {`); + cpp.indent(); + emitConvertValue(`result->${key}`, fieldType, "propValue", { type: "none" }, "assign"); + cpp.dedent(); + cpp.line(`} else {`); + cpp.indent(); + if (type.flags.required) { + cpp.line(`throwTypeError(global, throwScope);`); + cpp.line(`return false;`); + } else if ("default" in fieldType.flags) { + cpp.add(`result->${key} = `); + fieldType.emitCppDefaultValue(cpp); + cpp.line(";"); + } else { + throw new Error(`TODO: optional dictionary field`); + } + cpp.dedent(); + cpp.line(`}`); + } + + cpp.line(`return true;`); + cpp.dedent(); + cpp.line(`}`); + cpp.line(); +} + +function emitZigStruct(type: TypeImpl) { + zig.add(`pub const ${type.name()} = `); + + switch (type.kind) { + case "zigEnum": + case "stringEnum": { + const signPrefix = "u"; + const tagType = `${signPrefix}${alignForward(type.data.length, 8)}`; + zig.line(`enum(${tagType}) {`); + zig.indent(); + for (const value of type.data) { + zig.line(`${snake(value)},`); + } + zig.dedent(); + zig.line("};"); + return; + } + } + + const externLayout = type.canDirectlyMapToCAbi(); + if (externLayout) { + if (typeof externLayout === "string") { + zig.line(externLayout + ";"); + } else { + externLayout.emitZig(zig, "with-semi"); + } + return; + } + + switch (type.kind) { + case "dictionary": { + zig.line("struct {"); + zig.indent(); + for (const { key, type: fieldType } of type.data as DictionaryField[]) { + zig.line(` ${snake(key)}: ${zigTypeName(fieldType)},`); + } + zig.dedent(); + zig.line(`};`); + break; + } + default: { + throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); + } + } +} + +function emitCppStructHeader(w: CodeWriter, type: TypeImpl) { + if (type.kind === "zigEnum" || type.kind === "stringEnum") { + emitCppEnumHeader(w, type); + return; + } + + const externLayout = type.canDirectlyMapToCAbi(); + if (externLayout) { + if (typeof externLayout === "string") { + w.line(`typedef ${externLayout} ${type.name()};`); + console.warn("should this really be done lol", type); + } else { + externLayout.emitCpp(w, type.name()); + w.line(); + } + return; + } + + switch (type.kind) { + default: { + throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); + } + } +} + +function emitCppEnumHeader(w: CodeWriter, type: TypeImpl) { + assert(type.kind === "zigEnum" || type.kind === "stringEnum"); + + assert(type.kind === "stringEnum"); // TODO + assert(type.data.length > 0); + const signPrefix = "u"; + const intBits = alignForward(type.data.length, 8); + const tagType = `${signPrefix}int${intBits}_t`; + w.line(`enum class ${type.name()} : ${tagType} {`); + for (const value of type.data) { + w.line(` ${pascal(value)},`); + } + w.line(`};`); + w.line(); +} + +// This function assumes in the WebCore namespace +function emitConvertEnumFunction(w: CodeWriter, type: TypeImpl) { + assert(type.kind === "zigEnum" || type.kind === "stringEnum"); + assert(type.kind === "stringEnum"); // TODO + assert(type.data.length > 0); + + const name = "Generated::" + type.cppName(); + headers.add("JavaScriptCore/JSCInlines.h"); + headers.add("JavaScriptCore/JSString.h"); + headers.add("wtf/NeverDestroyed.h"); + headers.add("wtf/SortedArrayMap.h"); + + w.line(`String convertEnumerationToString(${name} enumerationValue) {`); + w.indent(); + w.line(` static const NeverDestroyed values[] = {`); + w.indent(); + for (const value of type.data) { + w.line(` MAKE_STATIC_STRING_IMPL(${str(value)}),`); + } + w.dedent(); + w.line(` };`); + w.line(` return values[static_cast(enumerationValue)];`); + w.dedent(); + w.line(`}`); + w.line(); + w.line(`template<> JSString* convertEnumerationToJS(JSC::JSGlobalObject& global, ${name} enumerationValue) {`); + w.line(` return jsStringWithCache(global.vm(), convertEnumerationToString(enumerationValue));`); + w.line(`}`); + w.line(); + w.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String& stringValue)`); + w.line(`{`); + w.line(` static constexpr std::pair mappings[] = {`); + for (const value of type.data) { + w.line(` { ${str(value)}, ${name}::${pascal(value)} },`); + } + w.line(` };`); + w.line(` static constexpr SortedArrayMap enumerationMapping { mappings };`); + w.line(` if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); LIKELY(enumerationValue))`); + w.line(` return *enumerationValue;`); + w.line(` return std::nullopt;`); + w.line(`}`); + w.line(); + w.line( + `template<> std::optional<${name}> parseEnumeration<${name}>(JSGlobalObject& lexicalGlobalObject, JSValue value)`, + ); + w.line(`{`); + w.line(` return parseEnumerationFromString<${name}>(value.toWTFString(&lexicalGlobalObject));`); + w.line(`}`); + w.line(); + w.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>()`); + w.line(`{`); + w.line(` return ${str(type.data.map(value => `${str(value)}`).join(", "))}_s;`); + w.line(`}`); + w.line(); +} + +function zigTypeName(type: TypeImpl): string { + let name = zigTypeNameInner(type); + if (type.flags.optional) { + name = "?" + name; + } + return name; +} + +function zigTypeNameInner(type: TypeImpl): string { + if (type.lowersToNamedType()) { + const namespace = typeHashToNamespace.get(type.hash()); + return namespace ? `${namespace}.${type.name()}` : type.name(); + } + switch (type.kind) { + case "USVString": + case "DOMString": + case "ByteString": + case "UTF8String": + return "bun.String"; + case "boolean": + return "bool"; + case "usize": + return "usize"; + case "globalObject": + case "zigVirtualMachine": + return "*JSC.JSGlobalObject"; + default: + const cAbiType = type.canDirectlyMapToCAbi(); + if (cAbiType) { + if (typeof cAbiType === "string") { + return cAbiType; + } + return cAbiType.name(); + } + throw new Error(`TODO: emitZigTypeName for Type ${type.kind}`); + } +} + +function returnStrategyCppType(strategy: ReturnStrategy): string { + switch (strategy.type) { + case "basic-out-param": + return "bool"; // true=success, false=exception + case "jsvalue": + return "JSC::EncodedJSValue"; + default: + throw new Error( + `TODO: returnStrategyCppType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, + ); + } +} + +function returnStrategyZigType(strategy: ReturnStrategy): string { + switch (strategy.type) { + case "basic-out-param": + return "bool"; // true=success, false=exception + case "jsvalue": + return "JSC.JSValue"; + default: + throw new Error( + `TODO: returnStrategyZigType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, + ); + } +} + +function emitNullableZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { + assert(children.length > 0); + const indent = children[0].type !== "c-abi-compatible"; + w.add(`if (${prefix}_set)`); + if (indent) { + w.indent(); + } else { + w.add(` `); + } + emitComplexZigDecoder(w, prefix + "_value", type, children); + if (indent) { + w.line(); + w.dedent(); + } else { + w.add(` `); + } + w.add(`else`); + if (indent) { + w.indent(); + } else { + w.add(` `); + } + w.add(`null`); + if (indent) w.dedent(); +} + +function emitComplexZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { + assert(children.length > 0); + if (children[0].type === "c-abi-compatible") { + w.add(`${prefix}`); + return; + } + + switch (type.kind) { + default: + throw new Error(`TODO: emitComplexZigDecoder for Type ${type.kind}`); + } +} + +// BEGIN MAIN CODE GENERATION + +// Search for all .bind.ts files +const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["node_modules", ".git"], [".bind.ts"]); + +// Sort for deterministic output +for (const fileName of [...unsortedFiles].sort()) { + const zigFile = path.relative(src, fileName.replace(/\.bind\.ts$/, ".zig")); + let file = files.get(zigFile); + if (!file) { + file = { functions: [], typedefs: [] }; + files.set(zigFile, file); + } + + const exports = import.meta.require(fileName); + + // Mark all exported TypeImpl as reachable + for (let [key, value] of Object.entries(exports)) { + if (value == null || typeof value !== "object") continue; + + if (value instanceof TypeImpl) { + value.assignName(key); + value.markReachable(); + file.typedefs.push({ name: key, type: value }); + } + + if (value[isFunc]) { + const func = value as Func; + func.name = key; + } + } + + for (const fn of file.functions) { + if (fn.name === "") { + const err = new Error(`This function definition needs to be exported`); + err.stack = `Error: ${err.message}\n${fn.snapshot}`; + throw err; + } + } +} + +const zig = new CodeWriter(); +const zigInternal = new CodeWriter(); +// TODO: split each *.bind file into a separate .cpp file +const cpp = new CodeWriter(); +const cppInternal = new CodeWriter(); +const headers = new Set(); + +zig.line('const bun = @import("root").bun;'); +zig.line("const JSC = bun.JSC;"); +zig.line("const JSHostFunctionType = JSC.JSHostFunctionType;\n"); + +zigInternal.line("const binding_internals = struct {"); +zigInternal.indent(); + +cpp.line("namespace Generated {"); +cpp.line(); +cpp.line("template"); +cpp.line("static String rangeErrorString(T value, T min, T max)"); +cpp.line("{"); +cpp.line(` return makeString("Value "_s, value, " is outside the range ["_s, min, ", "_s, max, ']');`); +cpp.line("}"); +cpp.line(); + +cppInternal.line('// These "Arguments" definitions are for communication between C++ and Zig.'); +cppInternal.line('// Field layout depends on implementation details in "bindgen.ts", and'); +cppInternal.line("// is not intended for usage outside generated binding code."); + +headers.add("root.h"); +headers.add("IDLTypes.h"); +headers.add("JSDOMBinding.h"); +headers.add("JSDOMConvertBase.h"); +headers.add("JSDOMConvertBoolean.h"); +headers.add("JSDOMConvertNumbers.h"); +headers.add("JSDOMConvertStrings.h"); +headers.add("JSDOMExceptionHandling.h"); +headers.add("JSDOMOperation.h"); + +/** + * Indexed by `zigFile`, values are the generated zig identifier name, without + * collisions. + */ +const fileMap = new Map(); +const fileNames = new Set(); + +for (const [filename, { functions, typedefs }] of files) { + const basename = path.basename(filename, ".zig"); + let varName = basename; + if (fileNames.has(varName)) { + throw new Error(`File name collision: ${basename}.zig`); + } + fileNames.add(varName); + fileMap.set(filename, varName); + + if (functions.length === 0) continue; + + for (const td of typedefs) { + typeHashToNamespace.set(td.type.hash(), varName); + } + + for (const fn of functions) { + for (const vari of fn.variants) { + for (const arg of vari.args) { + arg.type.markReachable(); + } + } + } +} + +let needsWebCore = false; +for (const type of typeHashToReachableType.values()) { + // Emit convert functions for compound types in the Generated namespace + switch (type.kind) { + case "dictionary": + emitConvertDictionaryFunction(type); + break; + case "stringEnum": + case "zigEnum": + needsWebCore = true; + break; + } +} + +for (const [filename, { functions, typedefs }] of files) { + const namespaceVar = fileMap.get(filename)!; + assert(namespaceVar, `namespaceVar not found for ${filename}, ${inspect(fileMap)}`); + zigInternal.line(`const import_${namespaceVar} = @import(${str(path.relative(src + "/bun.js", filename))});`); + + zig.line(`/// Generated for "src/${filename}"`); + zig.line(`pub const ${namespaceVar} = struct {`); + zig.indent(); + + for (const fn of functions) { + cpp.line(`// Dispatch for \"fn ${zid(fn.name)}(...)\" in \"src/${fn.zigFile}\"`); + const externName = extJsFunction(namespaceVar, fn.name); + + // C++ forward declarations + let variNum = 1; + for (const vari of fn.variants) { + resolveVariantStrategies( + vari, + `${pascal(namespaceVar)}${pascal(fn.name)}Arguments${fn.variants.length > 1 ? variNum : ""}`, + ); + const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); + + const args: string[] = []; + + let argNum = 0; + if (vari.globalObjectArg === "hidden") { + args.push("JSC::JSGlobalObject*"); + } + for (const arg of vari.args) { + argNum += 1; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + addHeaderForType(arg.type); + args.push(`const ${arg.type.cppName()}*`); + break; + case "c-abi-value": + addHeaderForType(arg.type); + args.push(arg.type.cppName()); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: C++ dispatch function for ${inspect(strategy)}`); + } + } + const { communicationStruct } = vari; + if (communicationStruct) { + args.push(`${communicationStruct.name()}*`); + } + const returnStrategy = vari.returnStrategy!; + if (returnStrategy.type === "basic-out-param") { + args.push(cAbiTypeName(returnStrategy.abiType) + "*"); + } + + cpp.line(`extern "C" ${returnStrategyCppType(vari.returnStrategy!)} ${dispatchName}(${args.join(", ")});`); + + variNum += 1; + } + + // Public function + zig.line( + `pub const ${zid("js" + cap(fn.name))} = @extern(*const JSHostFunctionType, .{ .name = ${str(externName)} });`, + ); + + // Generated JSC host function + cpp.line( + `extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, + ); + cpp.line(`{`); + cpp.indent(); + cpp.resetTemporaries(); + + if (fn.variants.length === 1) { + emitCppCallToVariant(fn.name, fn.variants[0], extDispatchVariant(namespaceVar, fn.name, 1)); + } else { + throw new Error(`TODO: multiple variant dispatch`); + } + + cpp.dedent(); + cpp.line(`}`); + cpp.line(); + + // Generated Zig dispatch functions + variNum = 1; + for (const vari of fn.variants) { + const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); + const args: string[] = []; + const returnStrategy = vari.returnStrategy!; + const { communicationStruct } = vari; + if (communicationStruct) { + zigInternal.add(`const ${communicationStruct.name()} = `); + communicationStruct.emitZig(zigInternal, "with-semi"); + } + + assert(vari.globalObjectArg !== undefined); + + let globalObjectArg = ""; + if (vari.globalObjectArg === "hidden") { + args.push(`global: *JSC.JSGlobalObject`); + globalObjectArg = "global"; + } + let argNum = 0; + for (const arg of vari.args) { + let argName = `arg_${snake(arg.name)}`; + if (vari.globalObjectArg === argNum) { + if (arg.type.kind !== "globalObject") { + argName = "global"; + } + globalObjectArg = argName; + } + argNum += 1; + arg.zigMappedName = argName; + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + args.push(`${argName}: *const ${zigTypeName(arg.type)}`); + break; + case "c-abi-value": + args.push(`${argName}: ${zigTypeName(arg.type)}`); + break; + case "uses-communication-buffer": + break; + default: + throw new Error(`TODO: zig dispatch function for ${inspect(strategy)}`); + } + } + assert(globalObjectArg, `globalObjectArg not found from ${vari.globalObjectArg}`); + + if (communicationStruct) { + args.push(`buf: *${communicationStruct.name()}`); + } + + if (returnStrategy.type === "basic-out-param") { + args.push(`out: *${zigTypeName(vari.ret)}`); + } + + zigInternal.line(`export fn ${zid(dispatchName)}(${args.join(", ")}) ${returnStrategyZigType(returnStrategy)} {`); + zigInternal.indent(); + + zigInternal.line( + `if (!@hasDecl(import_${namespaceVar}${fn.zigPrefix.length > 0 ? "." + fn.zigPrefix.slice(0, -1) : ""}, ${str(fn.name + vari.suffix)}))`, + ); + zigInternal.line( + ` @compileError(${str(`Missing binding declaration "${fn.zigPrefix}${fn.name + vari.suffix}" in "${path.basename(filename)}"`)});`, + ); + + for (const arg of vari.args) { + if (arg.type.kind === "UTF8String") { + zigInternal.line(`const ${arg.zigMappedName}_utf8 = ${arg.zigMappedName}.toUTF8(bun.default_allocator);`); + zigInternal.line(`defer ${arg.zigMappedName}_utf8.deinit();`); + } + } + + switch (returnStrategy.type) { + case "jsvalue": + zigInternal.add(`return JSC.toJSHostValue(${globalObjectArg}, `); + break; + case "basic-out-param": + zigInternal.add(`out.* = @as(bun.JSError!${returnStrategy.abiType}, `); + break; + } + + zigInternal.line(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}(`); + zigInternal.indent(); + for (const arg of vari.args) { + const argName = arg.zigMappedName!; + + if (arg.type.isVirtualArgument()) { + switch (arg.type.kind) { + case "zigVirtualMachine": + zigInternal.line(`${argName}.bunVM(),`); + break; + case "globalObject": + zigInternal.line(`${argName},`); + break; + default: + throw new Error("unexpected"); + } + continue; + } + + const strategy = arg.loweringStrategy!; + switch (strategy.type) { + case "c-abi-pointer": + if (arg.type.kind === "UTF8String") { + zigInternal.line(`${argName}_utf8.slice(),`); + break; + } + zigInternal.line(`${argName}.*,`); + break; + case "c-abi-value": + zigInternal.line(`${argName},`); + break; + case "uses-communication-buffer": + const prefix = `buf.${snake(arg.name)}`; + const type = arg.type; + const isNullable = (type.flags.optional && !("default" in type.flags)) || type.flags.nullable; + if (isNullable) emitNullableZigDecoder(zigInternal, prefix, type, strategy.children); + else emitComplexZigDecoder(zigInternal, prefix, type, strategy.children); + zigInternal.line(`,`); + break; + default: + throw new Error(`TODO: zig dispatch function for ${inspect(strategy satisfies never)}`); + } + } + zigInternal.dedent(); + switch (returnStrategy.type) { + case "jsvalue": + zigInternal.line(`));`); + break; + case "basic-out-param": + zigInternal.line(`)) catch |err| switch (err) {`); + zigInternal.line(` error.JSError => return false,`); + zigInternal.line(` error.OutOfMemory => ${globalObjectArg}.throwOutOfMemory() catch return false,`); + zigInternal.line(`};`); + zigInternal.line(`return true;`); + break; + } + zigInternal.dedent(); + zigInternal.line(`}`); + variNum += 1; + } + } + if (functions.length > 0) { + zig.line(); + } + for (const fn of functions) { + // Wrapper to init JSValue + const wrapperName = zid("create" + cap(fn.name) + "Callback"); + const minArgCount = fn.variants.reduce((acc, vari) => Math.min(acc, vari.args.length), Number.MAX_SAFE_INTEGER); + zig.line(`pub fn ${wrapperName}(global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue {`); + zig.line( + ` return JSC.NewRuntimeFunction(global, JSC.ZigString.static(${str(fn.name)}), ${minArgCount}, js${cap(fn.name)}, false, false, null);`, + ); + zig.line(`}`); + } + + if (typedefs.length > 0) { + zig.line(); + } + for (const td of typedefs) { + emitZigStruct(td.type); + } + + zig.dedent(); + zig.line(`};`); + zig.line(); +} + +cpp.line("} // namespace Generated"); +cpp.line(); +if (needsWebCore) { + cpp.line(`namespace WebCore {`); + cpp.line(); + for (const [type, reachableType] of typeHashToReachableType) { + switch (reachableType.kind) { + case "zigEnum": + case "stringEnum": + emitConvertEnumFunction(cpp, reachableType); + break; + } + } + cpp.line(`} // namespace WebCore`); + cpp.line(); +} + +zigInternal.dedent(); +zigInternal.line("};"); +zigInternal.line(); +zigInternal.line("comptime {"); +zigInternal.line(` if (bun.Environment.export_cpp_apis) {`); +zigInternal.line(" for (@typeInfo(binding_internals).Struct.decls) |decl| {"); +zigInternal.line(" _ = &@field(binding_internals, decl.name);"); +zigInternal.line(" }"); +zigInternal.line(" }"); +zigInternal.line("}"); + +writeIfNotChanged( + path.join(codegenRoot, "GeneratedBindings.cpp"), + [...headers].map(name => `#include ${str(name)}\n`).join("") + "\n" + cppInternal.buffer + "\n" + cpp.buffer, +); +writeIfNotChanged(path.join(src, "bun.js/bindings/GeneratedBindings.zig"), zig.buffer + zigInternal.buffer); + +// Headers +for (const [filename, { functions, typedefs }] of files) { + const namespaceVar = fileMap.get(filename)!; + const header = new CodeWriter(); + const headerIncludes = new Set(); + let needsWebCoreNamespace = false; + + headerIncludes.add("root.h"); + + header.line(`namespace {`); + header.line(); + for (const fn of functions) { + const externName = extJsFunction(namespaceVar, fn.name); + header.line(`extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject*, JSC::CallFrame*);`); + } + header.line(); + header.line(`} // namespace`); + header.line(); + + header.line(`namespace Generated {`); + header.line(); + header.line(`/// Generated binding code for src/${filename}`); + header.line(`namespace ${namespaceVar} {`); + header.line(); + for (const td of typedefs) { + emitCppStructHeader(header, td.type); + + switch (td.type.kind) { + case "zigEnum": + case "stringEnum": + case "dictionary": + needsWebCoreNamespace = true; + break; + } + } + for (const fn of functions) { + const externName = extJsFunction(namespaceVar, fn.name); + header.line(`constexpr auto* js${cap(fn.name)} = &${externName};`); + } + header.line(); + header.line(`} // namespace ${namespaceVar}`); + header.line(); + header.line(`} // namespace Generated`); + header.line(); + + if (needsWebCoreNamespace) { + header.line(`namespace WebCore {`); + header.line(); + for (const td of typedefs) { + switch (td.type.kind) { + case "zigEnum": + case "stringEnum": + headerIncludes.add("JSDOMConvertEnumeration.h"); + const basename = td.type.name(); + const name = `Generated::${namespaceVar}::${basename}`; + header.line(`// Implement WebCore::IDLEnumeration trait for ${basename}`); + header.line(`String convertEnumerationToString(${name});`); + header.line(`template<> JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, ${name});`); + header.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String&);`); + header.line( + `template<> std::optional<${name}> parseEnumeration<${name}>(JSC::JSGlobalObject&, JSC::JSValue);`, + ); + header.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>();`); + header.line(); + break; + case "dictionary": + // TODO: + // header.line(`// Implement WebCore::IDLDictionary trait for ${td.type.name()}`); + // header.line( + // "template<> FetchRequestInit convertDictionary(JSC::JSGlobalObject&, JSC::JSValue);", + // ); + // header.line(); + break; + default: + } + } + header.line(`} // namespace WebCore`); + } + + header.buffer = + "#pragma once\n" + [...headerIncludes].map(name => `#include ${str(name)}\n`).join("") + "\n" + header.buffer; + + writeIfNotChanged(path.join(codegenRoot, `Generated${pascal(namespaceVar)}.h`), header.buffer); +} diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 9a1d91d25a1baa..8cb8c59986b8f4 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -111,8 +111,7 @@ for (let i = 0; i < moduleList.length; i++) { true, x => requireTransformer(x, moduleList[i]), ); - let fileToTranspile = `// @ts-nocheck -// GENERATED TEMP FILE - DO NOT EDIT + let fileToTranspile = `// GENERATED TEMP FILE - DO NOT EDIT // Sourced from src/js/${moduleList[i]} ${importStatements.join("\n")} @@ -139,7 +138,8 @@ ${processed.result.slice(1).trim()} if (!fs.existsSync(path.dirname(outputPath))) { verbose("directory did not exist after mkdir twice:", path.dirname(outputPath)); } - // await Bun.sleep(10); + + fileToTranspile = "// @ts-nocheck\n" + fileToTranspile; try { await writeFile(outputPath, fileToTranspile); diff --git a/src/codegen/create-hash-table.ts b/src/codegen/create-hash-table.ts index e47c1d036ff13e..7c5bd88580d9ec 100644 --- a/src/codegen/create-hash-table.ts +++ b/src/codegen/create-hash-table.ts @@ -44,6 +44,7 @@ str = str.replaceAll(/^#include.*$/gm, ""); str = str.replaceAll(`namespace JSC {`, ""); str = str.replaceAll(`} // namespace JSC`, ""); str = str.replaceAll(/NativeFunctionType,\s([a-zA-Z0-99_]+)/gm, "NativeFunctionType, &$1"); +str = str.replaceAll('&Generated::', 'Generated::'); str = "#pragma once" + "\n" + "// File generated via `create-hash-table.ts`\n" + str.trim() + "\n"; writeIfNotChanged(output, str); diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index eb98745618cd13..7034c0b9852b6a 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -4,7 +4,7 @@ // For the actual parsing, see replacements.ts import path, { basename, sep } from "path"; -import { readdirRecursiveWithExclusionsAndExtensionsSync } from "./helpers"; +import { cap, readdirRecursiveWithExclusionsAndExtensionsSync } from "./helpers"; // interface NativeCall { @@ -25,7 +25,7 @@ interface WrapperCall { filename: string; } -type NativeCallType = "zig" | "cpp"; +type NativeCallType = "zig" | "cpp" | "bind"; const nativeCalls: NativeCall[] = []; const wrapperCalls: WrapperCall[] = []; @@ -33,7 +33,7 @@ const wrapperCalls: WrapperCall[] = []; const sourceFiles = readdirRecursiveWithExclusionsAndExtensionsSync( path.join(import.meta.dir, "../"), ["deps", "node_modules", "WebKit"], - [".cpp", ".zig"], + [".cpp", ".zig", ".bind.ts"], ); function callBaseName(x: string) { @@ -41,15 +41,15 @@ function callBaseName(x: string) { } function resolveNativeFileId(call_type: NativeCallType, filename: string) { - if (!filename.endsWith("." + call_type)) { - throw new Error( - `Expected filename for $${call_type} to have .${call_type} extension, got ${JSON.stringify(filename)}`, - ); + const ext = call_type === "bind" ? ".bind.ts" : `.${call_type}`; + if (!filename.endsWith(ext)) { + throw new Error(`Expected filename for $${call_type} to have ${ext} extension, got ${JSON.stringify(filename)}`); } const resolved = sourceFiles.find(file => file.endsWith(sep + filename)); if (!resolved) { - throw new Error(`Could not find file ${filename} in $${call_type} call`); + const fnName = call_type === "bind" ? "bindgenFn" : call_type; + throw new Error(`Could not find file ${filename} in $${fnName} call`); } if (call_type === "zig") { @@ -136,7 +136,7 @@ export function getJS2NativeCPP() { externs.push(`extern "C" SYSV_ABI JSC::EncodedJSValue ${symbol(call)}_workaround(Zig::GlobalObject*);` + "\n"), [ `static ALWAYS_INLINE JSC::JSValue ${symbol(call)}(Zig::GlobalObject* global) {`, - ` return JSValue::decode(${symbol(call)}_workaround(global));`, + ` return JSValue::decode(${symbol(call)}_workaround(global));`, `}` + "\n\n", ] ), @@ -180,10 +180,23 @@ export function getJS2NativeCPP() { "using namespace WebCore;" + "\n", ...nativeCallStrings, ...wrapperCallStrings, + ...nativeCalls + .filter(x => x.type === "bind") + .map( + x => + `extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(Zig::GlobalObject*);`, + ), `typedef JSC::JSValue (*JS2NativeFunction)(Zig::GlobalObject*);`, `static ALWAYS_INLINE JSC::JSValue callJS2Native(int32_t index, Zig::GlobalObject* global) {`, ` switch(index) {`, - ...nativeCalls.map(x => ` case ${x.id}: return ${symbol(x)}(global);`), + ...nativeCalls.map( + x => + ` case ${x.id}: return ${ + x.type === "bind" + ? `JSC::JSValue::decode(js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(global))` + : `${symbol(x)}(global)` + };`, + ), ` default:`, ` __builtin_unreachable();`, ` }`, @@ -196,7 +209,8 @@ export function getJS2NativeCPP() { export function getJS2NativeZig(gs2NativeZigPath: string) { return [ "//! This file is generated by src/codegen/generate-js2native.ts based on seen calls to the $zig() JS macro", - `const JSC = @import("root").bun.JSC;`, + `const bun = @import("root").bun;`, + `const JSC = bun.JSC;`, ...nativeCalls .filter(x => x.type === "zig") .flatMap(call => [ @@ -212,11 +226,22 @@ export function getJS2NativeZig(gs2NativeZigPath: string) { symbol: x.symbol_target, filename: x.filename, })}(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, JSC.toJSHostFunction(function.${x.symbol_target}), .{global, call_frame});`, + ` const function = @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), x.filename))});`, + ` return @call(.always_inline, JSC.toJSHostFunction(function.${x.symbol_target}), .{global, call_frame});`, "}", ]), + "comptime {", + ...nativeCalls + .filter(x => x.type === "bind") + .flatMap(x => { + const base = basename(x.filename.replace(/\.bind\.ts$/, "")); + return [ + ` @export(bun.gen.${base}.create${cap(x.symbol)}Callback, .{ .name = ${JSON.stringify( + `js2native_bindgen_${base}_${x.symbol}`, + )} });`, + ]; + }), + "}", ].join("\n"); } diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index bfd6cdafcc1b07..0b8ef644150620 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -107,7 +107,7 @@ export function readdirRecursiveWithExclusionsAndExtensionsSync( const fullPath = path.join(dir, entry.name); return entry.isDirectory() ? readdirRecursiveWithExclusionsAndExtensionsSync(fullPath, exclusions, exts) - : exts.includes(path.extname(fullPath)) + : exts.some(ext => fullPath.endsWith(ext)) ? fullPath : []; }); @@ -130,3 +130,26 @@ export function camelCase(string: string) { export function pascalCase(string: string) { return string.split(/[\s_]/).map((e, i) => (i ? e.charAt(0).toUpperCase() + e.slice(1) : e.toLowerCase())); } + +export function argParse(keys: string[]): any { + const options = {}; + for (const arg of process.argv.slice(2)) { + if (!arg.startsWith("--")) { + console.error("Unknown argument " + arg); + process.exit(1); + } + const split = arg.split("="); + const value = split[1] || "true"; + options[split[0].slice(2)] = value; + } + + const unknown = new Set(Object.keys(options)); + for (const key of keys) { + unknown.delete(key); + } + for (const key of unknown) { + console.error("Unknown argument: --" + key); + } + if (unknown.size > 0) process.exit(1); + return options; +} \ No newline at end of file diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index 025f0f854d126c..86da43ffaea040 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -141,10 +141,16 @@ export interface ReplacementRule { } export const function_replacements = [ - "$debug", "$assert", "$zig", "$newZigFunction", "$cpp", "$newCppFunction", + "$debug", + "$assert", + "$zig", + "$newZigFunction", + "$cpp", + "$newCppFunction", "$isPromiseResolved", + "$bindgenFn", ]; -const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll('$', '')})`); +const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll("$", "")})`); /** Applies source code replacements as defined in `replacements` */ export function applyReplacements(src: string, length: number) { @@ -155,10 +161,7 @@ export function applyReplacements(src: string, length: number) { slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__").replaceAll("%", "$")); } let match; - if ( - (match = slice.match(function_regexp)) && - rest.startsWith("(") - ) { + if ((match = slice.match(function_regexp)) && rest.startsWith("(")) { const name = match[1]; if (name === "debug") { const innerSlice = sliceSourceCode(rest, true); @@ -233,9 +236,31 @@ export function applyReplacements(src: string, length: number) { // use a property on @lazy as a temporary holder for the expression. only in debug! args = `($assert(__intrinsic__isPromise(__intrinsic__lazy.temp=${inner.result.slice(0, -1)}))),(__intrinsic__getPromiseInternalField(__intrinsic__lazy.temp, __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === (__intrinsic__lazy.temp = undefined, __intrinsic__promiseStateFulfilled))`; } else { - args = `((__intrinsic__getPromiseInternalField(${inner.result.slice(0,-1)}), __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === __intrinsic__promiseStateFulfilled)`; + args = `((__intrinsic__getPromiseInternalField(${inner.result.slice(0, -1)}), __intrinsic__promiseFieldFlags) & __intrinsic__promiseStateMask) === __intrinsic__promiseStateFulfilled)`; } return [slice.slice(0, match.index) + args, inner.rest, true]; + } else if (name === "bindgenFn") { + const inner = sliceSourceCode(rest, true); + let args; + try { + const str = + "[" + + inner.result + .slice(1, -1) + .replaceAll("'", '"') + .replace(/,[\s\n]*$/s, "") + + "]"; + args = JSON.parse(str); + } catch { + throw new Error(`Call is not known at bundle-time: '$${name}${inner.result}'`); + } + if (args.length != 2 || typeof args[0] !== "string" || typeof args[1] !== "string") { + throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); + } + + const id = registerNativeCall("bind", args[0], args[1], undefined); + + return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else { throw new Error("Unknown preprocessor macro " + name); } diff --git a/src/env.zig b/src/env.zig index bbc36aba6fb924..482d7d50589b38 100644 --- a/src/env.zig +++ b/src/env.zig @@ -19,7 +19,6 @@ pub const isBrowser = !isWasi and isWasm; pub const isWindows = @import("builtin").target.os.tag == .windows; pub const isPosix = !isWindows and !isWasm; pub const isDebug = std.builtin.Mode.Debug == @import("builtin").mode; -pub const isRelease = std.builtin.Mode.Debug != @import("builtin").mode and !isTest; pub const isTest = @import("builtin").is_test; pub const isLinux = @import("builtin").target.os.tag == .linux; pub const isAarch64 = @import("builtin").target.cpu.arch.isAARCH64(); @@ -28,6 +27,11 @@ pub const isX64 = @import("builtin").target.cpu.arch == .x86_64; pub const isMusl = builtin.target.abi.isMusl(); pub const allow_assert = isDebug or isTest or std.builtin.Mode.ReleaseSafe == @import("builtin").mode; +/// All calls to `@export` should be gated behind this check, so that code +/// generators that compile Zig code know not to reference and compile a ton of +/// unused code. +pub const export_cpp_apis = @import("builtin").output_mode == .Obj; + pub const build_options = @import("build_options"); pub const reported_nodejs_version = build_options.reported_nodejs_version; diff --git a/src/fmt.bind.ts b/src/fmt.bind.ts new file mode 100644 index 00000000000000..cae52da42c2d09 --- /dev/null +++ b/src/fmt.bind.ts @@ -0,0 +1,15 @@ +import { fn, t } from "bindgen"; + +const implNamespace = "js_bindings"; + +export const Formatter = t.stringEnum("highlight-javascript", "escape-powershell"); + +export const fmtString = fn({ + implNamespace, + args: { + global: t.globalObject, + code: t.UTF8String, + formatter: Formatter, + }, + ret: t.DOMString, +}); diff --git a/src/fmt.zig b/src/fmt.zig index c42dc178b1c4c6..73277701cc7f76 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1717,51 +1717,37 @@ fn escapePowershellImpl(str: []const u8, comptime f: []const u8, _: std.fmt.Form try writer.writeAll(remain); } -pub const fmt_js_test_bindings = struct { - const Formatter = enum { - fmtJavaScript, - escapePowershell, - }; +pub const js_bindings = struct { + const gen = bun.gen.fmt; /// Internal function for testing in highlighter.test.ts - pub fn jsFunctionStringFormatter(globalThis: *bun.JSC.JSGlobalObject, callframe: *bun.JSC.CallFrame) bun.JSError!bun.JSC.JSValue { - const args = callframe.arguments_old(2); - if (args.len < 2) { - return globalThis.throwNotEnoughArguments("code", 1, 0); - } - - const code = try args.ptr[0].toSliceOrNull(globalThis); - defer code.deinit(); - + pub fn fmtString(global: *bun.JSC.JSGlobalObject, code: []const u8, formatter_id: gen.Formatter) bun.JSError!bun.String { var buffer = bun.MutableString.initEmpty(bun.default_allocator); defer buffer.deinit(); var writer = buffer.bufferedWriter(); - const formatter_id: Formatter = @enumFromInt(args.ptr[1].toInt32()); switch (formatter_id) { - .fmtJavaScript => { - const formatter = bun.fmt.fmtJavaScript(code.slice(), .{ + .highlight_javascript => { + const formatter = bun.fmt.fmtJavaScript(code, .{ .enable_colors = true, .check_for_unhighlighted_write = false, }); std.fmt.format(writer.writer(), "{}", .{formatter}) catch |err| { - return globalThis.throwError(err, "Error formatting"); + return global.throwError(err, "while formatting"); }; }, - .escapePowershell => { - std.fmt.format(writer.writer(), "{}", .{escapePowershell(code.slice())}) catch |err| { - return globalThis.throwError(err, "Error formatting"); + .escape_powershell => { + std.fmt.format(writer.writer(), "{}", .{escapePowershell(code)}) catch |err| { + return global.throwError(err, "while formatting"); }; }, } writer.flush() catch |err| { - return globalThis.throwError(err, "Error formatting"); + return global.throwError(err, "while formatting"); }; - var str = bun.String.createUTF8(buffer.list.items); - defer str.deref(); - return str.toJS(globalThis); + return bun.String.createUTF8(buffer.list.items); } }; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index b21d2f330c10c5..0cfaa5507ef147 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -5,15 +5,10 @@ // In a debug build, the import is always allowed. // It is disallowed in release builds unless run in Bun's CI. -/// +const fmtBinding = $bindgenFn("fmt.bind.ts", "fmtString"); -const fmtBinding = $newZigFunction("fmt.zig", "fmt_js_test_bindings.jsFunctionStringFormatter", 2) as ( - code: string, - id: number, -) => string; - -export const quickAndDirtyJavaScriptSyntaxHighlighter = (code: string) => fmtBinding(code, 0); -export const escapePowershell = (code: string) => fmtBinding(code, 1); +export const highlightJavaScript = (code: string) => fmtBinding(code, "highlight-javascript"); +export const escapePowershell = (code: string) => fmtBinding(code, "escape-powershell"); export const TLSBinding = $cpp("NodeTLS.cpp", "createNodeTLSBinding"); @@ -146,6 +141,11 @@ export const isModuleResolveFilenameSlowPathEnabled: () => boolean = $newCppFunc export const frameworkRouterInternals = $zig("FrameworkRouter.zig", "JSFrameworkRouter.getBindings") as { parseRoutePattern: (style: string, pattern: string) => null | { kind: string; pattern: string }; FrameworkRouter: { - new(opts: any): any; + new (opts: any): any; }; }; + +export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as { + add: (a: any, b: any) => number; + requiredAndOptionalArg: (a: any, b?: any, c?: any, d?: any) => number; +}; diff --git a/src/js/internal/test/binding.ts b/src/js/internal/test/binding.ts new file mode 100644 index 00000000000000..a6240072ed7a7b --- /dev/null +++ b/src/js/internal/test/binding.ts @@ -0,0 +1,77 @@ +function internalBinding(name: string) { + switch (name) { + case "async_wrap": + case "buffer": + case "cares_wrap": + case "constants": + case "contextify": + case "config": + case "fs": + case "fs_event_wrap": + case "http_parser": + case "inspector": + case "os": + case "pipe_wrap": + case "process_wrap": + case "signal_wrap": + case "tcp_wrap": + case "tty_wrap": + case "udp_wrap": + case "url": + case "util": + case "uv": + case "v8": + case "zlib": + case "js_stream": { + // Public bindings + return (process as any).binding(name); + } + + case "blob": + case "block_list": + case "builtins": + case "credentials": + case "encoding_binding": + case "errors": + case "fs_dir": + case "heap_utils": + case "http2": + case "internal_only_v8": + case "js_udp_wrap": + case "messaging": + case "modules": + case "module_wrap": + case "mksnapshot": + case "options": + case "performance": + case "permission": + case "process_methods": + case "report": + case "sea": + case "serdes": + case "spawn_sync": + case "stream_pipe": + case "stream_wrap": + case "string_decoder": + case "symbols": + case "task_queue": + case "timers": + case "trace_events": + case "types": + case "wasi": + case "wasm_web_api": + case "watchdog": + case "worker": { + // Private bindings + throw new Error( + `Bun does not implement internal binding: ${name}. This being a node.js internal, it will not be implemented outside of usage in Node.js' test suite.`, + ); + } + + default: { + throw new Error(`No such binding: ${name}`); + } + } +} + +export { internalBinding }; diff --git a/src/js/node/async_hooks.ts b/src/js/node/async_hooks.ts index db0f0b82720c59..5f109ae3eabf31 100644 --- a/src/js/node/async_hooks.ts +++ b/src/js/node/async_hooks.ts @@ -23,6 +23,7 @@ // calls to $assert which will verify this invariant (only during bun-debug) // const [setAsyncHooksEnabled, cleanupLater] = $cpp("NodeAsyncHooks.cpp", "createAsyncHooksBinding"); +const { validateFunction, validateString } = require("internal/validators"); // Only run during debug function assertValidAsyncContextArray(array: unknown): array is ReadonlyArray | undefined { @@ -89,6 +90,7 @@ class AsyncLocalStorage { } static bind(fn, ...args: any) { + validateFunction(fn); return this.snapshot().bind(null, fn, ...args); } @@ -234,6 +236,14 @@ class AsyncLocalStorage { if (context[i] === this) return context[i + 1]; } } + + // Node.js internal function. In Bun's implementation, calling this is not + // observable from outside the AsyncLocalStorage implementation. + _enable() {} + + // Node.js internal function. In Bun's implementation, calling this is not + // observable from outside the AsyncLocalStorage implementation. + _propagate(resource, triggerResource, type) {} } if (IS_BUN_DEVELOPMENT) { @@ -251,9 +261,7 @@ class AsyncResource { #snapshot; constructor(type, options?) { - if (typeof type !== "string") { - throw new TypeError('The "type" argument must be of type string. Received type ' + typeof type); - } + validateString(type, "type"); setAsyncHooksEnabled(true); this.type = type; this.#snapshot = get(); @@ -320,11 +328,10 @@ function createWarning(message, isCreateHook?: boolean) { // times bundled into a framework or application. Their use defines three // handlers which are all TODO stubs. for more info see this comment: // https://github.com/oven-sh/bun/issues/13866#issuecomment-2397896065 - if (typeof arg1 === 'object') { + if (typeof arg1 === "object") { const { init, promiseResolve, destroy } = arg1; if (init && promiseResolve && destroy) { - if (isEmptyFunction(init) && isEmptyFunction(destroy)) - return; + if (isEmptyFunction(init) && isEmptyFunction(destroy)) return; } } } @@ -337,8 +344,8 @@ function createWarning(message, isCreateHook?: boolean) { function isEmptyFunction(f: Function) { let str = f.toString(); - if(!str.startsWith('function()'))return false; - str = str.slice('function()'.length).trim(); + if (!str.startsWith("function()")) return false; + str = str.slice("function()".length).trim(); return /^{\s*}$/.test(str); } diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 2b8d7d5bf6b70b..825d4a68fd3c0e 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -173,3 +173,8 @@ declare function $newZigFunction any>( symbol: string, argCount: number, ): T; +/** + * @param filename - The basename of the `.bind.ts` file. + * @param symbol - The name of the function to call. + */ +declare function $bindgenFn any>(filename: string, symbol: string): T; diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 9726e991c7d600..a2fd51e7dcd8a7 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,25 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["ESNext", "DOM"], - "module": "ESNext", - "isolatedModules": true, - "noEmit": true, - "emitDeclarationOnly": false, + // Path remapping + "baseUrl": ".", "paths": { - "internal/*": ["./internal/*"] //deprecated + "internal/*": ["./js/internal/*"] //deprecated } }, - "include": [ - // - "./node", - "./bun", - "./builtins", - "./functions", - "./internal", - "./thirdparty", - "./builtins.d.ts", - "./private.d.ts", - "../../build/codegen/WebCoreJSBuiltins.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", "./builtins.d.ts", "./private.d.ts"] } diff --git a/src/jsc_stub.zig b/src/jsc_stub.zig index 3679179f3c4cd9..34069b04a21ad9 100644 --- a/src/jsc_stub.zig +++ b/src/jsc_stub.zig @@ -1,5 +1,4 @@ // For WASM builds -pub const is_bindgen = true; pub const C = struct {}; pub const WebCore = struct {}; pub const Jest = struct {}; diff --git a/src/output.zig b/src/output.zig index aa25b4530146d2..ec0b60ac2101b4 100644 --- a/src/output.zig +++ b/src/output.zig @@ -468,16 +468,16 @@ pub fn isVerbose() bool { return false; } -var _source_for_test: if (Environment.isTest) Source else void = undefined; -var _source_for_test_set = false; -pub fn initTest() void { - if (_source_for_test_set) return; - _source_for_test_set = true; - const in = std.io.getStdErr(); - const out = std.io.getStdOut(); - _source_for_test = Source.init(File.from(out), File.from(in)); - Source.set(&_source_for_test); -} +// var _source_for_test: if (Environment.isTest) Source else void = undefined; +// var _source_for_test_set = false; +// pub fn initTest() void { +// if (_source_for_test_set) return; +// _source_for_test_set = true; +// const in = std.io.getStdErr(); +// const out = std.io.getStdOut(); +// _source_for_test = Source.init(File.from(out), File.from(in)); +// Source.set(&_source_for_test); +// } pub fn enableBuffering() void { if (comptime Environment.isNative) enable_buffering = true; } @@ -674,7 +674,7 @@ pub noinline fn println(comptime fmt: string, args: anytype) void { /// Print to stdout, but only in debug builds. /// Text automatically buffers pub fn debug(comptime fmt: string, args: anytype) void { - if (comptime Environment.isRelease) return; + if (!Environment.isDebug) return; prettyErrorln("DEBUG: " ++ fmt, args); flush(); } diff --git a/src/shell/braces.zig b/src/shell/braces.zig index 26ccec354fbeb9..3532be44f86520 100644 --- a/src/shell/braces.zig +++ b/src/shell/braces.zig @@ -66,7 +66,6 @@ pub fn StackStack(comptime T: type, comptime SizeType: type, comptime N: SizeTyp len: SizeType = 0, pub const Error = error{ - StackEmpty, StackFull, }; @@ -159,7 +158,7 @@ pub fn expand( tokens: []Token, out: []std.ArrayList(u8), contains_nested: bool, -) (error{ StackFull, StackEmpty } || ParserError)!void { +) (error{StackFull} || ParserError)!void { var out_key_counter: u16 = 1; if (!contains_nested) { var expansions_table = try buildExpansionTableAlloc(allocator, tokens); diff --git a/src/string.zig b/src/string.zig index 3b2dab9fcb3c15..054fe37e513da9 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1481,3 +1481,8 @@ pub const SliceWithUnderlyingString = struct { return this.underlying.toJS(globalObject); } }; + +comptime { + bun.assert_eql(@sizeOf(bun.String), 24); + bun.assert_eql(@alignOf(bun.String), 8); +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 00000000000000..927851b4f9a144 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + // Path remapping + "baseUrl": ".", + "paths": { + "bindgen": ["./codegen/bindgen-lib.ts"], + } + }, + "include": ["**/*.ts", "**/*.tsx"], + // separate projects have extra settings that only apply in those scopes + "exclude": ["js", "bake"] +} diff --git a/test/bake/dev-server-harness.ts b/test/bake/dev-server-harness.ts index 49cf8f572ed3c3..d937938bf68b8b 100644 --- a/test/bake/dev-server-harness.ts +++ b/test/bake/dev-server-harness.ts @@ -8,7 +8,7 @@ import { test } from "bun:test"; import { EventEmitter } from "node:events"; // @ts-ignore import { dedent } from "../bundler/expectBundled.ts"; -import { bunEnv, isWindows, mergeWindowEnvs } from "harness"; +import { bunEnv, isCI, isWindows, mergeWindowEnvs } from "harness"; import { expect } from "bun:test"; /** For testing bundler related bugs in the DevServer */ @@ -27,28 +27,31 @@ export const minimalFramework: Bake.Framework = { }, }; -export type DevServerTest = ({ - /** Starting files */ - files: FileObject; - /** - * Framework to use. Consider `minimalFramework` if possible. - * Provide this object or `files['bun.app.ts']` for a dynamic one. - */ - framework?: Bake.Framework | "react"; - /** - * Source code for a TSX file that `export default`s an array of BunPlugin, - * combined with the `framework` option. - */ - pluginFile?: string; -} | { - /** - * Copy all files from test/bake/fixtures/ - * This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .` - */ - fixture: string; -}) & { +export type DevServerTest = ( + | { + /** Starting files */ + files: FileObject; + /** + * Framework to use. Consider `minimalFramework` if possible. + * Provide this object or `files['bun.app.ts']` for a dynamic one. + */ + framework?: Bake.Framework | "react"; + /** + * Source code for a TSX file that `export default`s an array of BunPlugin, + * combined with the `framework` option. + */ + pluginFile?: string; + } + | { + /** + * Copy all files from test/bake/fixtures/ + * This directory must contain `bun.app.ts` to allow hacking on fixtures manually via `bun run .` + */ + fixture: string; + } +) & { test: (dev: Dev) => Promise; -} +}; type FileObject = Record; @@ -74,9 +77,9 @@ export class Dev { } fetch(url: string, init?: RequestInit) { - return new DevFetchPromise((resolve, reject) => - fetch(new URL(url, this.baseUrl).toString(), init).then(resolve, reject), - this + return new DevFetchPromise( + (resolve, reject) => fetch(new URL(url, this.baseUrl).toString(), init).then(resolve, reject), + this, ); } @@ -120,7 +123,10 @@ export class Dev { await Promise.race([ // On failure, give a little time in case a partial write caused a // bundling error, and a success came in. - err.then(() => Bun.sleep(500), () => {}), + err.then( + () => Bun.sleep(500), + () => {}, + ), success, ]); } @@ -138,7 +144,10 @@ export interface Step { class DevFetchPromise extends Promise { dev: Dev; - constructor(executor: (resolve: (value: Response | PromiseLike) => void, reject: (reason?: any) => void) => void, dev: Dev) { + constructor( + executor: (resolve: (value: Response | PromiseLike) => void, reject: (reason?: any) => void) => void, + dev: Dev, + ) { super(executor); this.dev = dev; } @@ -326,15 +335,15 @@ export function devTest(description: string, options: T const basename = path.basename(caller, ".test" + path.extname(caller)); const count = (counts[basename] = (counts[basename] ?? 0) + 1); - // TODO: Tests are too flaky on Windows. Cannot reproduce locally. - if (isWindows) { + // TODO: Tests are flaky on all platforms. Disable + if (isCI) { jest.test.todo(`DevServer > ${basename}.${count}: ${description}`); return options; } jest.test(`DevServer > ${basename}.${count}: ${description}`, async () => { const root = path.join(tempDir, basename + count); - if ('files' in options) { + if ("files" in options) { writeAll(root, options.files); if (options.files["bun.app.ts"] == undefined) { if (!options.framework) { @@ -346,9 +355,7 @@ export function devTest(description: string, options: T fs.writeFileSync( path.join(root, "bun.app.ts"), dedent` - ${options.pluginFile ? - `import plugins from './pluginFile.ts';` : "let plugins = undefined;" - } + ${options.pluginFile ? `import plugins from './pluginFile.ts';` : "let plugins = undefined;"} export default { app: { framework: ${JSON.stringify(options.framework)}, @@ -369,8 +376,8 @@ export function devTest(description: string, options: T const fixture = path.join(devTestRoot, "../fixtures", options.fixture); fs.cpSync(fixture, root, { recursive: true }); - if(!fs.existsSync(path.join(root, "bun.app.ts"))) { - throw new Error(`Fixture ${fixture} must contain a bun.app.ts file.`); + if (!fs.existsSync(path.join(root, "bun.app.ts"))) { + throw new Error(`Fixture ${fixture} must contain a bun.app.ts file.`); } if (!fs.existsSync(path.join(root, "node_modules"))) { // link the node_modules directory from test/node_modules to the temp directory diff --git a/test/http-test-server.ts b/test/http-test-server.ts index 4b94371f8fcf53..17413ba0007a14 100644 --- a/test/http-test-server.ts +++ b/test/http-test-server.ts @@ -67,6 +67,7 @@ function makeTestJsonResponse( } // Check to set headers headers.set("Content-Type", "text/plain"); + break; default: } diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts new file mode 100644 index 00000000000000..4129b9dd2e9f49 --- /dev/null +++ b/test/internal/bindgen.test.ts @@ -0,0 +1,70 @@ +import { bindgen } from "bun:internal-for-testing"; + +it("bindgen add example", () => { + // Simple cases + expect(bindgen.add(5, 3)).toBe(8); + expect(bindgen.add(-2, 7)).toBe(5); + expect(bindgen.add(0, 0)).toBe(0); + // https://tc39.es/ecma262/multipage/bigint-object.html#sec-tonumber + // 2. If argument is either a Symbol or a BigInt, throw a TypeError exception. + expect(() => bindgen.add(1n, 0)).toThrow("Conversion from 'BigInt' to 'number' is not allowed"); + expect(() => bindgen.add(Symbol("1"), 0)).toThrow("Cannot convert a symbol to a number"); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 3. If argument is null or false, return +0. + expect(bindgen.add(null, "32")).toBe(32); + expect(bindgen.add(false, "32")).toBe(32); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 3. If argument is undefined, return NaN. + // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + expect(bindgen.add(undefined, "32")).toBe(32); + expect(bindgen.add(NaN, "32")).toBe(32); + expect(bindgen.add(Infinity, "32")).toBe(32); + expect(bindgen.add(-Infinity, "32")).toBe(32); + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber + // 5. If argument is true, return 1. + expect(bindgen.add(true, "32")).toBe(33); + // https://tc39.es/ecma262/multipage/bigint-object.html#sec-tonumber + // 6. If argument is a String, return StringToNumber(argument). + expect(bindgen.add("1", "1")).toBe(2); + // 8. Let primValue be ? ToPrimitive(argument, number). + // 10. Return ? ToNumber(primValue). + expect(bindgen.add({ [Symbol.toPrimitive]: () => "1" }, "1")).toBe(2); + + expect(bindgen.add(2147483647.9, 0)).toBe(2147483647); + expect(bindgen.add(2147483647.1, 0)).toBe(2147483647); + + // Out of range wrapping behaviors. By adding `0`, this acts as an identity function. + // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + expect(bindgen.add(2147483648, 0)).toBe(-2147483648); + expect(bindgen.add(5555555555, 0)).toBe(1260588259); + expect(bindgen.add(-5555555555, 0)).toBe(-1260588259); + expect(bindgen.add(55555555555, 0)).toBe(-279019293); + expect(bindgen.add(-55555555555, 0)).toBe(279019293); + expect(bindgen.add(555555555555, 0)).toBe(1504774371); + expect(bindgen.add(-555555555555, 0)).toBe(-1504774371); + expect(bindgen.add(5555555555555, 0)).toBe(-2132125469); + expect(bindgen.add(-5555555555555, 0)).toBe(2132125469); + + // Test Zig error handling + expect(() => bindgen.add(2147483647, 1)).toThrow("Integer overflow while adding"); +}); + +it("optional arguments / default arguments", () => { + expect(bindgen.requiredAndOptionalArg(false)).toBe(123498); + expect(bindgen.requiredAndOptionalArg(false, 10)).toBe(52); + expect(bindgen.requiredAndOptionalArg(true, 10)).toBe(-52); + expect(bindgen.requiredAndOptionalArg(1, 10, 5)).toBe(-15); + expect(bindgen.requiredAndOptionalArg("coerce to true", 10, 5)).toBe(-15); + expect(bindgen.requiredAndOptionalArg("", 10, 5)).toBe(15); + expect(bindgen.requiredAndOptionalArg(true, 10, 5, 2)).toBe(-30); + expect(bindgen.requiredAndOptionalArg(true, null, 5, 2)).toBe(123463); +}); + +it("custom enforceRange boundaries", () => { + expect(bindgen.requiredAndOptionalArg(false, 0, 5)).toBe(5); + expect(() => bindgen.requiredAndOptionalArg(false, 0, -1)).toThrow("Value -1 is outside the range [0, 100]"); + expect(() => bindgen.requiredAndOptionalArg(false, 0, 101)).toThrow("Value 101 is outside the range [0, 100]"); + expect(bindgen.requiredAndOptionalArg(false, 0, 100)).toBe(100); + expect(bindgen.requiredAndOptionalArg(false, 0, 0)).toBe(0); +}); diff --git a/test/internal/highlighter.test.ts b/test/internal/highlighter.test.ts index e45e73ca9fc3a6..c1af283f1db89c 100644 --- a/test/internal/highlighter.test.ts +++ b/test/internal/highlighter.test.ts @@ -1,4 +1,4 @@ -import { quickAndDirtyJavaScriptSyntaxHighlighter as highlighter } from "bun:internal-for-testing"; +import { highlightJavaScript as highlighter } from "bun:internal-for-testing"; import { expect, test } from "bun:test"; test("highlighter", () => { diff --git a/test/js/bun/util/highlighter.test.ts b/test/js/bun/util/highlighter.test.ts index e45e73ca9fc3a6..c1af283f1db89c 100644 --- a/test/js/bun/util/highlighter.test.ts +++ b/test/js/bun/util/highlighter.test.ts @@ -1,4 +1,4 @@ -import { quickAndDirtyJavaScriptSyntaxHighlighter as highlighter } from "bun:internal-for-testing"; +import { highlightJavaScript as highlighter } from "bun:internal-for-testing"; import { expect, test } from "bun:test"; test("highlighter", () => { diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 38a48e89014ad4..9a044595e82f34 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -384,6 +384,57 @@ if (global.Storage) { ); } +if (global.Bun) { + knownGlobals.push( + global.addEventListener, + global.alert, + global.confirm, + global.dispatchEvent, + global.postMessage, + global.prompt, + global.removeEventListener, + global.reportError, + global.Bun, + global.File, + global.process, + global.Blob, + global.Buffer, + global.BuildError, + global.BuildMessage, + global.HTMLRewriter, + global.Request, + global.ResolveError, + global.ResolveMessage, + global.Response, + global.TextDecoder, + global.AbortSignal, + global.BroadcastChannel, + global.CloseEvent, + global.DOMException, + global.ErrorEvent, + global.Event, + global.EventTarget, + global.FormData, + global.Headers, + global.MessageChannel, + global.MessageEvent, + global.MessagePort, + global.PerformanceEntry, + global.PerformanceObserver, + global.PerformanceObserverEntryList, + global.PerformanceResourceTiming, + global.PerformanceServerTiming, + global.PerformanceTiming, + global.TextEncoder, + global.URL, + global.URLSearchParams, + global.WebSocket, + global.Worker, + global.onmessage, + global.onerror + ); +} + function allowGlobals(...allowlist) { knownGlobals = knownGlobals.concat(allowlist); } diff --git a/test/js/node/test/find-new-passes.ts b/test/js/node/test/find-new-passes.ts new file mode 100644 index 00000000000000..48c90c33e25d4c --- /dev/null +++ b/test/js/node/test/find-new-passes.ts @@ -0,0 +1,61 @@ +import path from "path"; +import fs from "fs"; +import { spawn } from "child_process"; + +const localDir = path.resolve(import.meta.dirname, "./parallel"); +const upstreamDir = path.resolve(import.meta.dirname, "../../../node.js/upstream/test/parallel"); + +const localFiles = fs.readdirSync(localDir); +const upstreamFiles = fs.readdirSync(upstreamDir); + +const newFiles = upstreamFiles.filter((file) => !localFiles.includes(file)); + +process.on('SIGTERM', () => { + console.log("SIGTERM received"); +}); +process.on('SIGINT', () => { + console.log("SIGINT received"); +}); + +const stdin = process.stdin; +if (stdin.isTTY) { + stdin.setRawMode(true); + stdin.on('data', (data) => { + if (data[0] === 0x03) { + stdin.setRawMode(false); + console.log("Cancelled"); + process.exit(0); + } + }); +} +process.on('exit', () => { + if (stdin.isTTY) { + stdin.setRawMode(false); + } +}); + +for (const file of newFiles) { + await new Promise((resolve, reject) => { + // Run with a timeout of 5 seconds + const proc = spawn("bun-debug", ["run", path.join(upstreamDir, file)], { + timeout: 5000, + stdio: "inherit", + env: { + ...process.env, + BUN_DEBUG_QUIET_LOGS: "1", + }, + }); + + proc.on("error", (err) => { + console.error(err); + }); + + proc.on("exit", (code) => { + if (code === 0) { + console.log(`New Pass: ${file}`); + fs.appendFileSync("new-passes.txt", file + "\n"); + } + resolve(); + }); + }); +} diff --git a/test/js/node/test/parallel/test-async-local-storage-bind.js b/test/js/node/test/parallel/test-async-local-storage-bind.js new file mode 100644 index 00000000000000..d8d4c4599826f9 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-bind.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +[1, false, '', {}, []].forEach((i) => { + assert.throws(() => AsyncLocalStorage.bind(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const fn = common.mustCall(AsyncLocalStorage.bind(() => 123)); +assert.strictEqual(fn(), 123); + +const fn2 = AsyncLocalStorage.bind(common.mustCall((arg) => assert.strictEqual(arg, 'test'))); +fn2('test'); diff --git a/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js b/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js new file mode 100644 index 00000000000000..61a339724008d5 --- /dev/null +++ b/test/js/node/test/parallel/test-async-local-storage-exit-does-not-leak.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const als = new AsyncLocalStorage(); + +// The _propagate function only exists on the old JavaScript implementation. +if (typeof als._propagate === 'function') { + // The als instance should be getting removed from the storageList in + // lib/async_hooks.js when exit(...) is called, therefore when the nested runs + // are called there should be no copy of the als in the storageList to run the + // _propagate method on. + als._propagate = common.mustNotCall('_propagate() should not be called'); +} + +const done = common.mustCall(); + +const data = true; + +function run(count) { + if (count === 0) return done(); + assert.notStrictEqual(als.getStore(), data); + als.run(data, () => { + als.exit(run, --count); + }); +} +run(100); diff --git a/test/js/node/test/parallel/test-binding-constants.js b/test/js/node/test/parallel/test-binding-constants.js new file mode 100644 index 00000000000000..4a96b7c7443fc6 --- /dev/null +++ b/test/js/node/test/parallel/test-binding-constants.js @@ -0,0 +1,33 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const { internalBinding } = require('internal/test/binding'); +const constants = internalBinding('constants'); +const assert = require('assert'); + +assert.deepStrictEqual( + Object.keys(constants).sort(), ['crypto', 'fs', 'os', 'trace', 'zlib'] +); + +assert.deepStrictEqual( + Object.keys(constants.os).sort(), ['UV_UDP_REUSEADDR', 'dlopen', 'errno', + 'priority', 'signals'] +); + +// Make sure all the constants objects don't inherit from Object.prototype +const inheritedProperties = Object.getOwnPropertyNames(Object.prototype); +function test(obj) { + assert(obj); + assert.strictEqual(Object.prototype.toString.call(obj), '[object Object]'); + assert.strictEqual(Object.getPrototypeOf(obj), null); + + inheritedProperties.forEach((property) => { + assert.strictEqual(property in obj, false); + }); +} + +[ + constants, constants.crypto, constants.fs, constants.os, constants.trace, + constants.zlib, constants.os.dlopen, constants.os.errno, constants.os.signals, +].forEach(test); diff --git a/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js b/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js new file mode 100644 index 00000000000000..afdbdb16364c03 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-types-symlinks.js @@ -0,0 +1,36 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/52663 +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +if (!common.canCreateSymLink()) + common.skip('insufficient privileges'); + +const tmpdir = require('../common/tmpdir'); +const readdirDir = tmpdir.path; +// clean up the tmpdir +tmpdir.refresh(); + +// a/1, a/2 +const a = path.join(readdirDir, 'a'); +fs.mkdirSync(a); +fs.writeFileSync(path.join(a, '1'), 'irrelevant'); +fs.writeFileSync(path.join(a, '2'), 'irrelevant'); + +// b/1 +const b = path.join(readdirDir, 'b'); +fs.mkdirSync(b); +fs.writeFileSync(path.join(b, '1'), 'irrelevant'); + +// b/c -> a +const c = path.join(readdirDir, 'b', 'c'); +fs.symlinkSync(a, c, 'dir'); + +// Just check that the number of entries are the same +assert.strictEqual( + fs.readdirSync(b, { recursive: true, withFileTypes: true }).length, + fs.readdirSync(b, { recursive: true, withFileTypes: false }).length +); diff --git a/test/js/node/test/parallel/test-http2-large-write-multiple-requests.js b/test/js/node/test/parallel/test-http2-large-write-multiple-requests.js deleted file mode 100644 index bcbb1434cbec91..00000000000000 --- a/test/js/node/test/parallel/test-http2-large-write-multiple-requests.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -// This tests that the http2 server sends data early when it accumulates -// enough from ongoing requests to avoid DoS as mitigation for -// CVE-2019-9517 and CVE-2019-9511. -// Added by https://github.com/nodejs/node/commit/8a4a193 -const fixtures = require('../common/fixtures'); -const assert = require('assert'); -const http2 = require('http2'); - -const content = fixtures.readSync('person-large.jpg'); - -const server = http2.createServer({ - maxSessionMemory: 1000 -}); -let streamCount = 0; -server.on('stream', (stream, headers) => { - stream.respond({ - 'content-type': 'image/jpeg', - ':status': 200 - }); - stream.end(content); - console.log('server sends content', ++streamCount); -}); - -server.listen(0, common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}/`); - - let endCount = 0; - let finished = 0; - for (let i = 0; i < 100; i++) { - const req = client.request({ ':path': '/' }).end(); - const chunks = []; - req.on('data', (chunk) => { - chunks.push(chunk); - }); - req.on('end', common.mustCall(() => { - console.log('client receives content', ++endCount); - assert.deepStrictEqual(Buffer.concat(chunks), content); - - if (++finished === 100) { - client.close(); - server.close(); - } - })); - req.on('error', (e) => { - console.log('client error', e); - }); - } -})); diff --git a/test/js/node/test/parallel/test-net-connect-abort-controller.js b/test/js/node/test/parallel/test-net-connect-abort-controller.js new file mode 100644 index 00000000000000..9c259cc3fc2c15 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-abort-controller.js @@ -0,0 +1,96 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); +const server = net.createServer(); +const { getEventListeners, once } = require('events'); + +const liveConnections = new Set(); + +server.listen(0, common.mustCall(async () => { + const port = server.address().port; + const host = 'localhost'; + const socketOptions = (signal) => ({ port, host, signal }); + server.on('connection', (connection) => { + liveConnections.add(connection); + connection.on('close', () => { + liveConnections.delete(connection); + }); + }); + + const assertAbort = async (socket, testName) => { + try { + await once(socket, 'close'); + assert.fail(`close ${testName} should have thrown`); + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } + }; + + async function postAbort() { + const ac = new AbortController(); + const { signal } = ac; + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + ac.abort(); + await assertAbort(socket, 'postAbort'); + } + + async function preAbort() { + const ac = new AbortController(); + const { signal } = ac; + ac.abort(); + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + await assertAbort(socket, 'preAbort'); + } + + async function tickAbort() { + const ac = new AbortController(); + const { signal } = ac; + setImmediate(() => ac.abort()); + const socket = net.connect(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + await assertAbort(socket, 'tickAbort'); + } + + async function testConstructor() { + const ac = new AbortController(); + const { signal } = ac; + ac.abort(); + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + await assertAbort(socket, 'testConstructor'); + } + + async function testConstructorPost() { + const ac = new AbortController(); + const { signal } = ac; + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + ac.abort(); + await assertAbort(socket, 'testConstructorPost'); + } + + async function testConstructorPostTick() { + const ac = new AbortController(); + const { signal } = ac; + const socket = new net.Socket(socketOptions(signal)); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + setImmediate(() => ac.abort()); + await assertAbort(socket, 'testConstructorPostTick'); + } + + await postAbort(); + await preAbort(); + await tickAbort(); + await testConstructor(); + await testConstructorPost(); + await testConstructorPostTick(); + + // Killing the net.socket without connecting hangs the server. + for (const connection of liveConnections) { + connection.destroy(); + } + server.close(common.mustCall()); +})); diff --git a/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js new file mode 100644 index 00000000000000..1a7f6808fe1780 --- /dev/null +++ b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js @@ -0,0 +1,21 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); + +// This tests that the prototype accessors added by StreamBase::AddMethods +// are not enumerable. They could be enumerated when inspecting the prototype +// with util.inspect or the inspector protocol. + +const assert = require('assert'); + +// Or anything that calls StreamBase::AddMethods when setting up its prototype +const { internalBinding } = require('internal/test/binding'); +const TTY = internalBinding('tty_wrap').TTY; + +{ + const ttyIsEnumerable = Object.prototype.propertyIsEnumerable.bind(TTY); + assert.strictEqual(ttyIsEnumerable('bytesRead'), false); + assert.strictEqual(ttyIsEnumerable('fd'), false); + assert.strictEqual(ttyIsEnumerable('_externalStream'), false); +} diff --git a/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js b/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js new file mode 100644 index 00000000000000..c6a51c25b309f6 --- /dev/null +++ b/test/js/node/test/parallel/test-timers-not-emit-duration-zero.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +function timerNotCanceled() { + assert.fail('Timer should be canceled'); +} + +process.on( + 'warning', + common.mustNotCall(() => { + assert.fail('Timer should be canceled'); + }) +); + +{ + const timeout = setTimeout(timerNotCanceled, 0); + clearTimeout(timeout); +} + +{ + const interval = setInterval(timerNotCanceled, 0); + clearInterval(interval); +} + +{ + const timeout = setTimeout(timerNotCanceled, 0); + timeout.refresh(); + clearTimeout(timeout); +} diff --git a/test/regression/issue/09555.test.ts b/test/regression/issue/09555.test.ts index ef2fc27b58ff80..52868bae84aa1d 100644 --- a/test/regression/issue/09555.test.ts +++ b/test/regression/issue/09555.test.ts @@ -28,8 +28,8 @@ describe("#09555", () => { let total = 0; const res = await fetch(server.url.href); - const stream = Readable.fromWeb(res.body); - let chunks = []; + const stream = Readable.fromWeb(res.body!); + let chunks: any[] = []; for await (const chunk of stream) { total += chunk.length; chunks.push(chunk); diff --git a/test/regression/issue/09559.test.ts b/test/regression/issue/09559.test.ts index 3399ec30225b0e..f462e6845ee62b 100644 --- a/test/regression/issue/09559.test.ts +++ b/test/regression/issue/09559.test.ts @@ -12,7 +12,6 @@ test("bun build --target bun should support non-ascii source", async () => { console.log(JSON.stringify({\u{6211}})); `, }; - const filenames = Object.keys(files); const source = tempDirWithFiles("source", files); $.throws(true); diff --git a/test/tsconfig.json b/test/tsconfig.json index cc96fd4e479fa1..c43516cf9a12ae 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,24 +1,7 @@ { - "include": [".", "../packages/bun-types/index.d.ts", "./testing-internals.d.ts"], + "extends": "../tsconfig.base.json", "compilerOptions": { - "lib": ["ESNext"], - "module": "ESNext", - "target": "ESNext", - "moduleResolution": "bundler", - "moduleDetection": "force", - "allowImportingTsExtensions": true, - "experimentalDecorators": true, - "noEmit": true, - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "resolveJsonModule": true, - "noImplicitThis": false, + // Path remapping "baseUrl": ".", "paths": { "harness": ["harness.ts"], @@ -29,8 +12,23 @@ "foo/bar": ["js/bun/resolve/baz.js"], "@faasjs/*": ["js/bun/resolve/*.js", "js/bun/resolve/*/src/index.js"], "@faasjs/larger/*": ["js/bun/resolve/*/larger-index.js"] - } + }, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, - - "exclude": ["bundler/fixtures", "snapshots", "js/deno"] + "include": [ + // + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.cts", + "../src/js/internal-for-testing.ts" + ], + "exclude": [ + "fixtures", + "__snapshots__", // bun snapshots (toMatchSnapshot) + "./snapshots", + "./js/deno", + "./node.js" // entire node.js upstream repository + ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index d186c359deee14..a28d20e3fae023 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,19 +1,38 @@ { "compilerOptions": { + "composite": true, + + // Enable latest features "lib": ["ESNext"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "Bundler", + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "resolveJsonModule": true, + + // Bundler mode + "moduleResolution": "bundler", "allowImportingTsExtensions": true, + // TODO: enable this + // "verbatimModuleSyntax": true, "noEmit": true, + + // Best practices "strict": true, - "noImplicitAny": false, - "allowJs": true, - "downlevelIteration": true, - "esModuleInterop": true, "skipLibCheck": true, - "jsx": "react-jsx", + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + + // Stricter type-checking + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "noImplicitAny": false, + "noImplicitThis": false, - "typeRoots": ["./packages"] + // Enable decorators + "experimentalDecorators": true, + "emitDecoratorMetadata": true } } diff --git a/tsconfig.json b/tsconfig.json index e1e46276580557..c3ec51e2a1cb7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,18 @@ { - "extends": "./tsconfig.base.json", + "files": [], + "include": [], "compilerOptions": { + "noEmit": true, + "skipLibCheck": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true, - // "skipLibCheck": true, - "allowJs": true + "emitDecoratorMetadata": true }, - "include": [".", "packages/bun-types/index.d.ts"], - "exclude": [ - "src/test", - "src/js/out", - // "src/js/builtins", - "packages", - "bench", - "examples/*/*", - "build", - ".zig-cache", - "test", - "vendor", - "bun-webkit", - "src/api/demo", - "node_modules" - ], - "files": ["src/js/builtins.d.ts"] + "references": [ + // + { "path": "./src" }, + { "path": "./src/bake" }, + { "path": "./src/js" }, + { "path": "./test" }, + { "path": "./packages/bun-types" } + ] }